diff --git a/CHANGELOG.md b/CHANGELOG.md index 2024544f893..32803173250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2728](https://github.com/open-telemetry/opentelemetry-python/pull/2728)) - fix: update entry point object references for metrics ([#2731](https://github.com/open-telemetry/opentelemetry-python/pull/2731)) +- Configure auto instrumentation to support metrics + ([#2705](https://github.com/open-telemetry/opentelemetry-python/pull/2705)) ## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 8cb1d0f412f..55463e00be9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -25,12 +25,13 @@ from pkg_resources import iter_entry_points -from opentelemetry import trace from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, OTEL_PYTHON_ID_GENERATOR, OTEL_TRACES_EXPORTER, ) +from opentelemetry.metrics import set_meter_provider from opentelemetry.sdk._logs import ( LogEmitterProvider, LoggingHandler, @@ -40,11 +41,17 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, ) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + MetricExporter, + PeriodicExportingMetricReader, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.trace import set_tracer_provider _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" @@ -87,7 +94,7 @@ def _init_tracing( id_generator=id_generator(), resource=Resource.create(auto_resource), ) - trace.set_tracer_provider(provider) + set_tracer_provider(provider) for _, exporter_class in exporters.items(): exporter_args = {} @@ -96,6 +103,33 @@ def _init_tracing( ) +def _init_metrics( + exporters: Dict[str, Type[MetricExporter]], + auto_instrumentation_version: Optional[str] = None, +): + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name + # from the env variable else defaults to "unknown_service" + auto_resource = {} + # populate version if using auto-instrumentation + if auto_instrumentation_version: + auto_resource[ + ResourceAttributes.TELEMETRY_AUTO_VERSION + ] = auto_instrumentation_version + + metric_readers = [] + + for _, exporter_class in exporters.items(): + exporter_args = {} + metric_readers.append( + PeriodicExportingMetricReader(exporter_class(**exporter_args)) + ) + + provider = MeterProvider( + resource=Resource.create(auto_resource), metric_readers=metric_readers + ) + set_meter_provider(provider) + + def _init_logging( exporters: Dict[str, Type[LogExporter]], auto_instrumentation_version: Optional[str] = None, @@ -145,9 +179,15 @@ def _import_config_components( def _import_exporters( trace_exporter_names: Sequence[str], + metric_exporter_names: Sequence[str], log_exporter_names: Sequence[str], -) -> Tuple[Dict[str, Type[SpanExporter]], Dict[str, Type[LogExporter]]]: +) -> Tuple[ + Dict[str, Type[SpanExporter]], + Dict[str, Type[MetricExporter]], + Dict[str, Type[LogExporter]], +]: trace_exporters = {} + metric_exporters = {} log_exporters = {} for (exporter_name, exporter_impl,) in _import_config_components( @@ -158,6 +198,14 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a trace exporter") + for (exporter_name, exporter_impl,) in _import_config_components( + metric_exporter_names, "opentelemetry_metrics_exporter" + ): + if issubclass(exporter_impl, MetricExporter): + metric_exporters[exporter_name] = exporter_impl + else: + raise RuntimeError(f"{exporter_name} is not a metric exporter") + for (exporter_name, exporter_impl,) in _import_config_components( log_exporter_names, "opentelemetry_logs_exporter" ): @@ -166,7 +214,7 @@ def _import_exporters( else: raise RuntimeError(f"{exporter_name} is not a log exporter") - return trace_exporters, log_exporters + return trace_exporters, metric_exporters, log_exporters def _import_id_generator(id_generator_name: str) -> IdGenerator: @@ -182,13 +230,15 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): - trace_exporters, log_exporters = _import_exporters( + trace_exporters, metric_exporters, log_exporters = _import_exporters( _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), + _get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)), _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) _init_tracing(trace_exporters, id_generator, auto_instrumentation_version) + _init_metrics(metric_exporters, auto_instrumentation_version) logging_enabled = os.getenv( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, "false" ) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 1b007d3b165..39548ff5d45 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # type: ignore +# pylint: skip-file import logging from os import environ +from typing import Dict, Iterable, Optional from unittest import TestCase from unittest.mock import patch @@ -28,12 +30,21 @@ _import_exporters, _import_id_generator, _init_logging, + _init_metrics, _init_tracing, _initialize_components, ) from opentelemetry.sdk._logs import LoggingHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter -from opentelemetry.sdk.metrics.export import ConsoleMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + ConsoleMetricExporter, + Metric, + MetricExporter, + MetricReader, +) +from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator @@ -61,6 +72,10 @@ def get_log_emitter(self, name): return DummyLogEmitter(name, self.resource, self.processor) +class DummyMeterProvider(MeterProvider): + pass + + class DummyLogEmitter: def __init__(self, name, resource, processor): self.name = name @@ -93,6 +108,44 @@ def __init__(self, exporter): self.exporter = exporter +class DummyMetricReader(MetricReader): + def __init__( + self, + exporter: MetricExporter, + preferred_temporality: Dict[type, AggregationTemporality] = None, + preferred_aggregation: Dict[type, Aggregation] = None, + export_interval_millis: Optional[float] = None, + export_timeout_millis: Optional[float] = None, + ) -> None: + super().__init__( + preferred_temporality=preferred_temporality, + preferred_aggregation=preferred_aggregation, + ) + self.exporter = exporter + + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + self.exporter.export(None) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + return True + + +class DummyOTLPMetricExporter: + def __init__(self, *args, **kwargs): + self.export_called = False + + def export(self, batch): + self.export_called = True + + def shutdown(self): + pass + + class Exporter: def __init__(self): tracer_provider = trace.get_tracer_provider() @@ -148,7 +201,7 @@ def setUp(self): "opentelemetry.sdk._configuration.BatchSpanProcessor", Processor ) self.set_provider_patcher = patch( - "opentelemetry.trace.set_tracer_provider" + "opentelemetry.sdk._configuration.set_tracer_provider" ) self.get_provider_mock = self.get_provider_patcher.start() @@ -304,6 +357,61 @@ def test_logging_init_enable_env(self, logging_mock, tracing_mock): self.assertEqual(tracing_mock.call_count, 1) +class TestMetricsInit(TestCase): + def setUp(self): + self.metric_reader_patch = patch( + "opentelemetry.sdk._configuration.PeriodicExportingMetricReader", + DummyMetricReader, + ) + self.provider_patch = patch( + "opentelemetry.sdk._configuration.MeterProvider", + DummyMeterProvider, + ) + self.set_provider_patch = patch( + "opentelemetry.sdk._configuration.set_meter_provider" + ) + + self.metric_reader_mock = self.metric_reader_patch.start() + self.provider_mock = self.provider_patch.start() + self.set_provider_mock = self.set_provider_patch.start() + + def tearDown(self): + self.metric_reader_patch.stop() + self.set_provider_patch.stop() + self.provider_patch.stop() + + def test_metrics_init_empty(self): + _init_metrics({}, "auto-version") + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get( + "telemetry.auto.version" + ), + "auto-version", + ) + + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + def test_metrics_init_exporter(self): + _init_metrics({"otlp": DummyOTLPMetricExporter}) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + self.assertIsInstance(provider._sdk_config.resource, Resource) + self.assertEqual( + provider._sdk_config.resource.attributes.get("service.name"), + "otlp-service", + ) + reader = provider._sdk_config.metric_readers[0] + self.assertIsInstance(reader, DummyMetricReader) + self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) + + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: @@ -328,8 +436,8 @@ def test_empty_exporters(self): class TestImportExporters(TestCase): def test_console_exporters(self): - trace_exporters, logs_exporters = _import_exporters( - ["console"], ["console"] + trace_exporters, metric_exporterts, logs_exporters = _import_exporters( + ["console"], ["console"], ["console"] ) self.assertEqual( trace_exporters["console"].__class__, ConsoleSpanExporter.__class__ @@ -338,6 +446,6 @@ def test_console_exporters(self): logs_exporters["console"].__class__, ConsoleLogExporter.__class__ ) self.assertEqual( - logs_exporters["console"].__class__, + metric_exporterts["console"].__class__, ConsoleMetricExporter.__class__, )