diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff2a1ff3e..ad92cf02b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2207](https://github.com/open-telemetry/opentelemetry-python/pull/2207)) - remove `X-B3-ParentSpanId` for B3 propagator as per OpenTelemetry specification ([#2237](https://github.com/open-telemetry/opentelemetry-python/pull/2237)) +- Return proxy instruments from ProxyMeter + [[#2169](https://github.com/open-telemetry/opentelemetry-python/pull/2169)] - Make Measurement a concrete class ([#2153](https://github.com/open-telemetry/opentelemetry-python/pull/2153)) - Add metrics API diff --git a/docs/conf.py b/docs/conf.py index 6559298b8e..f79b12db0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,6 +98,8 @@ nitpick_ignore = [ ("py:class", "ValueT"), ("py:class", "MetricT"), + ("py:class", "InstrumentT"), + ("py:obj", "opentelemetry.metrics.instrument.InstrumentT"), # Even if wrapt is added to intersphinx_mapping, sphinx keeps failing # with "class reference target not found: ObjectProxy". ("py:class", "ObjectProxy"), diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 2a60f0e202..2cf15cb267 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -25,7 +25,8 @@ from abc import ABC, abstractmethod from logging import getLogger from os import environ -from typing import Optional, cast +from threading import Lock +from typing import List, Optional, cast from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics.instrument import ( @@ -41,7 +42,15 @@ ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyInstrument, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, ) +from opentelemetry.util._once import Once from opentelemetry.util._providers import _load_provider _logger = getLogger(__name__) @@ -69,18 +78,33 @@ def get_meter( return _DefaultMeter(name, version=version, schema_url=schema_url) -class ProxyMeterProvider(MeterProvider): +class _ProxyMeterProvider(MeterProvider): + def __init__(self) -> None: + self._lock = Lock() + self._meters: List[_ProxyMeter] = [] + self._real_meter_provider: Optional[MeterProvider] = None + def get_meter( self, name, version=None, schema_url=None, ) -> "Meter": - if _METER_PROVIDER: - return _METER_PROVIDER.get_meter( - name, version=version, schema_url=schema_url - ) - return ProxyMeter(name, version=version, schema_url=schema_url) + with self._lock: + if self._real_meter_provider is not None: + return self._real_meter_provider.get_meter( + name, version, schema_url + ) + + meter = _ProxyMeter(name, version=version, schema_url=schema_url) + self._meters.append(meter) + return meter + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + with self._lock: + self._real_meter_provider = meter_provider + for meter in self._meters: + meter.on_set_meter_provider(meter_provider) class Meter(ABC): @@ -215,7 +239,7 @@ def create_observable_up_down_counter( pass -class ProxyMeter(Meter): +class _ProxyMeter(Meter): def __init__( self, name, @@ -223,43 +247,101 @@ def __init__( schema_url=None, ): super().__init__(name, version=version, schema_url=schema_url) + self._lock = Lock() + self._instruments: List[_ProxyInstrument] = [] self._real_meter: Optional[Meter] = None - self._noop_meter = _DefaultMeter( - name, version=version, schema_url=schema_url + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + """Called when a real meter provider is set on the creating _ProxyMeterProvider + + Creates a real backing meter for this instance and notifies all created + instruments so they can create real backing instruments. + """ + real_meter = meter_provider.get_meter( + self._name, self._version, self._schema_url ) - @property - def _meter(self) -> Meter: - if self._real_meter is not None: - return self._real_meter - - if _METER_PROVIDER: - self._real_meter = _METER_PROVIDER.get_meter( - self._name, - self._version, - ) - return self._real_meter - return self._noop_meter + with self._lock: + self._real_meter = real_meter + # notify all proxy instruments of the new meter so they can create + # real instruments to back themselves + for instrument in self._instruments: + instrument.on_meter_set(real_meter) - def create_counter(self, *args, **kwargs) -> Counter: - return self._meter.create_counter(*args, **kwargs) + def create_counter(self, name, unit="", description="") -> Counter: + with self._lock: + if self._real_meter: + return self._real_meter.create_counter(name, unit, description) + proxy = _ProxyCounter(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_up_down_counter(self, *args, **kwargs) -> UpDownCounter: - return self._meter.create_up_down_counter(*args, **kwargs) + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_up_down_counter( + name, unit, description + ) + proxy = _ProxyUpDownCounter(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_observable_counter(self, *args, **kwargs) -> ObservableCounter: - return self._meter.create_observable_counter(*args, **kwargs) + def create_observable_counter( + self, name, callback, unit="", description="" + ) -> ObservableCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_counter( + name, callback, unit, description + ) + proxy = _ProxyObservableCounter( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy - def create_histogram(self, *args, **kwargs) -> Histogram: - return self._meter.create_histogram(*args, **kwargs) + def create_histogram(self, name, unit="", description="") -> Histogram: + with self._lock: + if self._real_meter: + return self._real_meter.create_histogram( + name, unit, description + ) + proxy = _ProxyHistogram(name, unit, description) + self._instruments.append(proxy) + return proxy - def create_observable_gauge(self, *args, **kwargs) -> ObservableGauge: - return self._meter.create_observable_gauge(*args, **kwargs) + def create_observable_gauge( + self, name, callback, unit="", description="" + ) -> ObservableGauge: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_gauge( + name, callback, unit, description + ) + proxy = _ProxyObservableGauge( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy def create_observable_up_down_counter( - self, *args, **kwargs + self, name, callback, unit="", description="" ) -> ObservableUpDownCounter: - return self._meter.create_observable_up_down_counter(*args, **kwargs) + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_up_down_counter( + name, + callback, + unit, + description, + ) + proxy = _ProxyObservableUpDownCounter( + name, callback, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy class _DefaultMeter(Meter): @@ -319,8 +401,9 @@ def create_observable_up_down_counter( ) -_METER_PROVIDER = None -_PROXY_METER_PROVIDER = None +_METER_PROVIDER_SET_ONCE = Once() +_METER_PROVIDER: Optional[MeterProvider] = None +_PROXY_METER_PROVIDER = _ProxyMeterProvider() def get_meter( @@ -340,35 +423,40 @@ def get_meter( return meter_provider.get_meter(name, version) +def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: + def set_mp() -> None: + global _METER_PROVIDER # pylint: disable=global-statement + _METER_PROVIDER = meter_provider + + # gives all proxies real instruments off the newly set meter provider + _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) + + did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) + + if log and not did_set: + _logger.warning("Overriding of current MeterProvider is not allowed") + + def set_meter_provider(meter_provider: MeterProvider) -> None: """Sets the current global :class:`~.MeterProvider` object. This can only be done once, a warning will be logged if any furter attempt is made. """ - global _METER_PROVIDER # pylint: disable=global-statement - - if _METER_PROVIDER is not None: - _logger.warning("Overriding of current MeterProvider is not allowed") - return - - _METER_PROVIDER = meter_provider + _set_meter_provider(meter_provider, log=True) def get_meter_provider() -> MeterProvider: """Gets the current global :class:`~.MeterProvider` object.""" - # pylint: disable=global-statement - global _METER_PROVIDER - global _PROXY_METER_PROVIDER if _METER_PROVIDER is None: if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): - if _PROXY_METER_PROVIDER is None: - _PROXY_METER_PROVIDER = ProxyMeterProvider() return _PROXY_METER_PROVIDER - _METER_PROVIDER = cast( - "MeterProvider", - _load_provider(OTEL_PYTHON_METER_PROVIDER, "meter_provider"), + meter_provider: MeterProvider = _load_provider( + OTEL_PYTHON_METER_PROVIDER, "meter_provider" ) - return _METER_PROVIDER + _set_meter_provider(meter_provider, log=False) + + # _METER_PROVIDER will have been set by one thread + return cast("MeterProvider", _METER_PROVIDER) diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/instrument.py index 63e9dddf91..ce93b4ac45 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/instrument.py @@ -19,13 +19,24 @@ from abc import ABC, abstractmethod from collections import abc as collections_abc from logging import getLogger -from typing import Callable, Generator, Iterable, Union - +from typing import ( + Callable, + Generator, + Generic, + Iterable, + Optional, + TypeVar, + Union, +) + +# pylint: disable=unused-import; needed for typing and sphinx +from opentelemetry import metrics from opentelemetry.metrics.measurement import Measurement _TInstrumentCallback = Callable[[], Iterable[Measurement]] _TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None] TCallback = Union[_TInstrumentCallback, _TInstrumentCallbackGenerator] +InstrumentT = TypeVar("InstrumentT", bound="Instrument") _logger = getLogger(__name__) @@ -41,6 +52,32 @@ def __init__(self, name, unit="", description=""): # FIXME check that the unit contains only ASCII characters +class _ProxyInstrument(ABC, Generic[InstrumentT]): + def __init__(self, name, unit, description) -> None: + self._name = name + self._unit = unit + self._description = description + self._real_instrument: Optional[InstrumentT] = None + + def on_meter_set(self, meter: "metrics.Meter") -> None: + """Called when a real meter is set on the creating _ProxyMeter""" + + # We don't need any locking on proxy instruments because it's OK if some + # measurements get dropped while a real backing instrument is being + # created. + self._real_instrument = self._create_real_instrument(meter) + + @abstractmethod + def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: + """Create an instance of the real instrument. Implement this.""" + + +class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): + def __init__(self, name, callback, unit, description) -> None: + super().__init__(name, unit, description) + self._callback = callback + + class Synchronous(Instrument): pass @@ -122,6 +159,15 @@ def add(self, amount, attributes=None): return super().add(amount, attributes=attributes) +class _ProxyCounter(_ProxyInstrument[Counter], Counter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: + return meter.create_counter(self._name, self._unit, self._description) + + class UpDownCounter(_NonMonotonic, Synchronous): @abstractmethod def add(self, amount, attributes=None): @@ -136,6 +182,17 @@ def add(self, amount, attributes=None): return super().add(amount, attributes=attributes) +class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: + return meter.create_up_down_counter( + self._name, self._unit, self._description + ) + + class ObservableCounter(_Monotonic, Asynchronous): pass @@ -145,8 +202,18 @@ def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) -class ObservableUpDownCounter(_NonMonotonic, Asynchronous): +class _ProxyObservableCounter( + _ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableCounter: + return meter.create_observable_counter( + self._name, self._callback, self._unit, self._description + ) + +class ObservableUpDownCounter(_NonMonotonic, Asynchronous): pass @@ -155,6 +222,18 @@ def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) +class _ProxyObservableUpDownCounter( + _ProxyAsynchronousInstrument[ObservableUpDownCounter], + ObservableUpDownCounter, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableUpDownCounter: + return meter.create_observable_up_down_counter( + self._name, self._callback, self._unit, self._description + ) + + class Histogram(_Grouping, Synchronous): @abstractmethod def record(self, amount, attributes=None): @@ -169,6 +248,17 @@ def record(self, amount, attributes=None): return super().record(amount, attributes=attributes) +class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): + def record(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.record(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: + return meter.create_histogram( + self._name, self._unit, self._description + ) + + class ObservableGauge(_Grouping, Asynchronous): pass @@ -176,3 +266,15 @@ class ObservableGauge(_Grouping, Asynchronous): class DefaultObservableGauge(ObservableGauge): def __init__(self, name, callback, unit="", description=""): super().__init__(name, callback, unit=unit, description=description) + + +class _ProxyObservableGauge( + _ProxyAsynchronousInstrument[ObservableGauge], + ObservableGauge, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableGauge: + return meter.create_observable_gauge( + self._name, self._callback, self._unit, self._description + ) diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index c78de94cc7..15c4d7cea7 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -21,20 +21,24 @@ from opentelemetry import metrics from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER from opentelemetry.metrics import ( - ProxyMeter, - ProxyMeterProvider, _DefaultMeter, _DefaultMeterProvider, + _ProxyMeter, + _ProxyMeterProvider, get_meter_provider, set_meter_provider, ) from opentelemetry.metrics.instrument import ( - DefaultCounter, - DefaultHistogram, - DefaultObservableCounter, - DefaultObservableGauge, - DefaultObservableUpDownCounter, - DefaultUpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +) +from opentelemetry.test.globals_test import ( + MetricsGlobalsTest, + reset_metrics_globals, ) # FIXME Test that the instrument methods can be called concurrently safely. @@ -42,11 +46,9 @@ @fixture def reset_meter_provider(): - original_meter_provider_value = metrics._METER_PROVIDER - + reset_metrics_globals() yield - - metrics._METER_PROVIDER = original_meter_provider_value + reset_metrics_globals() def test_set_meter_provider(reset_meter_provider): @@ -61,6 +63,16 @@ def test_set_meter_provider(reset_meter_provider): set_meter_provider(mock) assert metrics._METER_PROVIDER is mock + assert get_meter_provider() is mock + + +def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): + with patch("opentelemetry.metrics._PROXY_METER_PROVIDER") as mock_proxy_mp: + mock_real_mp = Mock() + set_meter_provider(mock_real_mp) + mock_proxy_mp.on_set_meter_provider.assert_called_once_with( + mock_real_mp + ) def test_get_meter_provider(reset_meter_provider): @@ -70,7 +82,7 @@ def test_get_meter_provider(reset_meter_provider): assert metrics._METER_PROVIDER is None - assert isinstance(get_meter_provider(), ProxyMeterProvider) + assert isinstance(get_meter_provider(), _ProxyMeterProvider) metrics._METER_PROVIDER = None @@ -122,136 +134,180 @@ def test_invalid_name(self): self.assertEqual(meter.name, None) -class MockProvider(_DefaultMeterProvider): - def get_meter(self, name, version=None, schema_url=None): - return MockMeter(name, version=version, schema_url=schema_url) - - -class MockMeter(_DefaultMeter): - def create_counter(self, name, unit="", description=""): - return MockCounter("name") - - def create_up_down_counter(self, name, unit="", description=""): - return MockUpDownCounter("name") - - def create_observable_counter( - self, name, callback, unit="", description="" - ): - return MockObservableCounter("name", callback) - - def create_histogram(self, name, unit="", description=""): - return MockHistogram("name") - - def create_observable_gauge(self, name, callback, unit="", description=""): - return MockObservableGauge("name", callback) - - def create_observable_up_down_counter( - self, name, callback, unit="", description="" - ): - return MockObservableUpDownCounter("name", callback) - - -class MockCounter(DefaultCounter): - pass +class TestProxy(MetricsGlobalsTest, TestCase): + def test_global_proxy_meter_provider(self): + # Global get_meter_provider() should initially be a _ProxyMeterProvider + # singleton + proxy_meter_provider: _ProxyMeterProvider = get_meter_provider() + self.assertIsInstance(proxy_meter_provider, _ProxyMeterProvider) + self.assertIs(get_meter_provider(), proxy_meter_provider) -class MockHistogram(DefaultHistogram): - pass + def test_proxy_provider(self): + proxy_meter_provider = _ProxyMeterProvider() + # Should return a proxy meter when no real MeterProvider is set + name = "foo" + version = "1.2" + schema_url = "schema_url" + proxy_meter: _ProxyMeter = proxy_meter_provider.get_meter( + name, version=version, schema_url=schema_url + ) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + # After setting a real meter provider on the proxy, it should notify + # it's _ProxyMeters which should create their own real Meters + mock_real_mp = Mock() + proxy_meter_provider.on_set_meter_provider(mock_real_mp) + mock_real_mp.get_meter.assert_called_once_with( + name, version, schema_url + ) -class MockObservableCounter(DefaultObservableCounter): - pass - - -class MockObservableGauge(DefaultObservableGauge): - pass - - -class MockObservableUpDownCounter(DefaultObservableUpDownCounter): - pass - - -class MockUpDownCounter(DefaultUpDownCounter): - pass - + # After setting a real meter provider on the proxy, it should now return + # new meters directly from the set real meter + another_name = "bar" + meter2 = proxy_meter_provider.get_meter(another_name) + self.assertIsInstance(meter2, Mock) + mock_real_mp.get_meter.assert_called_with(another_name, None, None) -class TestProxy(TestCase): + # pylint: disable=too-many-locals def test_proxy_meter(self): - - """ - Test that the proxy meter provider and proxy meter automatically point - to updated objects. - """ - - original_provider = metrics._METER_PROVIDER - - provider = get_meter_provider() - self.assertIsInstance(provider, ProxyMeterProvider) - - meter = provider.get_meter("proxy-test") - self.assertIsInstance(meter, ProxyMeter) - - self.assertIsInstance(meter.create_counter("counter0"), DefaultCounter) - - self.assertIsInstance( - meter.create_histogram("histogram0"), DefaultHistogram + meter_name = "foo" + proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + # Should be able to create proxy instruments + name = "foo" + unit = "s" + description = "Foobar" + callback = Mock() + proxy_counter = proxy_meter.create_counter( + name, unit=unit, description=description ) - - def callback(): - yield - - self.assertIsInstance( - meter.create_observable_counter("observable_counter0", callback()), - DefaultObservableCounter, + proxy_updowncounter = proxy_meter.create_up_down_counter( + name, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_observable_gauge("observable_gauge0", callback()), - DefaultObservableGauge, + proxy_histogram = proxy_meter.create_histogram( + name, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_observable_up_down_counter( - "observable_up_down_counter0", callback() - ), - DefaultObservableUpDownCounter, + proxy_observable_counter = proxy_meter.create_observable_counter( + name, callback=callback, unit=unit, description=description ) - - self.assertIsInstance( - meter.create_up_down_counter("up_down_counter0"), - DefaultUpDownCounter, + proxy_observable_updowncounter = ( + proxy_meter.create_observable_up_down_counter( + name, callback=callback, unit=unit, description=description + ) ) - - set_meter_provider(MockProvider()) - - self.assertIsInstance(get_meter_provider(), MockProvider) - self.assertIsInstance(provider.get_meter("proxy-test"), MockMeter) - - self.assertIsInstance(meter.create_counter("counter1"), MockCounter) - - self.assertIsInstance( - meter.create_histogram("histogram1"), MockHistogram + proxy_overvable_gauge = proxy_meter.create_observable_gauge( + name, callback=callback, unit=unit, description=description ) - + self.assertIsInstance(proxy_counter, _ProxyCounter) + self.assertIsInstance(proxy_updowncounter, _ProxyUpDownCounter) + self.assertIsInstance(proxy_histogram, _ProxyHistogram) self.assertIsInstance( - meter.create_observable_counter("observable_counter1", callback()), - MockObservableCounter, + proxy_observable_counter, _ProxyObservableCounter ) - self.assertIsInstance( - meter.create_observable_gauge("observable_gauge1", callback()), - MockObservableGauge, + proxy_observable_updowncounter, _ProxyObservableUpDownCounter + ) + self.assertIsInstance(proxy_overvable_gauge, _ProxyObservableGauge) + + # Synchronous proxy instruments should be usable + amount = 12 + attributes = {"foo": "bar"} + proxy_counter.add(amount, attributes=attributes) + proxy_updowncounter.add(amount, attributes=attributes) + proxy_histogram.record(amount, attributes=attributes) + + # Calling _ProxyMeterProvider.on_set_meter_provider() should cascade down + # to the _ProxyInstruments which should create their own real instruments + # from the real Meter to back their calls + real_meter_provider = Mock() + proxy_meter.on_set_meter_provider(real_meter_provider) + real_meter_provider.get_meter.assert_called_once_with( + meter_name, None, None ) - self.assertIsInstance( - meter.create_observable_up_down_counter( - "observable_up_down_counter1", callback() - ), - MockObservableUpDownCounter, + real_meter: Mock = real_meter_provider.get_meter() + real_meter.create_counter.assert_called_once_with( + name, unit, description + ) + real_meter.create_up_down_counter.assert_called_once_with( + name, unit, description + ) + real_meter.create_histogram.assert_called_once_with( + name, unit, description + ) + real_meter.create_observable_counter.assert_called_once_with( + name, callback, unit, description + ) + real_meter.create_observable_up_down_counter.assert_called_once_with( + name, callback, unit, description + ) + real_meter.create_observable_gauge.assert_called_once_with( + name, callback, unit, description ) - self.assertIsInstance( - meter.create_up_down_counter("up_down_counter1"), MockUpDownCounter + # The synchronous instrument measurement methods should call through to + # the real instruments + real_counter: Mock = real_meter.create_counter() + real_updowncounter: Mock = real_meter.create_up_down_counter() + real_histogram: Mock = real_meter.create_histogram() + real_counter.assert_not_called() + real_updowncounter.assert_not_called() + real_histogram.assert_not_called() + + proxy_counter.add(amount, attributes=attributes) + real_counter.add.assert_called_once_with(amount, attributes) + proxy_updowncounter.add(amount, attributes=attributes) + real_updowncounter.add.assert_called_once_with(amount, attributes) + proxy_histogram.record(amount, attributes=attributes) + real_histogram.record.assert_called_once_with(amount, attributes) + + def test_proxy_meter_with_real_meter(self) -> None: + # Creating new instruments on the _ProxyMeter with a real meter set + # should create real instruments instead of proxies + meter_name = "foo" + proxy_meter: _ProxyMeter = _ProxyMeterProvider().get_meter(meter_name) + self.assertIsInstance(proxy_meter, _ProxyMeter) + + real_meter_provider = Mock() + proxy_meter.on_set_meter_provider(real_meter_provider) + + name = "foo" + unit = "s" + description = "Foobar" + callback = Mock() + counter = proxy_meter.create_counter( + name, unit=unit, description=description + ) + updowncounter = proxy_meter.create_up_down_counter( + name, unit=unit, description=description + ) + histogram = proxy_meter.create_histogram( + name, unit=unit, description=description + ) + observable_counter = proxy_meter.create_observable_counter( + name, callback=callback, unit=unit, description=description + ) + observable_updowncounter = ( + proxy_meter.create_observable_up_down_counter( + name, callback=callback, unit=unit, description=description + ) + ) + observable_gauge = proxy_meter.create_observable_gauge( + name, callback=callback, unit=unit, description=description ) - metrics._METER_PROVIDER = original_provider + real_meter: Mock = real_meter_provider.get_meter() + self.assertIs(counter, real_meter.create_counter()) + self.assertIs(updowncounter, real_meter.create_up_down_counter()) + self.assertIs(histogram, real_meter.create_histogram()) + self.assertIs( + observable_counter, real_meter.create_observable_counter() + ) + self.assertIs( + observable_updowncounter, + real_meter.create_observable_up_down_counter(), + ) + self.assertIs(observable_gauge, real_meter.create_observable_gauge()) diff --git a/tests/util/src/opentelemetry/test/globals_test.py b/tests/util/src/opentelemetry/test/globals_test.py index bb2cad6a0a..a2626116b1 100644 --- a/tests/util/src/opentelemetry/test/globals_test.py +++ b/tests/util/src/opentelemetry/test/globals_test.py @@ -14,6 +14,7 @@ import unittest +from opentelemetry import metrics as metrics_api from opentelemetry import trace as trace_api from opentelemetry.util._once import Once @@ -26,6 +27,14 @@ def reset_trace_globals() -> None: trace_api._PROXY_TRACER_PROVIDER = trace_api.ProxyTracerProvider() +# pylint: disable=protected-access +def reset_metrics_globals() -> None: + """WARNING: only use this for tests.""" + metrics_api._METER_PROVIDER_SET_ONCE = Once() # type: ignore[attr-defined] + metrics_api._METER_PROVIDER = None # type: ignore[attr-defined] + metrics_api._PROXY_METER_PROVIDER = metrics_api._ProxyMeterProvider() # type: ignore[attr-defined] + + class TraceGlobalsTest(unittest.TestCase): """Resets trace API globals in setUp/tearDown @@ -39,3 +48,18 @@ def setUp(self) -> None: def tearDown(self) -> None: super().tearDown() reset_trace_globals() + + +class MetricsGlobalsTest(unittest.TestCase): + """Resets metrics API globals in setUp/tearDown + + Use as a base class or mixin for your test that modifies metrics API globals. + """ + + def setUp(self) -> None: + super().setUp() + reset_metrics_globals() + + def tearDown(self) -> None: + super().tearDown() + reset_metrics_globals()