From b696c3b20f4b93b22391098024722f1b3f59a2d0 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 5 Feb 2024 08:12:22 -0600 Subject: [PATCH 1/2] Skip test for PyPy and Windows (#3670) This test fails randomly when ran under those conditions Fixes #3669 --- opentelemetry-sdk/tests/trace/test_span_processor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/test_span_processor.py b/opentelemetry-sdk/tests/trace/test_span_processor.py index b8568fc7a1b..c672d4ce102 100644 --- a/opentelemetry-sdk/tests/trace/test_span_processor.py +++ b/opentelemetry-sdk/tests/trace/test_span_processor.py @@ -16,10 +16,13 @@ import time import typing import unittest +from platform import python_implementation, system from threading import Event from typing import Optional from unittest import mock +from pytest import mark + from opentelemetry import trace as trace_api from opentelemetry.context import Context from opentelemetry.sdk import trace @@ -266,6 +269,10 @@ def create_multi_span_processor( ) -> trace.ConcurrentMultiSpanProcessor: return trace.ConcurrentMultiSpanProcessor(3) + @mark.skipif( + python_implementation() == "PyPy" and system() == "Windows", + reason="This test randomly fails in Windows with PyPy", + ) def test_force_flush_late_by_timeout(self): multi_processor = trace.ConcurrentMultiSpanProcessor(5) wait_event = Event() From 66e7d61ceba839c16d51b9297334ec8615cdf6a0 Mon Sep 17 00:00:00 2001 From: sarafonseca Date: Mon, 5 Feb 2024 15:42:03 -0300 Subject: [PATCH 2/2] Synchronous gauge prototype (#3462) * merge with cherry pick * docs/getting_started/metrics_example.py * Fix * edit changelog * Format * Update docs/getting_started/metrics_example.py Co-authored-by: Diego Hurtado * Make synchronous gauge private for opentelemetry API * Fix name of SDK Gauge * Fix docs --------- Co-authored-by: Diego Hurtado --- CHANGELOG.md | 2 + docs/conf.py | 11 ++++ docs/getting_started/metrics_example.py | 8 ++- .../src/opentelemetry/metrics/__init__.py | 10 ++++ .../metrics/_internal/__init__.py | 54 +++++++++++++++++++ .../metrics/_internal/instrument.py | 47 ++++++++++++++++ .../tests/metrics/test_instruments.py | 45 ++++++++++++++++ opentelemetry-api/tests/metrics/test_meter.py | 13 +++++ .../tests/metrics/test_meter_provider.py | 19 +++++++ .../src/opentelemetry/sdk/metrics/__init__.py | 4 +- .../sdk/metrics/_internal/__init__.py | 36 +++++++++++++ .../sdk/metrics/_internal/aggregation.py | 8 ++- .../sdk/metrics/_internal/export/__init__.py | 8 +++ .../sdk/metrics/_internal/instrument.py | 19 +++++++ .../tests/metrics/test_aggregation.py | 14 +++++ .../tests/metrics/test_import.py | 1 + .../tests/metrics/test_instrument.py | 19 +++++++ .../tests/metrics/test_metric_reader.py | 9 ++++ .../metrics/test_metric_reader_storage.py | 15 ++++++ .../tests/metrics/test_metrics.py | 23 ++++++++ 20 files changed, 361 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cadd831b9..b383bf0f844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Add Synchronous Gauge instrument + ([#3462](https://github.com/open-telemetry/opentelemetry-python/pull/3462)) - Drop support for 3.7 ([#3668](https://github.com/open-telemetry/opentelemetry-python/pull/3668)) - Include key in attribute sequence warning diff --git a/docs/conf.py b/docs/conf.py index 6e42aa1bd1f..352cf927cd4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -178,3 +178,14 @@ "scm_raw_web": (scm_raw_web + "/%s", "scm_raw_web"), "scm_web": (scm_web + "/%s", "scm_web"), } + + +def on_missing_reference(app, env, node, contnode): + # FIXME Remove when opentelemetry.metrics._Gauge is renamed to + # opentelemetry.metrics.Gauge + if node["reftarget"] == "opentelemetry.metrics.Gauge": + return contnode + + +def setup(app): + app.connect("missing-reference", on_missing_reference) diff --git a/docs/getting_started/metrics_example.py b/docs/getting_started/metrics_example.py index 83c9a1b8c45..85df5cc14ed 100644 --- a/docs/getting_started/metrics_example.py +++ b/docs/getting_started/metrics_example.py @@ -75,4 +75,10 @@ def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: histogram.record(99.9) # Async Gauge -gauge = meter.create_observable_gauge("gauge", [observable_gauge_func]) +observable_gauge = meter.create_observable_gauge( + "observable_gauge", [observable_gauge_func] +) + +# Sync Gauge +gauge = meter.create_gauge("gauge") +gauge.set(1) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 0de88ccdaa7..56ffabee6cd 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -53,9 +53,15 @@ CallbackOptions, CallbackT, Counter, +) +from opentelemetry.metrics._internal.instrument import Gauge as _Gauge +from opentelemetry.metrics._internal.instrument import ( Histogram, Instrument, NoOpCounter, +) +from opentelemetry.metrics._internal.instrument import NoOpGauge as _NoOpGauge +from opentelemetry.metrics._internal.instrument import ( NoOpHistogram, NoOpObservableCounter, NoOpObservableGauge, @@ -74,6 +80,8 @@ Synchronous, Asynchronous, CallbackOptions, + _Gauge, + _NoOpGauge, get_meter_provider, get_meter, Histogram, @@ -103,6 +111,8 @@ "NoOpMeterProvider", "Meter", "Counter", + "_Gauge", + "_NoOpGauge", "NoOpCounter", "UpDownCounter", "NoOpUpDownCounter", diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py index dc1e76c8ae8..2e6914f8e3c 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py @@ -51,8 +51,10 @@ from opentelemetry.metrics._internal.instrument import ( CallbackT, Counter, + Gauge, Histogram, NoOpCounter, + NoOpGauge, NoOpHistogram, NoOpObservableCounter, NoOpObservableGauge, @@ -63,6 +65,7 @@ ObservableUpDownCounter, UpDownCounter, _ProxyCounter, + _ProxyGauge, _ProxyHistogram, _ProxyObservableCounter, _ProxyObservableGauge, @@ -79,6 +82,7 @@ _ProxyInstrumentT = Union[ _ProxyCounter, _ProxyHistogram, + _ProxyGauge, _ProxyObservableCounter, _ProxyObservableGauge, _ProxyObservableUpDownCounter, @@ -381,6 +385,22 @@ def create_histogram( description: A description for this instrument and what it measures. """ + @abstractmethod + def create_gauge( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Gauge: + """Creates a ``Gauge`` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + @abstractmethod def create_observable_gauge( self, @@ -512,6 +532,19 @@ def create_histogram( self._instruments.append(proxy) return proxy + def create_gauge( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Gauge: + with self._lock: + if self._real_meter: + return self._real_meter.create_gauge(name, unit, description) + proxy = _ProxyGauge(name, unit, description) + self._instruments.append(proxy) + return proxy + def create_observable_gauge( self, name: str, @@ -579,6 +612,27 @@ def create_counter( ) return NoOpCounter(name, unit=unit, description=description) + def create_gauge( + self, + name: str, + unit: str = "", + description: str = "", + ) -> Gauge: + """Returns a no-op Gauge.""" + super().create_gauge(name, unit=unit, description=description) + if self._is_instrument_registered(name, NoOpGauge, unit, description)[ + 0 + ]: + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Gauge.__name__, + unit, + description, + ) + return NoOpGauge(name, unit=unit, description=description) + def create_up_down_counter( self, name: str, diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py index b02a15005c3..1115bb1f191 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/instrument.py @@ -396,3 +396,50 @@ def _create_real_instrument( return meter.create_observable_gauge( self._name, self._callbacks, self._unit, self._description ) + + +class Gauge(Synchronous): + """A Gauge is a synchronous `Instrument` which can be used to record non-additive values as they occur.""" + + @abstractmethod + def set( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: + pass + + +class NoOpGauge(Gauge): + """No-op implementation of ``Gauge``.""" + + def __init__( + self, + name: str, + unit: str = "", + description: str = "", + ) -> None: + super().__init__(name, unit=unit, description=description) + + def set( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: + return super().set(amount, attributes=attributes) + + +class _ProxyGauge( + _ProxyInstrument[Gauge], + Gauge, +): + def set( + self, + amount: Union[int, float], + attributes: Optional[Attributes] = None, + ) -> None: + if self._real_instrument: + self._real_instrument.set(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge: + return meter.create_gauge(self._name, self._unit, self._description) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index e66460de354..12267433af0 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -29,6 +29,7 @@ ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _Gauge, ) # FIXME Test that the instrument methods can be called concurrently safely. @@ -277,6 +278,50 @@ def test_histogram_record_method(self): self.assertIsNone(NoOpHistogram("name").record(1)) +class TestGauge(TestCase): + def test_create_gauge(self): + """ + Test that the Gauge can be created with create_gauge. + """ + + self.assertTrue( + isinstance(NoOpMeter("name").create_gauge("name"), _Gauge) + ) + + def test_api_gauge_abstract(self): + """ + Test that the API Gauge is an abstract class. + """ + + self.assertTrue(isabstract(_Gauge)) + + def test_create_gauge_api(self): + """ + Test that the API for creating a gauge accepts the name of the instrument. + Test that the API for creating a gauge accepts a sequence of callbacks. + Test that the API for creating a gauge accepts the unit of the instrument. + Test that the API for creating a gauge accepts the description of the instrument + """ + + create_gauge_signature = signature(Meter.create_gauge) + self.assertIn("name", create_gauge_signature.parameters.keys()) + self.assertIs( + create_gauge_signature.parameters["name"].default, + Signature.empty, + ) + create_gauge_signature = signature(Meter.create_gauge) + create_gauge_signature = signature(Meter.create_gauge) + self.assertIn("unit", create_gauge_signature.parameters.keys()) + self.assertIs(create_gauge_signature.parameters["unit"].default, "") + + create_gauge_signature = signature(Meter.create_gauge) + self.assertIn("description", create_gauge_signature.parameters.keys()) + self.assertIs( + create_gauge_signature.parameters["description"].default, + "", + ) + + class TestObservableGauge(TestCase): def test_create_observable_gauge(self): """ diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index 44e81bdc8c8..8b427a73721 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -41,6 +41,9 @@ def create_observable_counter( def create_histogram(self, name, unit="", description=""): super().create_histogram(name, unit=unit, description=description) + def create_gauge(self, name, unit="", description=""): + super().create_gauge(name, unit=unit, description=description) + def create_observable_gauge(self, name, callback, unit="", description=""): super().create_observable_gauge( name, callback, unit=unit, description=description @@ -64,6 +67,7 @@ def test_repeated_instrument_names(self): test_meter.create_up_down_counter("up_down_counter") test_meter.create_observable_counter("observable_counter", Mock()) test_meter.create_histogram("histogram") + test_meter.create_gauge("gauge") test_meter.create_observable_gauge("observable_gauge", Mock()) test_meter.create_observable_up_down_counter( "observable_up_down_counter", Mock() @@ -75,6 +79,7 @@ def test_repeated_instrument_names(self): "counter", "up_down_counter", "histogram", + "gauge", ]: with self.assertLogs(level=WARNING): getattr(test_meter, f"create_{instrument_name}")( @@ -123,6 +128,14 @@ def test_create_histogram(self): self.assertTrue(hasattr(Meter, "create_histogram")) self.assertTrue(Meter.create_histogram.__isabstractmethod__) + def test_create_gauge(self): + """ + Test that the meter provides a function to create a new Gauge + """ + + self.assertTrue(hasattr(Meter, "create_gauge")) + self.assertTrue(Meter.create_gauge.__isabstractmethod__) + def test_create_observable_gauge(self): """ Test that the meter provides a function to create a new ObservableGauge diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 2fa9fe1e736..559b56205ec 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -30,6 +30,7 @@ from opentelemetry.metrics._internal import _ProxyMeter, _ProxyMeterProvider from opentelemetry.metrics._internal.instrument import ( _ProxyCounter, + _ProxyGauge, _ProxyHistogram, _ProxyObservableCounter, _ProxyObservableGauge, @@ -195,6 +196,11 @@ def test_proxy_meter(self): proxy_histogram = proxy_meter.create_histogram( name, unit=unit, description=description ) + + proxy_gauge = proxy_meter.create_gauge( + name, unit=unit, description=description + ) + proxy_observable_counter = proxy_meter.create_observable_counter( name, callbacks=[callback], unit=unit, description=description ) @@ -209,6 +215,7 @@ def test_proxy_meter(self): self.assertIsInstance(proxy_counter, _ProxyCounter) self.assertIsInstance(proxy_updowncounter, _ProxyUpDownCounter) self.assertIsInstance(proxy_histogram, _ProxyHistogram) + self.assertIsInstance(proxy_gauge, _ProxyGauge) self.assertIsInstance( proxy_observable_counter, _ProxyObservableCounter ) @@ -223,6 +230,7 @@ def test_proxy_meter(self): proxy_counter.add(amount, attributes=attributes) proxy_updowncounter.add(amount, attributes=attributes) proxy_histogram.record(amount, attributes=attributes) + proxy_gauge.set(amount, attributes=attributes) # Calling _ProxyMeterProvider.on_set_meter_provider() should cascade down # to the _ProxyInstruments which should create their own real instruments @@ -243,6 +251,9 @@ def test_proxy_meter(self): real_meter.create_histogram.assert_called_once_with( name, unit, description ) + real_meter.create_gauge.assert_called_once_with( + name, unit, description + ) real_meter.create_observable_counter.assert_called_once_with( name, [callback], unit, description ) @@ -258,9 +269,11 @@ def test_proxy_meter(self): real_counter: Mock = real_meter.create_counter() real_updowncounter: Mock = real_meter.create_up_down_counter() real_histogram: Mock = real_meter.create_histogram() + real_gauge: Mock = real_meter.create_gauge() real_counter.assert_not_called() real_updowncounter.assert_not_called() real_histogram.assert_not_called() + real_gauge.assert_not_called() proxy_counter.add(amount, attributes=attributes) real_counter.add.assert_called_once_with(amount, attributes) @@ -268,6 +281,8 @@ def test_proxy_meter(self): real_updowncounter.add.assert_called_once_with(amount, attributes) proxy_histogram.record(amount, attributes=attributes) real_histogram.record.assert_called_once_with(amount, attributes) + proxy_gauge.set(amount, attributes=attributes) + real_gauge.set.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 @@ -292,6 +307,9 @@ def test_proxy_meter_with_real_meter(self) -> None: histogram = proxy_meter.create_histogram( name, unit=unit, description=description ) + gauge = proxy_meter.create_gauge( + name, unit=unit, description=description + ) observable_counter = proxy_meter.create_observable_counter( name, callbacks=[callback], unit=unit, description=description ) @@ -308,6 +326,7 @@ def test_proxy_meter_with_real_meter(self) -> None: 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(gauge, real_meter.create_gauge()) self.assertIs( observable_counter, real_meter.create_observable_counter() ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 1ca14283cf6..a907a289760 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -15,8 +15,9 @@ from opentelemetry.sdk.metrics._internal import Meter, MeterProvider from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError +from opentelemetry.sdk.metrics._internal.instrument import Counter +from opentelemetry.sdk.metrics._internal.instrument import Gauge as _Gauge from opentelemetry.sdk.metrics._internal.instrument import ( - Counter, Histogram, ObservableCounter, ObservableGauge, @@ -30,6 +31,7 @@ "MetricsTimeoutError", "Counter", "Histogram", + "_Gauge", "ObservableCounter", "ObservableGauge", "ObservableUpDownCounter", diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index ffec748ccb7..9b5aac16143 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -31,9 +31,11 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.metrics import _Gauge as APIGauge from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, + _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, @@ -218,6 +220,40 @@ def create_histogram(self, name, unit="", description="") -> APIHistogram: self._instrument_id_instrument[instrument_id] = instrument return instrument + def create_gauge(self, name, unit="", description="") -> APIGauge: + + ( + is_instrument_registered, + instrument_id, + ) = self._is_instrument_registered(name, _Gauge, unit, description) + + if is_instrument_registered: + # FIXME #2558 go through all views here and check if this + # instrument registration conflict can be fixed. If it can be, do + # not log the following warning. + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + APIGauge.__name__, + unit, + description, + ) + with self._instrument_id_instrument_lock: + return self._instrument_id_instrument[instrument_id] + + instrument = _Gauge( + name, + self._instrumentation_scope, + self._measurement_consumer, + unit, + description, + ) + + with self._instrument_id_instrument_lock: + self._instrument_id_instrument[instrument_id] = instrument + return instrument + def create_observable_gauge( self, name, callbacks=None, unit="", description="" ) -> APIObservableGauge: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py index 62ba091ebfe..3adf3327f63 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py @@ -32,6 +32,7 @@ ObservableUpDownCounter, Synchronous, UpDownCounter, + _Gauge, ) from opentelemetry.sdk.metrics._internal.exponential_histogram.buckets import ( Buckets, @@ -46,8 +47,8 @@ from opentelemetry.sdk.metrics._internal.point import Buckets as BucketsPoint from opentelemetry.sdk.metrics._internal.point import ( ExponentialHistogramDataPoint, - Gauge, ) +from opentelemetry.sdk.metrics._internal.point import Gauge as GaugePoint from opentelemetry.sdk.metrics._internal.point import ( Histogram as HistogramPoint, ) @@ -341,7 +342,7 @@ def collect( ) -class _LastValueAggregation(_Aggregation[Gauge]): +class _LastValueAggregation(_Aggregation[GaugePoint]): def __init__(self, attributes: Attributes): super().__init__(attributes) self._value = None @@ -1105,6 +1106,9 @@ def _create_aggregation( if isinstance(instrument, ObservableGauge): return _LastValueAggregation(attributes) + if isinstance(instrument, _Gauge): + return _LastValueAggregation(attributes) + raise Exception(f"Invalid instrument type {type(instrument)} found") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index 0568270ae6b..e7099562eb8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -44,12 +44,14 @@ from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.instrument import ( Counter, + Gauge, Histogram, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Counter, + _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, @@ -224,6 +226,7 @@ def __init__( _Counter: AggregationTemporality.CUMULATIVE, _UpDownCounter: AggregationTemporality.CUMULATIVE, _Histogram: AggregationTemporality.CUMULATIVE, + _Gauge: AggregationTemporality.CUMULATIVE, _ObservableCounter: AggregationTemporality.CUMULATIVE, _ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, _ObservableGauge: AggregationTemporality.CUMULATIVE, @@ -251,6 +254,8 @@ def __init__( self._instrument_class_temporality[ _Histogram ] = temporality + elif typ is Gauge: + self._instrument_class_temporality[_Gauge] = temporality elif typ is ObservableCounter: self._instrument_class_temporality[ _ObservableCounter @@ -271,6 +276,7 @@ def __init__( _Counter: DefaultAggregation(), _UpDownCounter: DefaultAggregation(), _Histogram: DefaultAggregation(), + _Gauge: DefaultAggregation(), _ObservableCounter: DefaultAggregation(), _ObservableUpDownCounter: DefaultAggregation(), _ObservableGauge: DefaultAggregation(), @@ -288,6 +294,8 @@ def __init__( self._instrument_class_aggregation[ _Histogram ] = aggregation + elif typ is Gauge: + self._instrument_class_aggregation[_Gauge] = aggregation elif typ is ObservableCounter: self._instrument_class_aggregation[ _ObservableCounter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py index 6c0320c4790..11dd8499341 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/instrument.py @@ -28,6 +28,7 @@ ObservableUpDownCounter as APIObservableUpDownCounter, ) from opentelemetry.metrics import UpDownCounter as APIUpDownCounter +from opentelemetry.metrics import _Gauge as APIGauge from opentelemetry.metrics._internal.instrument import CallbackOptions from opentelemetry.sdk.metrics._internal.measurement import Measurement from opentelemetry.sdk.util.instrumentation import InstrumentationScope @@ -212,6 +213,20 @@ def record( ) +class Gauge(_Synchronous, APIGauge): + def __new__(cls, *args, **kwargs): + if cls is Gauge: + raise TypeError("Gauge must be instantiated via a meter.") + return super().__new__(cls) + + def set( + self, amount: Union[int, float], attributes: Dict[str, str] = None + ): + self._measurement_consumer.consume_measurement( + Measurement(amount, self, attributes) + ) + + class ObservableGauge(_Asynchronous, APIObservableGauge): def __new__(cls, *args, **kwargs): if cls is ObservableGauge: @@ -242,5 +257,9 @@ class _Histogram(Histogram): pass +class _Gauge(Gauge): + pass + + class _ObservableGauge(ObservableGauge): pass diff --git a/opentelemetry-sdk/tests/metrics/test_aggregation.py b/opentelemetry-sdk/tests/metrics/test_aggregation.py index b7cfc63cd4f..50561022fb1 100644 --- a/opentelemetry-sdk/tests/metrics/test_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/test_aggregation.py @@ -25,6 +25,7 @@ ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, + _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, @@ -532,6 +533,19 @@ def test_histogram(self): ) self.assertIsInstance(aggregation, _ExplicitBucketHistogramAggregation) + def test_gauge(self): + + aggregation = self.default_aggregation._create_aggregation( + _Gauge( + "name", + Mock(), + Mock(), + ), + Mock(), + 0, + ) + self.assertIsInstance(aggregation, _LastValueAggregation) + def test_observable_gauge(self): aggregation = self.default_aggregation._create_aggregation( diff --git a/opentelemetry-sdk/tests/metrics/test_import.py b/opentelemetry-sdk/tests/metrics/test_import.py index f0302e00de4..73b9e1ece9c 100644 --- a/opentelemetry-sdk/tests/metrics/test_import.py +++ b/opentelemetry-sdk/tests/metrics/test_import.py @@ -33,6 +33,7 @@ def test_import_init(self): ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _Gauge, ) def test_import_export(self): diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py index 5eb1a908852..d103050994e 100644 --- a/opentelemetry-sdk/tests/metrics/test_instrument.py +++ b/opentelemetry-sdk/tests/metrics/test_instrument.py @@ -26,8 +26,10 @@ ObservableUpDownCounter, UpDownCounter, ) +from opentelemetry.sdk.metrics import _Gauge as _SDKGauge from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, + _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, @@ -293,6 +295,23 @@ def test_disallow_direct_observable_counter_creation(self): ObservableCounter("name", Mock(), Mock()) +class TestGauge(TestCase): + def testname(self): + self.assertEqual(_Gauge("name", Mock(), Mock()).name, "name") + self.assertEqual(_Gauge("Name", Mock(), Mock()).name, "name") + + def test_set(self): + mc = Mock() + gauge = _Gauge("name", Mock(), mc) + gauge.set(1.0) + mc.consume_measurement.assert_called_once() + + def test_disallow_direct_counter_creation(self): + with self.assertRaises(TypeError): + # pylint: disable=abstract-class-instantiated + _SDKGauge("name", Mock(), Mock()) + + class TestObservableUpDownCounter(TestCase): def test_callable_callback_0(self): observable_up_down_counter = _ObservableUpDownCounter( diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_metric_reader.py index 86404328d6c..fff645e36d9 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader.py @@ -17,8 +17,10 @@ from unittest.mock import patch from opentelemetry.sdk.metrics import Counter, Histogram, ObservableGauge +from opentelemetry.sdk.metrics import _Gauge as _SDKGauge from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, + _Gauge, _Histogram, _ObservableCounter, _ObservableGauge, @@ -39,6 +41,7 @@ _expected_keys = [ _Counter, _UpDownCounter, + _Gauge, _Histogram, _ObservableCounter, _ObservableUpDownCounter, @@ -76,6 +79,7 @@ def test_configure_temporality(self): preferred_temporality={ Histogram: AggregationTemporality.DELTA, ObservableGauge: AggregationTemporality.DELTA, + _SDKGauge: AggregationTemporality.DELTA, } ) @@ -114,6 +118,11 @@ def test_configure_temporality(self): AggregationTemporality.DELTA, ) + self.assertEqual( + dummy_metric_reader._instrument_class_temporality[_Gauge], + AggregationTemporality.DELTA, + ) + def test_configure_aggregation(self): dummy_metric_reader = DummyMetricReader() self.assertEqual( diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py index 1da6d5bcf60..5bcf07f6b68 100644 --- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py +++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py @@ -20,6 +20,7 @@ ) from opentelemetry.sdk.metrics._internal.instrument import ( _Counter, + _Gauge, _Histogram, _ObservableCounter, _UpDownCounter, @@ -728,6 +729,13 @@ def test_view_instrument_match_conflict_6(self): unit="unit", description="description", ) + gauge = _Gauge( + "gauge", + Mock(), + [Mock()], + unit="unit", + description="description", + ) metric_reader_storage = MetricReaderStorage( SdkConfiguration( resource=Mock(), @@ -735,6 +743,7 @@ def test_view_instrument_match_conflict_6(self): views=( View(instrument_name="observable_counter", name="foo"), View(instrument_name="histogram", name="foo"), + View(instrument_name="gauge", name="foo"), ), ), MagicMock( @@ -757,6 +766,12 @@ def test_view_instrument_match_conflict_6(self): Measurement(1, histogram) ) + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + metric_reader_storage.consume_measurement( + Measurement(1, gauge) + ) + def test_view_instrument_match_conflict_7(self): # There is a conflict between views and instruments because the # description being different does not avoid a conflict. diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index 0ccadf47cee..6232e072e23 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -28,6 +28,7 @@ ObservableGauge, ObservableUpDownCounter, UpDownCounter, + _Gauge, ) from opentelemetry.sdk.metrics._internal import SynchronousMeasurementConsumer from opentelemetry.sdk.metrics.export import ( @@ -348,6 +349,18 @@ def test_consume_measurement_histogram( sync_consumer_instance.consume_measurement.assert_called() + @patch( + "opentelemetry.sdk.metrics._internal." "SynchronousMeasurementConsumer" + ) + def test_consume_measurement_gauge(self, mock_sync_measurement_consumer): + sync_consumer_instance = mock_sync_measurement_consumer() + meter_provider = MeterProvider() + gauge = meter_provider.get_meter("name").create_gauge("name") + + gauge.set(1) + + sync_consumer_instance.consume_measurement.assert_called() + class TestMeter(TestCase): def setUp(self): @@ -361,6 +374,7 @@ def test_repeated_instrument_names(self): "observable_counter", callbacks=[Mock()] ) self.meter.create_histogram("histogram") + self.meter.create_gauge("gauge") self.meter.create_observable_gauge( "observable_gauge", callbacks=[Mock()] ) @@ -372,6 +386,7 @@ def test_repeated_instrument_names(self): "counter", "up_down_counter", "histogram", + "gauge", ]: with self.assertLogs(level=WARNING): getattr(self.meter, f"create_{instrument_name}")( @@ -428,6 +443,14 @@ def test_create_observable_gauge(self): self.assertIsInstance(observable_gauge, ObservableGauge) self.assertEqual(observable_gauge.name, "name") + def test_create_gauge(self): + gauge = self.meter.create_gauge( + "name", unit="unit", description="description" + ) + + self.assertIsInstance(gauge, _Gauge) + self.assertEqual(gauge.name, "name") + def test_create_observable_up_down_counter(self): observable_up_down_counter = ( self.meter.create_observable_up_down_counter(