Skip to content

Conversation

KJTsanaktsidis
Copy link
Contributor

@KJTsanaktsidis KJTsanaktsidis commented Sep 27, 2025

Fixes #3653

Changes

This commit adds a pair of methods Meter::RegisterCallback and Meter::DeregisterCallback which allow you to set up one callback which can record observations for multiple observable instruments.

The callback will be passed an object of type MultiObserverResult, which can be used to get an ObserverResultT for an instrument.

This allows a single expensive operation to yield multiple different observable metrics without the need to call said expensive operation multiple times.

There's a test demonstrating the usage, but the short version is

void callback(metrics::MultiObserverResult &result, void *state)
{
    auto instrument1 = reinterpret_cast<MyState *>(state)->instrument1;
    auto instrument2 = reinterpret_cast<MyState *>(state)->instrument2;

   result.ForInstrument<int64_t>(instrument1).Observe(32, {"some", "labels"});
   resullt.ForInstrument<double>(instrument2).Observe(3234.32);
}

auto instrument1 = meter->CreateInt64ObservableCounter(...);
auto instrument2 = meter->CreateDoubleObservableGauge(...);

MyState state{instrument1, instrument2};
std::array<opentelemetry::metrics::ObservableInstrument *, 2> reg_instruments{instrument1, instrument2};
meter->RegisterCallback(callback, state, reg_instruments);

Design notes

  • I picked the names RegisterCallback based on the Go SDK, which exposes a RegsiterCallback method on the Meter for this purpose. Is this the right name? Is Meter scope the right place to put this?
  • The callback is required to have access to a opentelemetry::metrics::ObservableInstrument pointer somehow through state in order to record a measurement. This is in line with the interface sketched out in the spec - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#multiple-instrument-callbacks
  • I felt pretty silly defining the variant ObserverResultDirect = std::variant<ObserverResultT<double>, ObserverResultT<int64_t>> when we already have ObserverResult = std::variant<shared_ptr<ObserverResultT<double>>, shared_ptr<ObserverResultT<int64_t>> but it does get rid of a needless extra heap allocation in the multi instrument case so it's probably for the best?
  • I don't love the fact that the MultiObserverResult has protected ForInstrumentDouble and ForInstrumentInt64; this is needed because you can't have a virtual template method, and I wanted the recording interface to look like res.ForInstrument<int64_t>(instrument).Observe(value). We could make ForInstrument return a variant, but that would require a) exposing ObserverResultDirect as part of the API, and b) requiring the calling code to std::get or std::visit that.
  • There is in fact no actual reason why this interface requires you to pre-register the instruments to be observed ahead of time; the code could perfectly happily add them on the fly in calls to ForInstrument. However this seems to be what the spec describes, so I've stuck to the spec.

Very keen to hear what you think!

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

Copy link

codecov bot commented Sep 27, 2025

Codecov Report

❌ Patch coverage is 80.91603% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.93%. Comparing base (9b38749) to head (e7d7e9d).

Files with missing lines Patch % Lines
sdk/src/metrics/state/observable_registry.cc 76.72% 17 Missing ⚠️
sdk/src/metrics/multi_observer_result.cc 84.91% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3667      +/-   ##
==========================================
- Coverage   90.14%   89.93%   -0.20%     
==========================================
  Files         222      224       +2     
  Lines        7154     7255     +101     
==========================================
+ Hits         6448     6524      +76     
- Misses        706      731      +25     
Files with missing lines Coverage Δ
api/include/opentelemetry/metrics/meter.h 100.00% <ø> (ø)
...lude/opentelemetry/metrics/multi_observer_result.h 100.00% <100.00%> (ø)
api/include/opentelemetry/metrics/noop.h 94.12% <ø> (ø)
sdk/include/opentelemetry/sdk/metrics/meter.h 57.15% <ø> (ø)
sdk/src/metrics/meter.cc 85.89% <ø> (ø)
sdk/src/metrics/multi_observer_result.cc 84.91% <84.91%> (ø)
sdk/src/metrics/state/observable_registry.cc 77.11% <76.72%> (-18.24%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch 5 times, most recently from 67d32ef to af792aa Compare September 27, 2025 04:14
@owent
Copy link
Member

owent commented Sep 28, 2025

Is it possiable to drop callbacks_ and allow constructing MultiObservableCallbackRecord from ObservableCallbackRecord? Maintaining multiple types is cumbersome for users.

@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch from af792aa to c182ada Compare September 28, 2025 05:32
@KJTsanaktsidis
Copy link
Contributor Author

Is it possiable to drop callbacks_ and allow constructing MultiObservableCallbackRecord from ObservableCallbackRecord?

Is the ask here to essentially do...

class ObservableRegistry {
private:
#if OPENTELEMETRY_ABI_VERSION_NO >= 2
    std::unordered_map<uintptr_t, std::unique_ptr<MultiObservableCallbackRecord>> multi_callbacks_;
#else
  std::vector<std::unique_ptr<ObservableCallbackRecord>> callbacks_;
#endif
  std::mutex callbacks_m_;
}

i.e. to re-implement the existing single-observable callback functionality in terms of the multi-observable callback functionality? Yes, I can do this - let me know if I understood correctly

Maintaining multiple types is cumbersome for users.

This I didn't fully understand, because ObservableCallbackRecord and MultiObservableCallbackRecord are implementation details of the SDK ObservableRegistry; users shouldn't be touching these classes, I thought?

(In fact: the definition of MultiObservableCallbackRecord could be moved out of the header and into observable_registry.cc, by type-erasing std::unordered_map<uintptr_t, std::unique_ptr<MultiObservableCallbackRecord>> multi_callbacks_ with a shared_ptr or a unique_ptr with a deleter function instead. I think I'll do this now, it seems worthwhile not to expose types that aren't needed).

@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch 4 times, most recently from d75abed to f76d7db Compare September 29, 2025 02:26
@KJTsanaktsidis
Copy link
Contributor Author

Oh, one more thought; if ObservableRegistry can be thought of as a part of the internal API/ABI of the SDK, I could probably dispense with the ABI version guards in it (since symbols from ObservableRegistry would only be used inside the SDK library itself).

this would allow unconditionally reimplementing the existing single-observable callback in terms of the multi-observable implementation.

Reckon I should do this?

@lalitb
Copy link
Member

lalitb commented Sep 29, 2025

Yes, it should be safe to do ABI breaking changes with ObservableRegistry.

@owent
Copy link
Member

owent commented Sep 29, 2025

Is it possiable to drop callbacks_ and allow constructing MultiObservableCallbackRecord from ObservableCallbackRecord?

Is the ask here to essentially do...

class ObservableRegistry {
private:
#if OPENTELEMETRY_ABI_VERSION_NO >= 2
    std::unordered_map<uintptr_t, std::unique_ptr<MultiObservableCallbackRecord>> multi_callbacks_;
#else
  std::vector<std::unique_ptr<ObservableCallbackRecord>> callbacks_;
#endif
  std::mutex callbacks_m_;
}

i.e. to re-implement the existing single-observable callback functionality in terms of the multi-observable callback functionality? Yes, I can do this - let me know if I understood correctly

Yes, that's what I want.

Maintaining multiple types is cumbersome for users.

This I didn't fully understand, because ObservableCallbackRecord and MultiObservableCallbackRecord are implementation details of the SDK ObservableRegistry; users shouldn't be touching these classes, I thought?

We maintain cache of registered callbacks in our project, which allow us to do lazy registration, reload , cleanup and some others things, another group of types will make us to double the cache and APIs to maintain them.

BTW: It's OK to break ABI or API in v2 now.

@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch 4 times, most recently from 45c9861 to 0abaaf5 Compare September 29, 2025 09:21
@KJTsanaktsidis
Copy link
Contributor Author

Yes, that's what I want.

OK, awesome - I think I've done that, hope this looks right to you 🙏

@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch 2 times, most recently from 5e5199f to 6256773 Compare September 30, 2025 07:01
This commit adds a pair of methods `Meter::RegisterCallback` and
`Meter::DeregisterCallback` which allow you to set up one callback
which can record observations for multiple observable instruments.

The callback will be passed an object of type MultiObserverResult, which
can be used to get an ObserverResultT for an instrument.

This allows a single expensive operation to yield multiple different
observable metrics without the need to call said expensive operation
multiple times.
@KJTsanaktsidis KJTsanaktsidis force-pushed the kjtsanaktsidis/multi_observable branch from 6256773 to 3c168b2 Compare September 30, 2025 10:40
@lalitb lalitb requested a review from Copilot October 2, 2025 18:29
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for registering a single callback function to observe multiple observable instruments, addressing the need to avoid redundant expensive operations when generating multiple related metrics. This implements the multi-instrument callback pattern described in the OpenTelemetry specification.

Key changes include:

  • Implementation of Meter::RegisterCallback() and Meter::DeregisterCallback() methods that work with multiple instruments
  • New MultiObserverResult API that allows callbacks to record observations for different instruments
  • Backward-compatible changes that preserve existing single-instrument callback functionality

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
api/include/opentelemetry/metrics/meter.h Adds RegisterCallback/DeregisterCallback method signatures for multi-instrument callbacks
api/include/opentelemetry/metrics/multi_observer_result.h New API class defining the interface for recording observations across multiple instruments
api/include/opentelemetry/metrics/noop.h No-op implementation of the new RegisterCallback/DeregisterCallback methods
sdk/include/opentelemetry/sdk/metrics/meter.h SDK implementation declarations for the new callback methods
sdk/include/opentelemetry/sdk/metrics/multi_observer_result.h SDK concrete implementation of MultiObserverResult
sdk/include/opentelemetry/sdk/metrics/state/observable_registry.h Updated registry interface to support multi-instrument callbacks
sdk/src/metrics/meter.cc Implementation of RegisterCallback/DeregisterCallback in the SDK meter
sdk/src/metrics/multi_observer_result.cc Implementation of the MultiObserverResult functionality
sdk/src/metrics/state/observable_registry.cc Refactored registry to handle both single and multi-instrument callback patterns
sdk/src/metrics/CMakeLists.txt Added multi_observer_result.cc to the build
sdk/test/metrics/multi_observer_test.cc Comprehensive tests for the new multi-instrument callback functionality
sdk/test/metrics/CMakeLists.txt Added multi_observer_test to the test suite
CHANGELOG.md Documents the new feature
Comments suppressed due to low confidence (1)

api/include/opentelemetry/metrics/multi_observer_result.h:1

  • Corrected spelling of 'unnescessary' to 'unnecessary'.
// Copyright The OpenTelemetry Authors

namespace
{

struct CallbackDeregisterdWhenDestroyedState
Copy link
Preview

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'CallbackDeregisterdWhenDestroyedState' to 'CallbackDeregisteredWhenDestroyedState'.

Copilot uses AI. Check for mistakes.

void callback_deregistered_when_destroyed_callback(metrics::MultiObserverResult &result,
void *vstate)
{
auto state = reinterpret_cast<CallbackDeregisterdWhenDestroyedState *>(vstate);
Copy link
Preview

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The struct name should be corrected to match the fixed spelling: 'CallbackDeregisteredWhenDestroyedState'.

Copilot uses AI. Check for mistakes.

MetricReader *metric_reader_ptr = nullptr;
auto meter = InitMeter(&metric_reader_ptr, "callback_deregistered_when_destroyed_test");

CallbackDeregisterdWhenDestroyedState state{};
Copy link
Preview

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The struct name should be corrected to match the fixed spelling: 'CallbackDeregisteredWhenDestroyedState'.

Copilot uses AI. Check for mistakes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A mechanism for updating multiple ObservableIntruments in one callback
3 participants