Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ 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 `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars
([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979))
- `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification
Expand Down
31 changes: 27 additions & 4 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ def __init__(
instrumentation_scope: InstrumentationScope,
*,
meter_provider: Optional[metrics_api.MeterProvider] = None,
tracer_metrics: Optional["TracerMetrics"] = None,
_tracer_provider: Optional["TracerProvider"] = None,
) -> None:
self.sampler = sampler
Expand All @@ -1130,9 +1131,11 @@ def __init__(
self._span_limits = span_limits
self._instrumentation_scope = instrumentation_scope
self._tracer_provider = _tracer_provider

meter_provider = meter_provider or metrics_api.get_meter_provider()
self._tracer_metrics = TracerMetrics(meter_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)

def _is_enabled(self) -> bool:
"""If the tracer is not enabled, start_span will create a NonRecordingSpan"""
Expand Down Expand Up @@ -1365,6 +1368,8 @@ def __init__(
self._tracer_configurator = (
_tracer_configurator or _default_tracer_configurator
)
self._tracer_metrics: Optional[TracerMetrics] = None
self._tracer_metrics_lock = threading.Lock()

def _set_tracer_configurator(
self, *, tracer_configurator: _TracerConfiguratorT
Expand All @@ -1387,6 +1392,24 @@ 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 get_tracer(
self,
instrumenting_module_name: str,
Expand Down Expand Up @@ -1430,7 +1453,7 @@ def get_tracer(
schema_url,
attributes,
),
meter_provider=self._meter_provider,
tracer_metrics=self._get_tracer_metrics(),
_tracer_provider=self,
)

Expand Down