diff --git a/README.md b/README.md index 6f5b9f213ec..734a824661e 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,14 @@ with tracer.start_as_current_span('foo'): ```python from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController -metrics.set_preferred_meter_implementation(lambda T: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) exporter = ConsoleMetricsExporter() +controller = PushController(meter, exporter, 5) counter = meter.create_metric( "available memory", @@ -89,9 +91,6 @@ counter = meter.create_metric( label_values = ("staging",) counter_handle = counter.get_handle(label_values) counter_handle.add(100) - -exporter.export([(counter, label_values)]) -exporter.shutdown() ``` See the [API documentation](https://open-telemetry.github.io/opentelemetry-python/) for more detail, and the [examples folder](./examples) for a more sample code. diff --git a/docs/opentelemetry.sdk.metrics.rst b/docs/opentelemetry.sdk.metrics.rst index ec8687dd2dc..88612046c8a 100644 --- a/docs/opentelemetry.sdk.metrics.rst +++ b/docs/opentelemetry.sdk.metrics.rst @@ -8,6 +8,7 @@ Submodules opentelemetry.sdk.metrics.export.aggregate opentelemetry.sdk.metrics.export.batcher + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.metrics :members: diff --git a/docs/opentelemetry.sdk.trace.rst b/docs/opentelemetry.sdk.trace.rst index 7bb3569fe63..1c0e9b6f61c 100644 --- a/docs/opentelemetry.sdk.trace.rst +++ b/docs/opentelemetry.sdk.trace.rst @@ -7,6 +7,7 @@ Submodules .. toctree:: opentelemetry.sdk.trace.export + opentelemetry.sdk.util.instrumentation .. automodule:: opentelemetry.sdk.trace :members: diff --git a/docs/opentelemetry.sdk.util.instrumentation.rst b/docs/opentelemetry.sdk.util.instrumentation.rst new file mode 100644 index 00000000000..a7d391bcee1 --- /dev/null +++ b/docs/opentelemetry.sdk.util.instrumentation.rst @@ -0,0 +1,4 @@ +opentelemetry.sdk.util.instrumentation +========================================== + +.. automodule:: opentelemetry.sdk.util.instrumentation diff --git a/examples/metrics/prometheus.py b/examples/metrics/prometheus.py index 14f612c6a93..4d30f8abcca 100644 --- a/examples/metrics/prometheus.py +++ b/examples/metrics/prometheus.py @@ -21,15 +21,15 @@ from opentelemetry import metrics from opentelemetry.ext.prometheus import PrometheusMetricsExporter -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export.controller import PushController # Start Prometheus client start_http_server(port=8000, addr="localhost") # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) +meter = metrics.get_meter(__name__) # exporter to export metrics to Prometheus prefix = "MyAppPrefix" exporter = PrometheusMetricsExporter(prefix) diff --git a/examples/metrics/record.py b/examples/metrics/record.py index be68c8083ff..f008ff67466 100644 --- a/examples/metrics/record.py +++ b/examples/metrics/record.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,13 +19,15 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter +from opentelemetry.sdk.metrics import Counter, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter from opentelemetry.sdk.metrics.export.controller import PushController +# The preferred tracer implementation must be set, as the opentelemetry-api +# defines the interface with a no-op implementation. +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter()) -meter = metrics.meter() +meter = metrics.get_meter(__name__) # exporter to export metrics to the console exporter = ConsoleMetricsExporter() # controller collects metrics created from meter and exports it via the diff --git a/examples/metrics/simple_example.py b/examples/metrics/simple_example.py index 75da80b73ac..75879155176 100644 --- a/examples/metrics/simple_example.py +++ b/examples/metrics/simple_example.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,9 +23,8 @@ import time from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Measure, Meter +from opentelemetry.sdk.metrics import Counter, Measure, MeterProvider from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher from opentelemetry.sdk.metrics.export.controller import PushController batcher_mode = "stateful" @@ -44,18 +43,15 @@ def usage(argv): usage(sys.argv) sys.exit(1) -# Batcher used to collect all created metrics from meter ready for exporting -# Pass in True/False to indicate whether the batcher is stateful. -# True indicates the batcher computes checkpoints from over the process -# lifetime. -# False indicates the batcher computes checkpoints which describe the updates -# of a single collection period (deltas) -batcher = UngroupedBatcher(batcher_mode == "stateful") - -# If a batcher is not provided, a default batcher is used # Meter is responsible for creating and recording metrics -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() +metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider()) + +# Meter's namespace corresponds to the string passed as the first argument Pass +# in True/False to indicate whether the batcher is stateful. True indicates the +# batcher computes checkpoints from over the process lifetime. False indicates +# the batcher computes checkpoints which describe the updates of a single +# collection period (deltas) +meter = metrics.get_meter(__name__, batcher_mode == "stateful") # Exporter to export metrics to the console exporter = ConsoleMetricsExporter() diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py deleted file mode 100644 index 2f423619021..00000000000 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2019, 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. -# -""" -This module serves as an example for a simple application using metrics -""" - -from opentelemetry import metrics -from opentelemetry.sdk.metrics import Counter, Meter -from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter -from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher -from opentelemetry.sdk.metrics.export.controller import PushController - -batcher = UngroupedBatcher(True) -metrics.set_preferred_meter_implementation(lambda _: Meter(batcher)) -meter = metrics.meter() -counter = meter.create_metric( - "available memory", - "available memory", - "bytes", - int, - Counter, - ("environment",), -) - -label_set = meter.get_label_set({"environment": "staging"}) - -# Direct metric usage -counter.add(25, label_set) - -# Handle usage -counter_handle = counter.get_handle(label_set) -counter_handle.add(100) - -# Record batch usage -meter.record_batch(label_set, [(counter, 50)]) - -exporter = ConsoleMetricsExporter() -controller = PushController(meter, exporter, 5) diff --git a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py index 94fea96c5b5..f6883475386 100644 --- a/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py +++ b/ext/opentelemetry-ext-prometheus/tests/test_prometheus_exporter.py @@ -28,7 +28,7 @@ class TestPrometheusMetricExporter(unittest.TestCase): def setUp(self): - self._meter = metrics.Meter() + self._meter = metrics.MeterProvider().get_meter(__name__) self._test_metric = self._meter.create_metric( "testname", "testdesc", @@ -74,7 +74,7 @@ def test_export(self): self.assertIs(result, MetricsExportResult.SUCCESS) def test_counter_to_prometheus(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "test@name", "testdesc", @@ -111,7 +111,7 @@ def test_counter_to_prometheus(self): def test_invalid_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = meter.create_metric( "tesname", "testdesc", "unit", int, TestMetric ) diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 5045c38eed9..c1b330551aa 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,10 +27,13 @@ """ import abc +import logging from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar from opentelemetry.util import loader +logger = logging.getLogger(__name__) + ValueT = TypeVar("ValueT", int, float) @@ -224,6 +227,56 @@ def record(self, value: ValueT, label_set: LabelSet) -> None: """ +class MeterProvider(abc.ABC): + @abc.abstractmethod + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + instrumenting_module_name: The name of the instrumenting module + (usually just ``__name__``). + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.ext.http_requests"``. + + stateful: True/False to indicate whether the meter will be + stateful. True indicates the meter computes checkpoints + from over the process lifetime. False indicates the meter + computes checkpoints which describe the updates of a single + collection period (deltas). + + instrumenting_library_version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + """ + + +class DefaultMeterProvider(MeterProvider): + """The default MeterProvider, used when no implementation is available. + + All operations are no-op. + """ + + def get_meter( + self, + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", + ) -> "Meter": + # pylint:disable=no-self-use,unused-argument + return DefaultMeter() + + MetricT = TypeVar("MetricT", Counter, Gauge, Measure) @@ -322,45 +375,69 @@ def get_label_set(self, labels: Dict[str, str]) -> "LabelSet": # Once https://github.com/python/mypy/issues/7092 is resolved, # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory -ImplementationFactory = Callable[[Type[Meter]], Optional[Meter]] - -_METER = None -_METER_FACTORY = None +ImplementationFactory = Callable[ + [Type[MeterProvider]], Optional[MeterProvider] +] + +_METER_PROVIDER = None +_METER_PROVIDER_FACTORY = None + + +def get_meter( + instrumenting_module_name: str, + stateful: bool = True, + instrumenting_library_version: str = "", +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + This function is a convenience wrapper for + opentelemetry.metrics.meter_provider().get_meter + """ + return meter_provider().get_meter( + instrumenting_module_name, stateful, instrumenting_library_version + ) -def meter() -> Meter: - """Gets the current global :class:`~.Meter` object. +def meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object. If there isn't one set yet, a default will be loaded. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER, _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER is None: + if _METER_PROVIDER is None: # pylint:disable=protected-access try: - _METER = loader._load_impl(Meter, _METER_FACTORY) # type: ignore + _METER_PROVIDER = loader._load_impl( + MeterProvider, _METER_PROVIDER_FACTORY # type: ignore + ) except TypeError: # if we raised an exception trying to instantiate an - # abstract class, default to no-op tracer impl - _METER = DefaultMeter() - del _METER_FACTORY + # abstract class, default to no-op meter impl + logger.warning( + "Unable to instantiate MeterProvider from meter provider factory.", + exc_info=True, + ) + _METER_PROVIDER = DefaultMeterProvider() + _METER_PROVIDER_FACTORY = None - return _METER + return _METER_PROVIDER -def set_preferred_meter_implementation(factory: ImplementationFactory) -> None: - """Set the factory to be used to create the meter. +def set_preferred_meter_provider_implementation( + factory: ImplementationFactory, +) -> None: + """Set the factory to be used to create the meter provider. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a meter is already loaded. Args: - factory: Callback that should create a new :class:`Meter` instance. + factory: Callback that should create a new :class:`MeterProvider` instance. """ - global _METER, _METER_FACTORY # pylint:disable=global-statement + global _METER_PROVIDER_FACTORY # pylint:disable=global-statement - if _METER: - raise RuntimeError("Meter already loaded.") + if _METER_PROVIDER: + raise RuntimeError("MeterProvider already loaded.") - _METER_FACTORY = factory + _METER_PROVIDER_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/util/__init__.py b/opentelemetry-api/src/opentelemetry/util/__init__.py index cbf36d4c05a..9bfc79df21c 100644 --- a/opentelemetry-api/src/opentelemetry/util/__init__.py +++ b/opentelemetry-api/src/opentelemetry/util/__init__.py @@ -1,3 +1,16 @@ +# Copyright 2020, 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. import time # Since we want API users to be able to provide timestamps, diff --git a/opentelemetry-api/tests/metrics/test_metrics.py b/opentelemetry-api/tests/metrics/test_metrics.py index 3ec0f81c718..788ce57680a 100644 --- a/opentelemetry-api/tests/metrics/test_metrics.py +++ b/opentelemetry-api/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,31 +13,11 @@ # limitations under the License. import unittest -from contextlib import contextmanager -from unittest import mock from opentelemetry import metrics # pylint: disable=no-self-use -class TestMeter(unittest.TestCase): - def setUp(self): - self.meter = metrics.DefaultMeter() - - def test_record_batch(self): - counter = metrics.Counter() - label_set = metrics.LabelSet() - self.meter.record_batch(label_set, ((counter, 1),)) - - def test_create_metric(self): - metric = self.meter.create_metric("", "", "", float, metrics.Counter) - self.assertIsInstance(metric, metrics.DefaultMetric) - - def test_get_label_set(self): - metric = self.meter.get_label_set({}) - self.assertIsInstance(metric, metrics.DefaultLabelSet) - - class TestMetrics(unittest.TestCase): def test_default(self): default = metrics.DefaultMetric() @@ -92,34 +72,3 @@ def test_gauge_handle(self): def test_measure_handle(self): handle = metrics.MeasureHandle() handle.record(1) - - -@contextmanager -# type: ignore -def patch_metrics_globals(meter=None, meter_factory=None): - """Mock metrics._METER and metrics._METER_FACTORY. - - This prevents previous changes to these values from affecting the code in - this scope, and prevents changes in this scope from leaking out and - affecting other tests. - """ - with mock.patch("opentelemetry.metrics._METER", meter): - with mock.patch("opentelemetry.metrics._METER_FACTORY", meter_factory): - yield - - -class TestGlobals(unittest.TestCase): - def test_meter_default_factory(self): - """Check that the default meter is a DefaultMeter.""" - with patch_metrics_globals(): - meter = metrics.meter() - self.assertIsInstance(meter, metrics.DefaultMeter) - # Check that we don't create a new instance on each call - self.assertIs(meter, metrics.meter()) - - def test_meter_custom_factory(self): - """Check that we use the provided factory for custom global meters.""" - mock_meter = mock.Mock(metrics.Meter) - with patch_metrics_globals(meter_factory=lambda _: mock_meter): - meter = metrics.meter() - self.assertIs(meter, mock_meter) diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index d1e1a0913a3..0035803bd26 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ class TestAPIOnlyImplementation(unittest.TestCase): https://github.com/open-telemetry/opentelemetry-python/issues/142 """ + # TRACER + def test_tracer(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated @@ -54,12 +56,31 @@ def test_default_span(self): self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), False) + # METER + def test_meter(self): with self.assertRaises(TypeError): # pylint: disable=abstract-class-instantiated metrics.Meter() # type:ignore def test_default_meter(self): + meter_provider = metrics.DefaultMeterProvider() + meter = meter_provider.get_meter(__name__) + self.assertIsInstance(meter, metrics.DefaultMeter) + + # pylint: disable=no-self-use + def test_record_batch(self): + meter = metrics.DefaultMeter() + counter = metrics.Counter() + label_set = metrics.LabelSet() + meter.record_batch(label_set, ((counter, 1),)) + + def test_create_metric(self): meter = metrics.DefaultMeter() metric = meter.create_metric("", "", "", float, metrics.Counter) self.assertIsInstance(metric, metrics.DefaultMetric) + + def test_get_label_set(self): + meter = metrics.DefaultMeter() + label_set = meter.get_label_set({}) + self.assertIsInstance(label_set, metrics.DefaultLabelSet) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py index 4c9231582c8..fdf145d87ca 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ from opentelemetry import metrics as metrics_api from opentelemetry.sdk.metrics.export.aggregate import Aggregator from opentelemetry.sdk.metrics.export.batcher import Batcher, UngroupedBatcher +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -261,12 +262,16 @@ class Meter(metrics_api.Meter): """See `opentelemetry.metrics.Meter`. Args: - batcher: The `Batcher` used for this meter. + instrumentation_info: The `InstrumentationInfo` for this meter. + stateful: Indicates whether the meter is stateful. """ - def __init__(self, batcher: Batcher = UngroupedBatcher(True)): - self.batcher = batcher + def __init__( + self, instrumentation_info: "InstrumentationInfo", stateful: bool, + ): + self.instrumentation_info = instrumentation_info self.metrics = set() + self.batcher = UngroupedBatcher(stateful) def collect(self) -> None: """Collects all the metrics created with this `Meter` for export. @@ -328,3 +333,20 @@ def get_label_set(self, labels: Dict[str, str]): if len(labels) == 0: return EMPTY_LABEL_SET return LabelSet(labels=labels) + + +class MeterProvider(metrics_api.MeterProvider): + def get_meter( + self, + instrumenting_module_name: str, + stateful=True, + instrumenting_library_version: str = "", + ) -> "metrics_api.Meter": + if not instrumenting_module_name: # Reject empty strings too. + raise ValueError("get_meter called with missing module name.") + return Meter( + InstrumentationInfo( + instrumenting_module_name, instrumenting_library_version + ), + stateful=stateful, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index dd0169ea9f7..03fcbac9096 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -26,6 +26,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import util from opentelemetry.sdk.util import BoundedDict, BoundedList +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import SpanContext, sampling from opentelemetry.trace.propagation import SPAN_KEY from opentelemetry.trace.status import Status, StatusCanonicalCode @@ -152,7 +153,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), - instrumentation_info: "InstrumentationInfo" = None, + instrumentation_info: InstrumentationInfo = None, set_status_on_exception: bool = True, ) -> None: @@ -385,46 +386,6 @@ def generate_trace_id() -> int: return random.getrandbits(128) -class InstrumentationInfo: - """Immutable information about an instrumentation library module. - - See `TracerProvider.get_tracer` for the meaning of the properties. - """ - - __slots__ = ("_name", "_version") - - def __init__(self, name: str, version: str): - self._name = name - self._version = version - - def __repr__(self): - return "{}({}, {})".format( - type(self).__name__, self._name, self._version - ) - - def __hash__(self): - return hash((self._name, self._version)) - - def __eq__(self, value): - return type(value) is type(self) and (self._name, self._version) == ( - value._name, - value._version, - ) - - def __lt__(self, value): - if type(value) is not type(self): - return NotImplemented - return (self._name, self._version) < (value._name, value._version) - - @property - def version(self) -> str: - return self._version - - @property - def name(self) -> str: - return self._name - - class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py similarity index 99% rename from opentelemetry-sdk/src/opentelemetry/sdk/util.py rename to opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py index 2265c29460b..009a0bcdd73 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py @@ -11,7 +11,6 @@ # 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. - import datetime import threading from collections import OrderedDict, deque diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py new file mode 100644 index 00000000000..893a6066d9f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -0,0 +1,55 @@ +# Copyright 2020, 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. + + +class InstrumentationInfo: + """Immutable information about an instrumentation library module. + + See `opentelemetry.trace.TracerProvider.get_tracer` or + `opentelemetry.metrics.MeterProvider.get_meter` for the meaning of these + properties. + """ + + __slots__ = ("_name", "_version") + + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + def __repr__(self): + return "{}({}, {})".format( + type(self).__name__, self._name, self._version + ) + + def __hash__(self): + return hash((self._name, self._version)) + + def __eq__(self, value): + return type(value) is type(self) and (self._name, self._version) == ( + value._name, + value._version, + ) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version) < (value._name, value._version) + + @property + def version(self) -> str: + return self._version + + @property + def name(self) -> str: + return self._name diff --git a/opentelemetry-sdk/tests/metrics/export/test_export.py b/opentelemetry-sdk/tests/metrics/export/test_export.py index 51d7aaaf4fd..cd12bcbb6bf 100644 --- a/opentelemetry-sdk/tests/metrics/export/test_export.py +++ b/opentelemetry-sdk/tests/metrics/export/test_export.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ class TestConsoleMetricsExporter(unittest.TestCase): # pylint: disable=no-self-use def test_export(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) exporter = ConsoleMetricsExporter() metric = metrics.Counter( "available memory", @@ -45,7 +45,7 @@ def test_export(self): ("environment",), ) kvp = {"environment": "staging"} - label_set = meter.get_label_set(kvp) + label_set = metrics.LabelSet(kvp) aggregator = CounterAggregator() record = MetricRecord(aggregator, label_set, metric) result = '{}(data="{}", label_set="{}", value={})'.format( @@ -71,7 +71,7 @@ def test_aggregator_for_counter(self): # TODO: Add other aggregator tests def test_checkpoint_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -99,7 +99,7 @@ def test_checkpoint_set_empty(self): self.assertEqual(len(records), 0) def test_finished_collection_stateless(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(False) aggregator = CounterAggregator() metric = metrics.Counter( @@ -119,7 +119,7 @@ def test_finished_collection_stateless(self): self.assertEqual(len(batcher._batch_map), 0) def test_finished_collection_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -140,7 +140,7 @@ def test_finished_collection_stateful(self): # TODO: Abstract the logic once other batchers implemented def test_ungrouped_batcher_process_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() aggregator2 = CounterAggregator() @@ -169,7 +169,7 @@ def test_ungrouped_batcher_process_exists(self): ) def test_ungrouped_batcher_process_not_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -196,7 +196,7 @@ def test_ungrouped_batcher_process_not_exists(self): ) def test_ungrouped_batcher_process_not_stateful(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher = UngroupedBatcher(True) aggregator = CounterAggregator() metric = metrics.Counter( @@ -304,9 +304,7 @@ def call_update(mmsc): def test_update(self): mmsc = MinMaxSumCountAggregator() # test current values without any update - self.assertEqual( - mmsc.current, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -314,7 +312,7 @@ def test_update(self): mmsc.update(val) self.assertEqual( - mmsc.current, (min(values), max(values), sum(values), len(values)), + mmsc.current, (min(values), max(values), sum(values), len(values)) ) def test_checkpoint(self): @@ -322,9 +320,7 @@ def test_checkpoint(self): # take checkpoint wihtout any update mmsc.take_checkpoint() - self.assertEqual( - mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.checkpoint, MinMaxSumCountAggregator._EMPTY) # call update with some values values = (3, 50, 3, 97) @@ -337,9 +333,7 @@ def test_checkpoint(self): (min(values), max(values), sum(values), len(values)), ) - self.assertEqual( - mmsc.current, MinMaxSumCountAggregator._EMPTY, - ) + self.assertEqual(mmsc.current, MinMaxSumCountAggregator._EMPTY) def test_merge(self): mmsc1 = MinMaxSumCountAggregator() diff --git a/opentelemetry-sdk/tests/metrics/test_implementation.py b/opentelemetry-sdk/tests/metrics/test_implementation.py new file mode 100644 index 00000000000..1fedc9ae571 --- /dev/null +++ b/opentelemetry-sdk/tests/metrics/test_implementation.py @@ -0,0 +1,35 @@ +# Copyright 2020, 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. + +import unittest + +from opentelemetry.metrics import DefaultLabelSet, DefaultMeter, DefaultMetric +from opentelemetry.sdk import metrics + + +class TestMeterImplementation(unittest.TestCase): + """ + This test is in place to ensure the SDK implementation of the API + is returning values that are valid. The same tests have been added + to the API with different expected results. See issue for more details: + https://github.com/open-telemetry/opentelemetry-python/issues/142 + """ + + def test_meter(self): + meter = metrics.MeterProvider().get_meter(__name__) + metric = meter.create_metric("", "", "", float, metrics.Counter) + label_set = meter.get_label_set({"key1": "val1"}) + self.assertNotIsInstance(meter, DefaultMeter) + self.assertNotIsInstance(metric, DefaultMetric) + self.assertNotIsInstance(label_set, DefaultLabelSet) diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py index db7e2d8c850..daba171c51c 100644 --- a/opentelemetry-sdk/tests/metrics/test_metrics.py +++ b/opentelemetry-sdk/tests/metrics/test_metrics.py @@ -1,4 +1,4 @@ -# Copyright 2019, OpenTelemetry Authors +# Copyright 2020, OpenTelemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,11 +22,11 @@ class TestMeter(unittest.TestCase): def test_extends_api(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) self.assertIsInstance(meter, metrics_api.Meter) def test_collect(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -41,14 +41,14 @@ def test_collect(self): self.assertTrue(batcher_mock.process.called) def test_collect_no_metrics(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock meter.collect() self.assertFalse(batcher_mock.process.called) def test_collect_disabled_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) batcher_mock = mock.Mock() meter.batcher = batcher_mock label_keys = ("key1",) @@ -63,7 +63,7 @@ def test_collect_disabled_metric(self): self.assertFalse(batcher_mock.process.called) def test_record_batch(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) counter = metrics.Counter( "name", "desc", "unit", float, meter, label_keys @@ -75,7 +75,7 @@ def test_record_batch(self): self.assertEqual(counter.get_handle(label_set).aggregator.current, 1.0) def test_record_batch_multiple(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1", "key2", "key3") kvp = {"key1": "value1", "key2": "value2", "key3": "value3"} label_set = meter.get_label_set(kvp) @@ -96,7 +96,7 @@ def test_record_batch_multiple(self): ) def test_record_batch_exists(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) label_keys = ("key1",) kvp = {"key1": "value1"} label_set = meter.get_label_set(kvp) @@ -111,7 +111,7 @@ def test_record_batch_exists(self): self.assertEqual(handle.aggregator.current, 2.0) def test_create_metric(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) counter = meter.create_metric( "name", "desc", "unit", int, metrics.Counter, () ) @@ -120,7 +120,7 @@ def test_create_metric(self): self.assertEqual(counter.name, "name") def test_create_gauge(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) gauge = meter.create_metric( "name", "desc", "unit", float, metrics.Gauge, () ) @@ -129,7 +129,7 @@ def test_create_gauge(self): self.assertEqual(gauge.name, "name") def test_create_measure(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) measure = meter.create_metric( "name", "desc", "unit", float, metrics.Measure, () ) @@ -138,7 +138,7 @@ def test_create_measure(self): self.assertEqual(measure.name, "name") def test_get_label_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {"environment": "staging", "a": "z"} label_set = meter.get_label_set(kvp) label_set2 = meter.get_label_set(kvp) @@ -146,7 +146,7 @@ def test_get_label_set(self): self.assertEqual(len(labels), 1) def test_get_label_set_empty(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) kvp = {} label_set = meter.get_label_set(kvp) self.assertEqual(label_set, metrics.EMPTY_LABEL_SET) @@ -154,7 +154,7 @@ def test_get_label_set_empty(self): class TestMetric(unittest.TestCase): def test_get_handle(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric_types = [metrics.Counter, metrics.Gauge, metrics.Measure] for _type in metric_types: metric = _type("name", "desc", "unit", int, meter, ("key",)) @@ -166,7 +166,7 @@ def test_get_handle(self): class TestCounter(unittest.TestCase): def test_add(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -178,7 +178,7 @@ def test_add(self): class TestGauge(unittest.TestCase): def test_set(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Gauge("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) @@ -192,7 +192,7 @@ def test_set(self): class TestMeasure(unittest.TestCase): def test_record(self): - meter = metrics.Meter() + meter = metrics.MeterProvider().get_meter(__name__) metric = metrics.Measure("name", "desc", "unit", int, meter, ("key",)) kvp = {"key": "value"} label_set = meter.get_label_set(kvp) diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/trace/test_implementation.py similarity index 85% rename from opentelemetry-sdk/tests/test_implementation.py rename to opentelemetry-sdk/tests/trace/test_implementation.py index 3c0cd2ba20d..74d3d5a9232 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/trace/test_implementation.py @@ -14,12 +14,11 @@ import unittest -from opentelemetry.metrics import DefaultMetric -from opentelemetry.sdk import metrics, trace +from opentelemetry.sdk import trace from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT -class TestSDKImplementation(unittest.TestCase): +class TestTracerImplementation(unittest.TestCase): """ This test is in place to ensure the SDK implementation of the API is returning values that are valid. The same tests have been added @@ -46,8 +45,3 @@ def test_span(self): span = trace.Span("name", INVALID_SPAN_CONTEXT) self.assertEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertIs(span.is_recording_events(), True) - - def test_meter(self): - meter = metrics.Meter() - metric = meter.create_metric("", "", "", float, metrics.Counter) - self.assertNotIsInstance(metric, DefaultMetric) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 982df896678..54961e465b7 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -20,6 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.sdk import trace +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo from opentelemetry.trace import sampling from opentelemetry.trace.status import StatusCanonicalCode from opentelemetry.util import time_ns @@ -153,11 +154,10 @@ def test_instrumentation_info(self): span1 = tracer1.start_span("s1") span2 = tracer2.start_span("s2") self.assertEqual( - span1.instrumentation_info, trace.InstrumentationInfo("instr1", "") + span1.instrumentation_info, InstrumentationInfo("instr1", "") ) self.assertEqual( - span2.instrumentation_info, - trace.InstrumentationInfo("instr2", "1.3b3"), + span2.instrumentation_info, InstrumentationInfo("instr2", "1.3b3") ) self.assertEqual(span2.instrumentation_info.version, "1.3b3") @@ -177,7 +177,7 @@ def test_invalid_instrumentation_info(self): tracer1.instrumentation_info, tracer2.instrumentation_info ) self.assertIsInstance( - tracer1.instrumentation_info, trace.InstrumentationInfo + tracer1.instrumentation_info, InstrumentationInfo ) span1 = tracer1.start_span("foo") self.assertTrue(span1.is_recording_events())