diff --git a/CHANGELOG.md b/CHANGELOG.md index afe82e55b2b..bda3202e2d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#5135](https://github.com/open-telemetry/opentelemetry-python/pull/5135)) - ci: wait for tracecontext server readiness instead of a fixed sleep in `scripts/tracecontext-integration-test.sh` ([#5149](https://github.com/open-telemetry/opentelemetry-python/pull/5149)) +- Add ability to selectively enable exporting of SDK internal metrics with the `OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED` environment variable. + ([#5151](https://github.com/open-telemetry/opentelemetry-python/pull/5151)) ## Version 1.41.0/0.62b0 (2026-04-09) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py index 96d12a8857d..30f4069e22e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py @@ -15,10 +15,10 @@ from __future__ import annotations from collections import Counter -from contextlib import contextmanager +from contextlib import AbstractContextManager, contextmanager from dataclasses import dataclass from time import perf_counter -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Iterator, Protocol from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -56,6 +56,18 @@ class ExportResult: error_attrs: Attributes = None +class ExporterMetricsT(Protocol): + def export_operation( + self, num_items: int + ) -> AbstractContextManager[ExportResult]: ... + + +class NoOpExporterMetrics: + @contextmanager + def export_operation(self, num_items: int) -> Iterator[ExportResult]: + yield ExportResult() + + class ExporterMetrics: def __init__( self, @@ -85,14 +97,14 @@ def __init__( elif endpoint.scheme == "http": port = 80 - component_type = ( - component_type or OtelComponentTypeValues("unknown_otlp_exporter") - ).value - count = _component_counter[component_type] - _component_counter[component_type] = count + 1 + component_type_value = ( + component_type.value if component_type else "unknown_otlp_exporter" + ) + count = _component_counter[component_type_value] + _component_counter[component_type_value] = count + 1 self._standard_attrs: dict[str, AttributeValue] = { - OTEL_COMPONENT_TYPE: component_type, - OTEL_COMPONENT_NAME: f"{component_type}/{count}", + OTEL_COMPONENT_TYPE: component_type_value, + OTEL_COMPONENT_NAME: f"{component_type_value}/{count}", } if endpoint.hostname: self._standard_attrs[SERVER_ADDRESS] = endpoint.hostname @@ -131,3 +143,21 @@ def export_operation(self, num_items: int) -> Iterator[ExportResult]: else exported_attrs ) self._duration.record(end_time - start_time, duration_attrs) + + +def create_exporter_metrics( + component_type: OtelComponentTypeValues | None, + signal: Literal["traces", "metrics", "logs"], + endpoint: UrlParseResult, + meter_provider: MeterProvider | None, + enabled: bool, +) -> ExporterMetricsT: + if not enabled: + return NoOpExporterMetrics() + + return ExporterMetrics( + component_type, + signal, + endpoint, + meter_provider, + ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py new file mode 100644 index 00000000000..0e74b6d6512 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py @@ -0,0 +1,70 @@ +# 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. + +import unittest +from unittest.mock import Mock, patch +from urllib.parse import urlparse + +from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( + ExporterMetrics, + NoOpExporterMetrics, + create_exporter_metrics, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) + + +class TestExporterMetrics(unittest.TestCase): + def test_factory_returns_noop_when_disabled(self): + meter_provider = Mock() + + with patch( + "opentelemetry.exporter.otlp.proto.common." + "_exporter_metrics.get_meter_provider" + ) as get_meter_provider: + metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + urlparse("http://localhost:4318/v1/traces"), + meter_provider, + False, + ) + + self.assertIsInstance(metrics, NoOpExporterMetrics) + meter_provider.get_meter.assert_not_called() + get_meter_provider.assert_not_called() + + def test_factory_returns_exporter_metrics_when_enabled(self): + meter_provider = Mock() + meter_provider.get_meter.return_value = Mock() + + metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + urlparse("http://localhost:4318/v1/traces"), + meter_provider, + True, + ) + + self.assertIsInstance(metrics, ExporterMetrics) + meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") + + def test_noop_export_operation_yields_result(self): + metrics = NoOpExporterMetrics() + + with metrics.export_operation(1) as result: + result.error = RuntimeError("error") + + self.assertIsInstance(result.error, RuntimeError) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index 3627db70581..0eab60697d0 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -57,7 +57,7 @@ ssl_channel_credentials, ) from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, + create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, @@ -103,6 +103,10 @@ OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.metrics.export import MetricExportResult, MetricsData from opentelemetry.sdk.resources import Resource as SDKResource @@ -387,11 +391,14 @@ def __init__( self._component_type = component_type self._signal: Literal["traces", "metrics", "logs"] = signal self._parsed_url = parsed_url - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( self._component_type, signal, parsed_url, meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) self._initialize_channel_and_stub() @@ -549,9 +556,12 @@ def _exporting(self) -> str: pass def _set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( self._component_type, self._signal, self._parsed_url, meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 87bd72c5550..de5c680381b 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -54,6 +54,7 @@ from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -387,13 +388,19 @@ def test_otlp_exporter_otlp_compression_envvar( ), ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_shutdown(self): add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams(StatusCode.OK), self.server, ) + exporter = OTLPSpanExporterForTesting( + insecure=True, meter_provider=self.meter_provider + ) self.assertEqual( - self.exporter.export([self.span]), SpanExportResult.SUCCESS + exporter.export([self.span]), SpanExportResult.SUCCESS ) metrics_data = self.metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] @@ -415,10 +422,10 @@ def test_shutdown(self): metrics[2].data.data_points[0].attributes ) - self.exporter.shutdown() + exporter.shutdown() with self.assertLogs(level=WARNING) as warning: self.assertEqual( - self.exporter.export([self.span]), SpanExportResult.FAILURE + exporter.export([self.span]), SpanExportResult.FAILURE ) self.assertEqual( warning.records[0].message, @@ -480,6 +487,9 @@ def test_export_over_closed_grpc_channel(self): system() == "Windows", "For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.", ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_retry_info_is_respected(self): mock_trace_service = TraceServiceServicerWithExportParams( StatusCode.UNAVAILABLE, @@ -622,7 +632,13 @@ def test_otlp_headers_from_env(self): (), ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_permanent_failure(self): + exporter = OTLPSpanExporterForTesting( + insecure=True, meter_provider=self.meter_provider + ) with self.assertLogs(level=WARNING) as warning: add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams( @@ -631,7 +647,7 @@ def test_permanent_failure(self): self.server, ) self.assertEqual( - self.exporter.export([self.span]), SpanExportResult.FAILURE + exporter.export([self.span]), SpanExportResult.FAILURE ) self.assertEqual( warning.records[-1].message, diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 6032433dd12..11bf9d683a8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -27,7 +27,7 @@ from requests.exceptions import ConnectionError from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, + create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import ( @@ -61,6 +61,10 @@ OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OtelComponentTypeValues, @@ -152,11 +156,14 @@ def __init__( ) self._shutdown = False - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, "logs", urlparse(self._endpoint), meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def _export( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index efd63b45438..edd3fa655f4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -35,7 +35,7 @@ from typing_extensions import deprecated from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, + create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( _get_resource_data, @@ -86,6 +86,10 @@ OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.metrics._internal.aggregation import Aggregation from opentelemetry.sdk.metrics.export import ( # noqa: F401 @@ -217,11 +221,14 @@ def __init__( self._max_export_batch_size: int | None = max_export_batch_size self._shutdown = False - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", urlparse(self._endpoint), meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def _export( @@ -399,11 +406,14 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool: return True def set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", urlparse(self._endpoint), meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 018d89df1ee..dc8eb71276c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -27,7 +27,7 @@ from requests.exceptions import ConnectionError from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, + create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( encode_spans, @@ -57,6 +57,10 @@ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult @@ -147,11 +151,14 @@ def __init__( ) self._shutdown = False - self._metrics = ExporterMetrics( + self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", urlparse(self._endpoint), meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def _export( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 5f7ae2afa9b..83ecc8379de 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -69,6 +69,7 @@ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import ( Counter, @@ -334,6 +335,9 @@ def test_headers_parse_from_env(self): ), ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_success(self, mock_post): resp = Response() @@ -372,6 +376,9 @@ def test_success(self, mock_post): metrics[2].data.data_points[0].attributes ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_failure(self, mock_post): resp = Response() @@ -1276,6 +1283,21 @@ def test_2xx_status_code(self, mock_otlp_metric_exporter): MetricExportResult.SUCCESS, ) + @patch.dict("os.environ", {}, clear=True) + @patch.object(OTLPMetricExporter, "_export", return_value=Mock(ok=True)) + def test_exporter_metrics_disabled_after_set_meter_provider( + self, _mock_export + ): + exporter = OTLPMetricExporter() + exporter.set_meter_provider(self.meter_provider) + + self.assertEqual( + exporter.export(self.metrics["sum_int"]), + MetricExportResult.SUCCESS, + ) + + self.assertIsNone(self.metric_reader.get_metrics_data()) + def test_preferred_aggregation_override(self): histogram_aggregation = ExplicitBucketHistogramAggregation( boundaries=[0.05, 0.1, 0.5, 1, 5, 10], @@ -1291,6 +1313,9 @@ def test_preferred_aggregation_override(self): exporter._preferred_aggregation[Histogram], histogram_aggregation ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_retry_timeout(self, mock_post): exporter = OTLPMetricExporter( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 7981b0bc821..bdcdfbe3c74 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -58,6 +58,7 @@ OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -467,6 +468,9 @@ def test_2xx_status_code(self, mock_otlp_metric_exporter): LogRecordExportResult.SUCCESS, ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_retry_timeout(self, mock_post): exporter = OTLPLogExporter( @@ -555,6 +559,9 @@ def test_export_no_collector_available_retryable(self, mock_post): warning.records[0].message, ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_export_no_collector_available(self, mock_post): exporter = OTLPLogExporter( diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 0df471aa693..1f671be6107 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -48,6 +48,7 @@ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -287,6 +288,20 @@ def test_2xx_status_code(self, mock_otlp_metric_exporter): OTLPSpanExporter().export(MagicMock()), SpanExportResult.SUCCESS ) + @patch.dict("os.environ", {}, clear=True) + @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True)) + def test_exporter_metrics_disabled_by_default(self, _mock_export): + exporter = OTLPSpanExporter(meter_provider=self.meter_provider) + + self.assertEqual( + exporter.export([BASIC_SPAN]), SpanExportResult.SUCCESS + ) + + self.assertIsNone(self.metric_reader.get_metrics_data()) + + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_retry_timeout(self, mock_post): exporter = OTLPSpanExporter( @@ -375,6 +390,9 @@ def test_export_no_collector_available_retryable(self, mock_post): warning.records[0].message, ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) @patch.object(Session, "post") def test_export_no_collector_available(self, mock_post): exporter = OTLPSpanExporter( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 956d9f28bd7..09289e1310d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -57,12 +57,19 @@ _create_log_record_with_exception, _set_log_record_exception_attributes, ) -from opentelemetry.sdk._logs._internal._logger_metrics import LoggerMetrics +from opentelemetry.sdk._logs._internal._logger_metrics import ( + LoggerMetricsT, + create_logger_metrics, +) from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, OTEL_SDK_DISABLED, ) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util import ns_to_iso_str from opentelemetry.sdk.util._configurator import RuleBasedConfigurator @@ -77,6 +84,8 @@ ) from opentelemetry.util.types import AnyValue, _ExtendedAttributes +# pylint: disable=too-many-lines + _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" @@ -675,7 +684,7 @@ def __init__( ], instrumentation_scope: InstrumentationScope, *, - logger_metrics: LoggerMetrics, + logger_metrics: LoggerMetricsT, _logger_config: _LoggerConfig, ): super().__init__( @@ -797,8 +806,11 @@ def __init__( self._multi_log_record_processor = ( multi_log_record_processor or SynchronousMultiLogRecordProcessor() ) - self._logger_metrics = LoggerMetrics( - meter_provider or get_meter_provider() + self._logger_metrics = create_logger_metrics( + meter_provider or get_meter_provider(), + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py index 92a4c76a450..76d5508bc47 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py @@ -12,12 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Protocol + from opentelemetry import metrics as metrics_api from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_log_created, ) +class LoggerMetricsT(Protocol): + def emit_log(self) -> None: ... + + +class NoOpLoggerMetrics: + def emit_log(self) -> None: + pass + + class LoggerMetrics: def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") @@ -25,3 +36,13 @@ def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: def emit_log(self) -> None: self._created_logs.add(1) + + +def create_logger_metrics( + meter_provider: metrics_api.MeterProvider, + enabled: bool, +) -> LoggerMetricsT: + if not enabled: + return NoOpLoggerMetrics() + + return LoggerMetrics(meter_provider) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 1c0f82ac055..95fd1bd71eb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -39,13 +39,19 @@ from opentelemetry.sdk._shared_internal import ( BatchProcessor, DuplicateFilter, - ProcessorMetrics, +) +from opentelemetry.sdk._shared_internal._processor_metrics import ( + create_processor_metrics, ) from opentelemetry.sdk.environment_variables import ( OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE, OTEL_BLRP_SCHEDULE_DELAY, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.resources import Resource from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -186,10 +192,13 @@ def __init__( ): self._exporter = exporter self._shutdown = False - self._metrics = ProcessorMetrics( + self._metrics = create_processor_metrics( "logs", OtelComponentTypeValues.SIMPLE_LOG_PROCESSOR, meter_provider or get_meter_provider(), + enabled=parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def on_emit(self, log_record: ReadWriteLogRecord): @@ -299,11 +308,14 @@ def __init__( export_timeout_millis, max_queue_size, "Log", - ProcessorMetrics( + create_processor_metrics( "logs", OtelComponentTypeValues.BATCHING_LOG_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, + enabled=parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ), ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py index cde19165d62..e58093be889 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py @@ -37,7 +37,7 @@ set_value, ) from opentelemetry.sdk._shared_internal._processor_metrics import ( - ProcessorMetrics, + ProcessorMetricsT, ) from opentelemetry.util._once import Once @@ -101,7 +101,7 @@ def __init__( export_timeout_millis: float, max_queue_size: int, exporting: str, - metrics: ProcessorMetrics, + metrics: ProcessorMetricsT, ): self._bsp_reset_once = Once() self._exporter = exporter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py index 47f90c28522..8afa00c66bd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py @@ -16,7 +16,7 @@ from collections import Counter from collections.abc import Callable -from typing import Literal +from typing import Literal, Protocol from opentelemetry.metrics import CallbackOptions, MeterProvider, Observation from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -37,6 +37,27 @@ _component_counter = Counter() +class ProcessorMetricsT(Protocol): + def register_queue_size( + self, get_queue_size: Callable[[], int] + ) -> None: ... + + def drop_items(self, count: int) -> None: ... + + def finish_items(self, count: int, error: Exception | None) -> None: ... + + +class NoOpProcessorMetrics: + def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: + pass + + def drop_items(self, count: int) -> None: + pass + + def finish_items(self, count: int, error: Exception | None) -> None: + pass + + class ProcessorMetrics: def __init__( self, @@ -114,3 +135,22 @@ def finish_items(self, count: int, error: Exception | None) -> None: ERROR_TYPE: type(error).__name__, } self._processed.add(count, attrs) + + +def create_processor_metrics( + signal: Literal["traces", "logs"], + component_type: OtelComponentTypeValues, + meter_provider: MeterProvider, + *, + capacity: int | None = None, + enabled: bool, +) -> ProcessorMetricsT: + if not enabled: + return NoOpProcessorMetrics() + + return ProcessorMetrics( + signal, + component_type, + meter_provider, + capacity=capacity, + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 2e5f350512b..8e5bf7d3b09 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -845,3 +845,14 @@ def channel_credential_provider() -> grpc.ChannelCredentials: This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ + +OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED = ( + "OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED" +) +""" +.. envvar:: OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + +The :envvar:`OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED` environment variable enables +metrics emitted by the SDK about its own internal state. +Default: "false" +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/_internal.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/_internal.py new file mode 100644 index 00000000000..0e59c57aa4f --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/_internal.py @@ -0,0 +1,39 @@ +# 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 logging import getLogger +from os import environ + +_logger = getLogger(__name__) + + +def parse_boolean_environment_variable( + environment_variable: str, default: bool = False +) -> bool: + value = environ.get(environment_variable) + if value is None: + return default + + match value.strip().lower(): + case "true": + return True + case "false": + return False + case _: + _logger.warning( + "Invalid value for %s: %r. Expected 'true' or 'false'.", + environment_variable, + value, + ) + return default 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 66f327306a6..99f5132cc4f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -39,6 +39,10 @@ from opentelemetry.sdk.environment_variables import ( OTEL_METRIC_EXPORT_INTERVAL, OTEL_METRIC_EXPORT_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.metrics._internal.aggregation import ( AggregationTemporality, @@ -67,7 +71,7 @@ ) from opentelemetry.util._once import Once -from ._metric_reader_metrics import MetricReaderMetrics +from ._metric_reader_metrics import create_metric_reader_metrics _logger = getLogger(__name__) @@ -331,8 +335,12 @@ def __init__( if otel_component_type else type(self).__qualname__ ) - self._metrics = MetricReaderMetrics( - self._otel_component_type, NoOpMeterProvider() + self._metrics = create_metric_reader_metrics( + self._otel_component_type, + NoOpMeterProvider(), + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) @final @@ -390,8 +398,12 @@ def _receive_metrics( """Called by `MetricReader.collect` when it receives a batch of metrics""" def _set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = MetricReaderMetrics( - self._otel_component_type, meter_provider + self._metrics = create_metric_reader_metrics( + self._otel_component_type, + meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def force_flush(self, timeout_millis: float = 10_000) -> bool: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py index 435d9c2da7b..995207ea6c1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py @@ -1,4 +1,5 @@ from collections import Counter +from typing import Protocol from opentelemetry.metrics import MeterProvider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -12,6 +13,15 @@ _component_counter = Counter() +class MetricReaderMetricsT(Protocol): + def record_collection(self, duration: float) -> None: ... + + +class NoOpMetricReaderMetrics: + def record_collection(self, duration: float) -> None: + pass + + class MetricReaderMetrics: def __init__( self, component_type: str, meter_provider: MeterProvider @@ -32,3 +42,14 @@ def __init__( def record_collection(self, duration: float) -> None: self._collection_duration.record(duration, self._standard_attrs) + + +def create_metric_reader_metrics( + component_type: str, + meter_provider: MeterProvider, + enabled: bool, +) -> MetricReaderMetricsT: + if not enabled: + return NoOpMetricReaderMetrics() + + return MetricReaderMetrics(component_type, meter_provider) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 18fced70612..72830aab9f3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -54,15 +54,19 @@ OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT, OTEL_LINK_ATTRIBUTE_COUNT_LIMIT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, OTEL_SDK_DISABLED, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMIT, ) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling -from opentelemetry.sdk.trace._tracer_metrics import TracerMetrics +from opentelemetry.sdk.trace._tracer_metrics import create_tracer_metrics from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedList from opentelemetry.sdk.util._configurator import RuleBasedConfigurator @@ -1137,7 +1141,12 @@ def __init__( self._tracer_config = _tracer_config or _TracerConfig.default() meter_provider = meter_provider or metrics_api.get_meter_provider() - self._tracer_metrics = TracerMetrics(meter_provider) + self._tracer_metrics = create_tracer_metrics( + meter_provider, + parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), + ) def _set_tracer_config(self, tracer_config: _TracerConfig): self._tracer_config = tracer_config diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py index ad7de330c78..9e59f789313 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py @@ -15,6 +15,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Protocol from opentelemetry import metrics as metrics_api from opentelemetry.sdk.trace.sampling import Decision @@ -30,6 +31,24 @@ from opentelemetry.trace.span import SpanContext +class TracerMetricsT(Protocol): + def start_span( + self, + parent_span_context: SpanContext | None, + sampling_decision: Decision, + ) -> Callable[[], None]: ... + + +# pylint: disable=no-self-use +class NoOpTracerMetrics: + def start_span( + self, + parent_span_context: SpanContext | None, + sampling_decision: Decision, + ) -> Callable[[], None]: + return noop + + class TracerMetrics: def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") @@ -65,6 +84,16 @@ def end_span() -> None: return end_span +def create_tracer_metrics( + meter_provider: metrics_api.MeterProvider, + enabled: bool, +) -> TracerMetricsT: + if not enabled: + return NoOpTracerMetrics() + + return TracerMetrics(meter_provider) + + def noop() -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 8cf9c5e922d..7fae9fdcdea 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -27,12 +27,21 @@ set_value, ) from opentelemetry.metrics import MeterProvider, get_meter_provider -from opentelemetry.sdk._shared_internal import BatchProcessor, ProcessorMetrics +from opentelemetry.sdk._shared_internal import ( + BatchProcessor, +) +from opentelemetry.sdk._shared_internal._processor_metrics import ( + create_processor_metrics, +) from opentelemetry.sdk.environment_variables import ( OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, ) from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -102,10 +111,13 @@ def __init__( meter_provider: MeterProvider | None = None, ): self.span_exporter = span_exporter - self._metrics = ProcessorMetrics( + self._metrics = create_processor_metrics( "traces", OtelComponentTypeValues.SIMPLE_SPAN_PROCESSOR, meter_provider or get_meter_provider(), + enabled=parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def on_start( @@ -196,11 +208,14 @@ def __init__( export_timeout_millis, max_queue_size, "Span", - ProcessorMetrics( + create_processor_metrics( "traces", OtelComponentTypeValues.BATCHING_SPAN_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, + enabled=parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ), ) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index c36eeccfdc8..bc8396b472e 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -51,6 +51,7 @@ OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE, OTEL_BLRP_SCHEDULE_DELAY, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -406,6 +407,9 @@ def test_simple_log_record_processor_different_msg_types_with_formatter( ] self.assertEqual(expected, emitted) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_metrics(self): # pylint: disable=too-many-locals metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) @@ -695,6 +699,9 @@ def test_validation_negative_max_queue_size(self): max_export_batch_size=101, ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_metrics(self): # pylint: disable=too-many-locals,too-many-statements metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index deb242f4a11..54c9218ad91 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -20,7 +20,6 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current -from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk._logs import ( Logger, LoggerProvider, @@ -28,12 +27,12 @@ ReadWriteLogRecord, ) from opentelemetry.sdk._logs._internal import ( - LoggerMetrics, NoOpLogger, SynchronousMultiLogRecordProcessor, _disable_logger_configurator, _LoggerConfig, _RuleBasedLoggerConfigurator, + create_logger_metrics, ) from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED from opentelemetry.sdk.resources import Resource @@ -289,7 +288,7 @@ def _get_logger(): "schema_url", {"an": "attribute"}, ), - logger_metrics=LoggerMetrics(NoOpMeterProvider()), + logger_metrics=create_logger_metrics(Mock(), False), _logger_config=_LoggerConfig.default(), ) return logger, log_record_processor_mock diff --git a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py index 5a0e18c4fb9..8095971b9cf 100644 --- a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py +++ b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py @@ -13,12 +13,17 @@ # limitations under the License. from unittest import TestCase +from unittest.mock import patch from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader +@patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) class TestLoggerProviderMetrics(TestCase): def setUp(self): self.metric_reader = InMemoryMetricReader() @@ -57,3 +62,16 @@ def test_create_logs(self): 2, {}, ) + + +class TestLoggerProviderMetricsDisabled(TestCase): + def test_disabled_by_default(self): + metric_reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[metric_reader]) + logger_provider = LoggerProvider(meter_provider=meter_provider) + logger = logger_provider.get_logger("test") + + logger.emit(body="log1") + + self.assertIsNone(metric_reader.get_metrics_data()) + meter_provider.shutdown() diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 3e47e577689..06bf447e301 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -20,10 +20,13 @@ from logging import WARNING from time import sleep, time_ns from typing import Optional, cast -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) from opentelemetry.sdk.metrics import ( Counter, MeterProvider, @@ -315,6 +318,9 @@ def test_metric_exporer_gc(self): "The PeriodicExportingMetricReader object created by this test wasn't garbage collected", ) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_metric_reader_metrics(self): exporter = FakeMetricsExporter() pmr = PeriodicExportingMetricReader( diff --git a/opentelemetry-sdk/tests/test_environment_variables_internal.py b/opentelemetry-sdk/tests/test_environment_variables_internal.py new file mode 100644 index 00000000000..ab340585ed2 --- /dev/null +++ b/opentelemetry-sdk/tests/test_environment_variables_internal.py @@ -0,0 +1,73 @@ +# 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 patch + +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, +) + + +class TestParseBooleanEnvironmentVariable(TestCase): + def test_unset_returns_default(self): + for default, expected in ( + (False, False), + (True, True), + ): + with self.subTest(default=default): + with patch.dict("os.environ", {}, clear=True): + self.assertEqual( + parse_boolean_environment_variable( + "TEST_BOOL", default=default + ), + expected, + ) + + def test_valid_values(self): + for value, expected in ( + ("true", True), + (" TrUe ", True), + ("false", False), + (" FaLsE ", False), + ): + with self.subTest(value=value): + with patch.dict("os.environ", {"TEST_BOOL": value}): + self.assertEqual( + parse_boolean_environment_variable("TEST_BOOL"), + expected, + ) + + def test_invalid_value_warns_and_returns_default(self): + for default, expected in ( + (False, False), + (True, True), + ): + with self.subTest(default=default): + with patch.dict("os.environ", {"TEST_BOOL": "yes"}): + with self.assertLogs( + "opentelemetry.sdk.environment_variables._internal", + level="WARNING", + ) as logs: + self.assertEqual( + parse_boolean_environment_variable( + "TEST_BOOL", default=default + ), + expected, + ) + + self.assertIn( + "Invalid value for TEST_BOOL", + logs.records[0].message, + ) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 2d1321df81b..abae8a29b86 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -28,6 +28,7 @@ OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_SCHEDULE_DELAY, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -146,6 +147,9 @@ def test_simple_span_processor_not_sampled(self): self.assertListEqual([], spans_names_list) + @mock.patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_metrics(self): metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) @@ -397,6 +401,9 @@ def test_batch_span_processor_parameters(self): max_export_batch_size=512, ) + @mock.patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) def test_metrics(self): # pylint: disable=too-many-locals,too-many-statements metric_reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[metric_reader]) diff --git a/opentelemetry-sdk/tests/trace/test_sdk_metrics.py b/opentelemetry-sdk/tests/trace/test_sdk_metrics.py index 2baa967f8ad..51fdd4c7155 100644 --- a/opentelemetry-sdk/tests/trace/test_sdk_metrics.py +++ b/opentelemetry-sdk/tests/trace/test_sdk_metrics.py @@ -13,8 +13,12 @@ # limitations under the License. from unittest import TestCase +from unittest.mock import patch from opentelemetry import trace as trace_api +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider @@ -27,6 +31,7 @@ from opentelemetry.trace.span import SpanContext +@patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}) class TestTracerProviderMetrics(TestCase): def setUp(self): self.metric_reader = InMemoryMetricReader() @@ -159,6 +164,7 @@ def test_dropped(self): }, ) self.assert_live_spans(metric_data, None, {}) + span.end() metric_data = self.metric_reader.get_metrics_data() self.assert_started_spans( @@ -242,3 +248,19 @@ def test_dropped_local_parent(self): }, ) self.assert_live_spans(metric_data, None, {}) + + +class TestTracerProviderMetricsDisabled(TestCase): + def test_disabled_by_default(self): + metric_reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[metric_reader]) + tracer_provider = TracerProvider( + sampler=ALWAYS_ON, meter_provider=meter_provider + ) + tracer = tracer_provider.get_tracer("test") + + with tracer.start_as_current_span("span"): + pass + + self.assertIsNone(metric_reader.get_metrics_data()) + meter_provider.shutdown()