diff --git a/CHANGELOG.md b/CHANGELOG.md index e8bafe43f6..df83398ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-sdk`: Fix memory leak in `TracerProvider.get_tracer()` where a new + `TracerMetrics` instance was created on every call, causing `ProxyMeterProvider` to + accumulate proxy meters indefinitely when no SDK `MeterProvider` was configured. + ([#5016](https://github.com/open-telemetry/opentelemetry-python/issues/5016)) - `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service` ([#5003](https://github.com/open-telemetry/opentelemetry-python/pull/5003)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 18fced7061..0a613988bf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1125,6 +1125,8 @@ def __init__( instrumentation_scope: InstrumentationScope, *, meter_provider: Optional[metrics_api.MeterProvider] = None, + tracer_metrics: Optional["TracerMetrics"] = None, + _tracer_provider: Optional["TracerProvider"] = None, _tracer_config: Optional[_TracerConfig] = None, ) -> None: self.sampler = sampler @@ -1134,6 +1136,12 @@ def __init__( self.instrumentation_info = instrumentation_info self._span_limits = span_limits self._instrumentation_scope = instrumentation_scope + self._tracer_provider = _tracer_provider + if tracer_metrics is not None: + self._tracer_metrics = tracer_metrics + else: + meter_provider = meter_provider or metrics_api.get_meter_provider() + self._tracer_metrics = TracerMetrics(meter_provider) self._tracer_config = _tracer_config or _TracerConfig.default() meter_provider = meter_provider or metrics_api.get_meter_provider() @@ -1333,6 +1341,8 @@ def __init__( self._tracer_configurator = ( _tracer_configurator or _default_tracer_configurator ) + self._tracer_metrics: Optional[TracerMetrics] = None + self._tracer_metrics_lock = threading.Lock() self._tracers_lock = threading.Lock() self._tracers: dict[InstrumentationScope, Tracer] = {} @@ -1357,6 +1367,23 @@ def _set_tracer_configurator( def resource(self) -> Resource: return self._resource + def _get_tracer_metrics(self) -> TracerMetrics: + """Return a single cached TracerMetrics instance for this provider. + + Creating a new TracerMetrics on every get_tracer() call causes + ProxyMeterProvider to accumulate proxy meters indefinitely when no + SDK MeterProvider is configured, leading to unbounded memory growth. + + See: https://github.com/open-telemetry/opentelemetry-python/issues/5016 + """ + if self._tracer_metrics is None: + with self._tracer_metrics_lock: + if self._tracer_metrics is None: + self._tracer_metrics = TracerMetrics( + self._meter_provider + or metrics_api.get_meter_provider() + ) + return self._tracer_metrics def _apply_tracer_configurator( self, instrumentation_scope: InstrumentationScope ): @@ -1399,6 +1426,21 @@ def get_tracer( schema_url, ) + tracer = Tracer( + self.sampler, + self.resource, + self._active_span_processor, + self.id_generator, + instrumentation_info, + self._span_limits, + InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ), + tracer_metrics=self._get_tracer_metrics(), + _tracer_provider=self, instrumentation_scope = InstrumentationScope( instrumenting_module_name, instrumenting_library_version,