From a7e439d67d2db37cc5f888660c55b36201635b3a Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 19 Oct 2021 11:45:53 +0200 Subject: [PATCH] Add MeterProvider and Meter to the SDK Fixes #2200 --- .../src/opentelemetry/sdk/metrics/__init__.py | 185 ++++++++++++++++ .../src/opentelemetry/sdk/metrics/api.py | 54 +++++ .../opentelemetry/sdk/resources/__init__.py | 1 + .../tests/metrics/test_metrics.py | 203 ++++++++++++++++++ tox.ini | 3 - 5 files changed, 443 insertions(+), 3 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/api.py create mode 100644 opentelemetry-sdk/tests/metrics/test_metrics.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py new file mode 100644 index 00000000000..f1e2add5e31 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -0,0 +1,185 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=function-redefined,too-many-ancestors + +from typing import Optional +from logging import getLogger +from atexit import register + +from opentelemetry.metrics import Meter, MeterProvider +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.metrics.api import MetricReader, MetricExporter, View +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.metrics.instrument import ( + Counter, + Histogram, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, +) + +_logger = getLogger(__name__) + + +class Meter(Meter): + + def __init__(self, instrumentation_info: InstrumentationInfo): + self._instrumentation_info = instrumentation_info + self._meter_provider = None + + def create_counter(self, name, unit=None, description=None) -> Counter: + pass + + def create_up_down_counter( + self, name, unit=None, description=None + ) -> UpDownCounter: + pass + + def create_observable_counter( + self, name, callback, unit=None, description=None + ) -> ObservableCounter: + pass + + def create_histogram(self, name, unit=None, description=None) -> Histogram: + pass + + def create_observable_gauge( + self, name, callback, unit=None, description=None + ) -> ObservableGauge: + pass + + def create_observable_up_down_counter( + self, name, callback, unit=None, description=None + ) -> ObservableUpDownCounter: + pass + + +class MeterProvider(MeterProvider): + """See `opentelemetry.metrics.Provider`.""" + + def __init__( + self, + resource: Resource = Resource.create({}), + shutdown_on_exit: bool = True, + ): + self._resource = resource + self._atexit_handler = None + + if shutdown_on_exit: + self._atexit_handler = register(self.shutdown) + + self._metric_readers = [] + self._metric_exporters = [] + self._views = [] + self._shutdown = False + + @property + def metric_readers(self): + return self._metric_readers + + @property + def metric_exporters(self): + return self._metric_exporters + + @property + def views(self): + return self._views + + @property + def resource(self) -> Resource: + return self._resource + + def get_meter( + self, + instrumenting_module_name: str, + instrumenting_library_version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Meter: + meter = Meter( + InstrumentationInfo( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + ) + ) + + meter._meter_provider = self + + return meter + + def shutdown(self): + + if self._shutdown: + _logger.warning("shutdown can only be called once") + return False + + result = True + + for metric_reader in self._metric_readers: + result = result and metric_reader.shutdown() + + for metric_exporter in self._metric_exporters: + result = result and metric_exporter.shutdown() + + self._shutdown = True + + return result + + def force_flush(self) -> bool: + result = True + + for metric_reader in self._metric_readers: + result = result and metric_reader.force_flush() + + for metric_exporter in self._metric_exporters: + result = result and metric_exporter.force_flush() + + return result + + def register_metric_reader(self, metric_reader: "MetricReader") -> None: + self._metric_readers.append(metric_reader) + + def register_metric_exporter( + self, metric_exporter: "MetricExporter" + ) -> None: + self._metric_exporters.append(metric_exporter) + + def register_view(self, view: "View") -> None: + self._views.append(view) + + +class MetricReader(MetricReader): + + def collect(self): + pass + + +class MetricExporter(MetricExporter): + + def __init__(self): + self._shutdown = False + + def export(self): + pass + + def shutdown(self): + self._shutdown = True + + +class View(View): + + def __init__(self): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/api.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/api.py new file mode 100644 index 00000000000..40ac623217d --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/api.py @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=function-redefined,too-many-ancestors + +from abc import ABC, abstractmethod +from logging import getLogger + + +_logger = getLogger(__name__) + + +class MetricReader(ABC): + + def __init__(self): + self._shutdown = False + + @abstractmethod + def collect(self): + pass + + def shutdown(self): + self._shutdown = True + + +class MetricExporter(ABC): + + def __init__(self): + self._shutdown = False + + @abstractmethod + def export(self): + pass + + def shutdown(self): + self._shutdown = True + + +class View(ABC): + + @abstractmethod + def __init__(self): + pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 5878f375d79..d3728e8cb01 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -65,6 +65,7 @@ import pkg_resources from opentelemetry.attributes import BoundedAttributes + from opentelemetry.sdk.environment_variables import ( OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py new file mode 100644 index 00000000000..98326a8739f --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -0,0 +1,203 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from unittest import TestCase +from unittest.mock import Mock +from logging import WARNING + +from opentelemetry.sdk.metrics import ( + MeterProvider, MetricReader, MetricExporter, View +) +from opentelemetry.sdk.resources import Resource + + +class TestMeterProvider(TestCase): + + def test_meter_provider_resource(self): + """ + `MeterProvider` provides a way to allow a `Resource` to be specified. + """ + + meter_provider_0 = MeterProvider() + meter_provider_1 = MeterProvider() + + self.assertIs(meter_provider_0.resource, meter_provider_1.resource) + self.assertIsInstance(meter_provider_0.resource, Resource) + self.assertIsInstance(meter_provider_1.resource, Resource) + + resource = Resource({"key": "value"}) + self.assertIs(MeterProvider(resource).resource, resource) + + def test_get_meter(self): + """ + `MeterProvider.get_meter` arguments are used to create an + `InstrumentationInfo` object on the created `Meter`. + """ + + meter = MeterProvider().get_meter( + "name", + instrumenting_library_version="version", + schema_url="schema_url" + ) + + self.assertEqual(meter._instrumentation_info.name, "name") + self.assertEqual(meter._instrumentation_info.version, "version") + self.assertEqual(meter._instrumentation_info.schema_url, "schema_url") + + def test_register_metric_reader(self): + """" + `MeterProvider` provides a way to configure `MetricReader`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_metric_reader")) + + metric_reader = MetricReader() + + meter_provider.register_metric_reader(metric_reader) + + self.assertTrue(meter_provider._metric_readers, [metric_reader]) + + def test_register_metric_exporter(self): + """" + `MeterProvider` provides a way to configure `MetricExporter`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_metric_exporter")) + + metric_exporter = MetricExporter() + + meter_provider.register_metric_exporter(metric_exporter) + + self.assertTrue(meter_provider._metric_exporters, [metric_exporter]) + + def test_register_view(self): + """" + `MeterProvider` provides a way to configure `View`s. + """ + + meter_provider = MeterProvider() + + self.assertTrue(hasattr(meter_provider, "register_view")) + + view = View() + + meter_provider.register_view(view) + + self.assertTrue(meter_provider._views, [view]) + + def test_meter_configuration(self): + """ + Any updated configuration is applied to all returned `Meter`s. + """ + + meter_provider = MeterProvider() + + view_0 = View() + + meter_provider.register_view(view_0) + + meter_0 = meter_provider.get_meter("meter_0") + meter_1 = meter_provider.get_meter("meter_1") + + self.assertEqual(meter_0._meter_provider.views, [view_0]) + self.assertEqual(meter_1._meter_provider.views, [view_0]) + + view_1 = View() + + meter_provider.register_view(view_1) + + self.assertEqual(meter_0._meter_provider.views, [view_0, view_1]) + self.assertEqual(meter_1._meter_provider.views, [view_0, view_1]) + + def test_shutdown_subsequent_calls(self): + """ + No subsequent attempts to get a `Meter` are allowed after calling + `MeterProvider.shutdown` + """ + + meter_provider = MeterProvider() + + with self.assertRaises(AssertionError): + with self.assertLogs(level=WARNING): + meter_provider.shutdown() + + with self.assertLogs(level=WARNING): + meter_provider.shutdown() + + def test_shutdown_result(self): + """ + `MeterProvider.shutdown` provides a way to let the caller know if it + succeeded or failed. + + `MeterProvider.shutdown` is implemented by at least invoking + ``shutdown`` on all registered `MetricReader`s and `MetricExporter`s. + """ + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"shutdown.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"shutdown.return_value": True}) + ) + + self.assertTrue(meter_provider.shutdown()) + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"shutdown.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"shutdown.return_value": False}) + ) + + self.assertFalse(meter_provider.shutdown()) + + def test_force_flush_result(self): + """ + `MeterProvider.force_flush` provides a way to let the caller know if it + succeeded or failed. + + `MeterProvider.force_flush` is implemented by at least invoking + ``force_flush`` on all registered `MetricReader`s and `MetricExporter`s. + """ + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"force_flush.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"force_flush.return_value": True}) + ) + + self.assertTrue(meter_provider.force_flush()) + + meter_provider = MeterProvider() + + meter_provider.register_metric_reader( + Mock(**{"force_flush.return_value": True}) + ) + meter_provider.register_metric_exporter( + Mock(**{"force_flush.return_value": False}) + ) + + self.assertFalse(meter_provider.force_flush()) diff --git a/tox.ini b/tox.ini index bd48d073d28..74d4a4c60de 100644 --- a/tox.ini +++ b/tox.ini @@ -235,10 +235,7 @@ commands_pre = -e {toxinidir}/opentelemetry-semantic-conventions \ -e {toxinidir}/opentelemetry-sdk \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" \ -<<<<<<< HEAD -======= -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" \ ->>>>>>> merge_main_3 -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests" \ -e "{env:CONTRIB_REPO}#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi"