diff --git a/CHANGELOG.md b/CHANGELOG.md index 614f240d4e..bebcda8f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#5120](https://github.com/open-telemetry/opentelemetry-python/pull/5120)) - Add WeaverLiveCheck test util ([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088)) +- 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 96d12a8857..55686a66e6 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 @@ -63,6 +63,8 @@ def __init__( signal: Literal["traces", "metrics", "logs"], endpoint: UrlParseResult, meter_provider: MeterProvider | None, + *, + disabled: bool = False, ) -> None: if signal == "traces": create_exported = create_otel_sdk_exporter_span_exported @@ -104,9 +106,14 @@ def __init__( self._inflight = create_inflight(meter) self._exported = create_exported(meter) self._duration = create_otel_sdk_exporter_operation_duration(meter) + self._disabled = disabled @contextmanager def export_operation(self, num_items: int) -> Iterator[ExportResult]: + if self._disabled: + yield ExportResult() + return + start_time = perf_counter() self._inflight.add(num_items, self._standard_attrs) 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 3627db7058..bd22bf75e5 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 @@ -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 @@ -392,6 +396,9 @@ def __init__( signal, parsed_url, meter_provider, + disabled=not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) self._initialize_channel_and_stub() 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 87bd72c555..de5c680381 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 6032433dd1..115ef7c86a 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 @@ -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, @@ -157,6 +161,9 @@ def __init__( "logs", urlparse(self._endpoint), meter_provider, + disabled=not 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 efd63b4543..af348fe246 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 @@ -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 @@ -222,6 +226,9 @@ def __init__( "metrics", urlparse(self._endpoint), meter_provider, + disabled=not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def _export( @@ -404,6 +411,9 @@ def set_meter_provider(self, meter_provider: MeterProvider) -> None: "metrics", urlparse(self._endpoint), meter_provider, + disabled=not 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 018d89df1e..8977833ff8 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 @@ -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 @@ -152,6 +156,9 @@ def __init__( "traces", urlparse(self._endpoint), meter_provider, + disabled=not 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 5f7ae2afa9..39128e3187 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() @@ -1291,6 +1298,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 7981b0bc82..bdcdfbe3c7 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 0df471aa69..1f671be610 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/_logger_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py index 92a4c76a45..1f1ca83c3d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py @@ -13,6 +13,12 @@ # limitations under the License. from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, +) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( create_otel_sdk_log_created, ) @@ -22,6 +28,11 @@ class LoggerMetrics: def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: meter = meter_provider.get_meter("opentelemetry-sdk") self._created_logs = create_otel_sdk_log_created(meter) + self._disabled = not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ) def emit_log(self) -> None: + if self._disabled: + return self._created_logs.add(1) 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 1c0f82ac05..132dfcd239 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -46,6 +46,10 @@ 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 ( @@ -190,6 +194,9 @@ def __init__( "logs", OtelComponentTypeValues.SIMPLE_LOG_PROCESSOR, meter_provider or get_meter_provider(), + disabled=not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def on_emit(self, log_record: ReadWriteLogRecord): @@ -304,6 +311,9 @@ def __init__( OtelComponentTypeValues.BATCHING_LOG_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, + disabled=not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ), ) 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 47f90c2852..d5703733ae 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py @@ -45,6 +45,7 @@ def __init__( meter_provider: MeterProvider, *, capacity: int | None = None, + disabled: bool = False, ) -> None: self._signal = signal meter = meter_provider.get_meter("opentelemetry-sdk") @@ -75,12 +76,17 @@ def __init__( ) self._processed = create_processed(meter) + self._disabled = disabled if capacity is not None: self._queue_capacity = create_queue_capacity(meter) - self._queue_capacity.add(capacity, self._standard_attrs) + if not self._disabled: + self._queue_capacity.add(capacity, self._standard_attrs) def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: + if self._disabled: + return + def record_queue_size( _options: CallbackOptions, ) -> tuple[Observation]: @@ -103,9 +109,13 @@ def record_queue_size( ) def drop_items(self, count: int) -> None: + if self._disabled: + return self._processed.add(count, self._dropped_attrs) def finish_items(self, count: int, error: Exception | None) -> None: + if self._disabled: + return if not error: self._processed.add(count, self._standard_attrs) return diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 2e5f350512..8e5bf7d3b0 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 0000000000..0e59c57aa4 --- /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/_metric_reader_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py index 435d9c2da7..b826091ac8 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,6 +1,12 @@ from collections import Counter from opentelemetry.metrics import MeterProvider +from opentelemetry.sdk.environment_variables import ( + 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 ( OTEL_COMPONENT_NAME, OTEL_COMPONENT_TYPE, @@ -29,6 +35,11 @@ def __init__( self._collection_duration = ( create_otel_sdk_metric_reader_collection_duration(meter) ) + self._disabled = not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ) def record_collection(self, duration: float) -> None: + if self._disabled: + return self._collection_duration.record(duration, self._standard_attrs) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py index ad7de330c7..8868a3cd4a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_tracer_metrics.py @@ -17,6 +17,12 @@ from collections.abc import Callable from opentelemetry import metrics as metrics_api +from opentelemetry.sdk.environment_variables import ( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.environment_variables._internal import ( + parse_boolean_environment_variable, +) from opentelemetry.sdk.trace.sampling import Decision from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_SPAN_PARENT_ORIGIN, @@ -36,12 +42,18 @@ def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: self._started_spans = create_otel_sdk_span_started(meter) self._live_spans = create_otel_sdk_span_live(meter) + self._disabled = not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ) def start_span( self, parent_span_context: SpanContext | None, sampling_decision: Decision, ) -> Callable[[], None]: + if self._disabled: + return noop + sampling_result_value = sampling_result(sampling_decision) self._started_spans.add( 1, @@ -60,6 +72,8 @@ def start_span( self._live_spans.add(1, live_span_attrs) def end_span() -> None: + if self._disabled: + return self._live_spans.add(-1, live_span_attrs) return end_span diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 8cf9c5e922..666623a746 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -33,6 +33,10 @@ 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 ( @@ -106,6 +110,9 @@ def __init__( "traces", OtelComponentTypeValues.SIMPLE_SPAN_PROCESSOR, meter_provider or get_meter_provider(), + disabled=not parse_boolean_environment_variable( + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED + ), ) def on_start( @@ -201,6 +208,9 @@ def __init__( OtelComponentTypeValues.BATCHING_SPAN_PROCESSOR, meter_provider or get_meter_provider(), capacity=max_queue_size, + disabled=not 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 c36eeccfdc..bc8396b472 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_sdk_metrics.py b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py index 5a0e18c4fb..8095971b9c 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 3e47e57768..06bf447e30 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 0000000000..ab340585ed --- /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 2d1321df81..abae8a29b8 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 2baa967f8a..51fdd4c715 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()