Skip to content

Commit 899d92b

Browse files
Merge branch 'main' into fix/metrics-attributes-copy
2 parents 24b14a6 + ecc1fb5 commit 899d92b

35 files changed

Lines changed: 706 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- `opentelemetry-sdk`: Fix mutable attributes reference in metrics: attributes passed to instrument `add`/`record` are now copied so that subsequent mutations to the caller's dict do not affect recorded data points
1616
([#5106](https://github.com/open-telemetry/opentelemetry-python/pull/5106))
17+
- `opentelemetry-sdk`: fix multi-processor `force_flush` skipping remaining processors when one returns `None`
18+
([#5179](https://github.com/open-telemetry/opentelemetry-python/pull/5179))
1719
- Apply fixes for `UP` ruff rule
1820
([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133))
1921
- Switch to SPDX license headers and add CI enforcement
@@ -60,6 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6062
([#5095](https://github.com/open-telemetry/opentelemetry-python/pull/5095))
6163
- Add `registry` keyword argument to `PrometheusMetricReader` to allow passing a custom Prometheus registry
6264
([#5055](https://github.com/open-telemetry/opentelemetry-python/pull/5055))
65+
- Add ability to selectively enable exporting of SDK internal metrics with the `OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED` environment variable.
66+
([#5151](https://github.com/open-telemetry/opentelemetry-python/pull/5151))
6367

6468
## Version 1.41.0/0.62b0 (2026-04-09)
6569

CONTRIBUTING.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,14 @@ git commit
234234
git push fork feature
235235
```
236236

237-
Open a pull request against the main `opentelemetry-python` repo.
237+
Open a pull request (PR) against the main `opentelemetry-python` repo.
238+
239+
A descriptive PR title will help the community better triage and review your changes. Make sure to prefix with the name(s) of the package/subdirectory/domain that your PR updates. Following any of these examples will help:
240+
241+
* "opentelemetry-sdk: make test_force_flush_late_by_timeout less flaky on pypy/windows"
242+
* "opentelemetry-exporter-otlp-proto-http: enable typechecking"
243+
* "docs: replace TODO placeholders with API and SDK overview descriptions"
244+
* "feat(config): Add TracerProvider support for declarative config"
238245

239246
Pull requests are also tested for their compatibility with packages distributed
240247
by OpenTelemetry in the [OpenTelemetry Python Contrib Repository](https://github.com/open-telemetry/opentelemetry-python.git).

dev-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ requests==2.32.3
1414
ruamel.yaml==0.17.21
1515
asgiref==3.7.2
1616
psutil==7.2.2
17-
GitPython==3.1.47
17+
GitPython==3.1.50
1818
pre-commit==3.7.0
1919
ruff==0.14.1

exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from collections import Counter
77
from collections.abc import Iterator
8-
from contextlib import contextmanager
8+
from contextlib import AbstractContextManager, contextmanager
99
from dataclasses import dataclass
1010
from time import perf_counter
11-
from typing import TYPE_CHECKING
11+
from typing import TYPE_CHECKING, Protocol
1212

1313
from opentelemetry.metrics import MeterProvider, get_meter_provider
1414
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
@@ -46,6 +46,18 @@ class ExportResult:
4646
error_attrs: Attributes = None
4747

4848

49+
class ExporterMetricsT(Protocol):
50+
def export_operation(
51+
self, num_items: int
52+
) -> AbstractContextManager[ExportResult]: ...
53+
54+
55+
class NoOpExporterMetrics:
56+
@contextmanager
57+
def export_operation(self, num_items: int) -> Iterator[ExportResult]:
58+
yield ExportResult()
59+
60+
4961
class ExporterMetrics:
5062
def __init__(
5163
self,
@@ -75,14 +87,14 @@ def __init__(
7587
elif endpoint.scheme == "http":
7688
port = 80
7789

78-
component_type = (
79-
component_type or OtelComponentTypeValues("unknown_otlp_exporter")
80-
).value
81-
count = _component_counter[component_type]
82-
_component_counter[component_type] = count + 1
90+
component_type_value = (
91+
component_type.value if component_type else "unknown_otlp_exporter"
92+
)
93+
count = _component_counter[component_type_value]
94+
_component_counter[component_type_value] = count + 1
8395
self._standard_attrs: dict[str, AttributeValue] = {
84-
OTEL_COMPONENT_TYPE: component_type,
85-
OTEL_COMPONENT_NAME: f"{component_type}/{count}",
96+
OTEL_COMPONENT_TYPE: component_type_value,
97+
OTEL_COMPONENT_NAME: f"{component_type_value}/{count}",
8698
}
8799
if endpoint.hostname:
88100
self._standard_attrs[SERVER_ADDRESS] = endpoint.hostname
@@ -121,3 +133,21 @@ def export_operation(self, num_items: int) -> Iterator[ExportResult]:
121133
else exported_attrs
122134
)
123135
self._duration.record(end_time - start_time, duration_attrs)
136+
137+
138+
def create_exporter_metrics(
139+
component_type: OtelComponentTypeValues | None,
140+
signal: Literal["traces", "metrics", "logs"],
141+
endpoint: UrlParseResult,
142+
meter_provider: MeterProvider | None,
143+
enabled: bool,
144+
) -> ExporterMetricsT:
145+
if not enabled:
146+
return NoOpExporterMetrics()
147+
148+
return ExporterMetrics(
149+
component_type,
150+
signal,
151+
endpoint,
152+
meter_provider,
153+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright The OpenTelemetry Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import unittest
5+
from unittest.mock import Mock, patch
6+
from urllib.parse import urlparse
7+
8+
from opentelemetry.exporter.otlp.proto.common._exporter_metrics import (
9+
ExporterMetrics,
10+
NoOpExporterMetrics,
11+
create_exporter_metrics,
12+
)
13+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
14+
OtelComponentTypeValues,
15+
)
16+
17+
18+
class TestExporterMetrics(unittest.TestCase):
19+
def test_factory_returns_noop_when_disabled(self):
20+
meter_provider = Mock()
21+
22+
with patch(
23+
"opentelemetry.exporter.otlp.proto.common."
24+
"_exporter_metrics.get_meter_provider"
25+
) as get_meter_provider:
26+
metrics = create_exporter_metrics(
27+
OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER,
28+
"traces",
29+
urlparse("http://localhost:4318/v1/traces"),
30+
meter_provider,
31+
False,
32+
)
33+
34+
self.assertIsInstance(metrics, NoOpExporterMetrics)
35+
meter_provider.get_meter.assert_not_called()
36+
get_meter_provider.assert_not_called()
37+
38+
def test_factory_returns_exporter_metrics_when_enabled(self):
39+
meter_provider = Mock()
40+
meter_provider.get_meter.return_value = Mock()
41+
42+
metrics = create_exporter_metrics(
43+
OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER,
44+
"traces",
45+
urlparse("http://localhost:4318/v1/traces"),
46+
meter_provider,
47+
True,
48+
)
49+
50+
self.assertIsInstance(metrics, ExporterMetrics)
51+
meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk")
52+
53+
def test_noop_export_operation_yields_result(self):
54+
metrics = NoOpExporterMetrics()
55+
56+
with metrics.export_operation(1) as result:
57+
result.error = RuntimeError("error")
58+
59+
self.assertIsInstance(result.error, RuntimeError)

exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
ssl_channel_credentials,
4747
)
4848
from opentelemetry.exporter.otlp.proto.common._exporter_metrics import (
49-
ExporterMetrics,
49+
create_exporter_metrics,
5050
)
5151
from opentelemetry.exporter.otlp.proto.common._internal import (
5252
_get_resource_data,
@@ -93,6 +93,7 @@
9393
OTEL_EXPORTER_OTLP_HEADERS,
9494
OTEL_EXPORTER_OTLP_INSECURE,
9595
OTEL_EXPORTER_OTLP_TIMEOUT,
96+
OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED,
9697
)
9798
from opentelemetry.sdk.metrics.export import MetricExportResult, MetricsData
9899
from opentelemetry.sdk.resources import Resource as SDKResource
@@ -395,11 +396,15 @@ def __init__(
395396
self._component_type = component_type
396397
self._signal: Literal["traces", "metrics", "logs"] = signal
397398
self._parsed_url = parsed_url
398-
self._metrics = ExporterMetrics(
399+
self._metrics = create_exporter_metrics(
399400
self._component_type,
400401
signal,
401402
parsed_url,
402403
meter_provider,
404+
os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "")
405+
.strip()
406+
.lower()
407+
== "true",
403408
)
404409

405410
self._initialize_channel_and_stub()
@@ -557,9 +562,13 @@ def _exporting(self) -> str:
557562
pass
558563

559564
def _set_meter_provider(self, meter_provider: MeterProvider) -> None:
560-
self._metrics = ExporterMetrics(
565+
self._metrics = create_exporter_metrics(
561566
self._component_type,
562567
self._signal,
563568
self._parsed_url,
564569
meter_provider,
570+
os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "")
571+
.strip()
572+
.lower()
573+
== "true",
565574
)

exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from opentelemetry.sdk.environment_variables import (
4646
_OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER,
4747
OTEL_EXPORTER_OTLP_COMPRESSION,
48+
OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED,
4849
)
4950
from opentelemetry.sdk.metrics import MeterProvider
5051
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
@@ -378,13 +379,19 @@ def test_otlp_exporter_otlp_compression_envvar(
378379
),
379380
)
380381

382+
@patch.dict(
383+
"os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: " true "}
384+
)
381385
def test_shutdown(self):
382386
add_TraceServiceServicer_to_server(
383387
TraceServiceServicerWithExportParams(StatusCode.OK),
384388
self.server,
385389
)
390+
exporter = OTLPSpanExporterForTesting(
391+
insecure=True, meter_provider=self.meter_provider
392+
)
386393
self.assertEqual(
387-
self.exporter.export([self.span]), SpanExportResult.SUCCESS
394+
exporter.export([self.span]), SpanExportResult.SUCCESS
388395
)
389396
metrics_data = self.metric_reader.get_metrics_data()
390397
scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0]
@@ -406,10 +413,10 @@ def test_shutdown(self):
406413
metrics[2].data.data_points[0].attributes
407414
)
408415

409-
self.exporter.shutdown()
416+
exporter.shutdown()
410417
with self.assertLogs(level=WARNING) as warning:
411418
self.assertEqual(
412-
self.exporter.export([self.span]), SpanExportResult.FAILURE
419+
exporter.export([self.span]), SpanExportResult.FAILURE
413420
)
414421
self.assertEqual(
415422
warning.records[0].message,
@@ -471,6 +478,9 @@ def test_export_over_closed_grpc_channel(self):
471478
system() == "Windows",
472479
"For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.",
473480
)
481+
@patch.dict(
482+
"os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}
483+
)
474484
def test_retry_info_is_respected(self):
475485
mock_trace_service = TraceServiceServicerWithExportParams(
476486
StatusCode.UNAVAILABLE,
@@ -613,7 +623,13 @@ def test_otlp_headers_from_env(self):
613623
(),
614624
)
615625

626+
@patch.dict(
627+
"os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"}
628+
)
616629
def test_permanent_failure(self):
630+
exporter = OTLPSpanExporterForTesting(
631+
insecure=True, meter_provider=self.meter_provider
632+
)
617633
with self.assertLogs(level=WARNING) as warning:
618634
add_TraceServiceServicer_to_server(
619635
TraceServiceServicerWithExportParams(
@@ -622,7 +638,7 @@ def test_permanent_failure(self):
622638
self.server,
623639
)
624640
self.assertEqual(
625-
self.exporter.export([self.span]), SpanExportResult.FAILURE
641+
exporter.export([self.span]), SpanExportResult.FAILURE
626642
)
627643
self.assertEqual(
628644
warning.records[-1].message,

exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import gzip
55
import logging
6+
import os
67
import random
78
import threading
89
import zlib
@@ -16,7 +17,7 @@
1617
from requests.exceptions import ConnectionError
1718

1819
from opentelemetry.exporter.otlp.proto.common._exporter_metrics import (
19-
ExporterMetrics,
20+
create_exporter_metrics,
2021
)
2122
from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs
2223
from opentelemetry.exporter.otlp.proto.http import (
@@ -50,6 +51,7 @@
5051
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
5152
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
5253
OTEL_EXPORTER_OTLP_TIMEOUT,
54+
OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED,
5355
)
5456
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
5557
OtelComponentTypeValues,
@@ -141,11 +143,15 @@ def __init__(
141143
)
142144
self._shutdown = False
143145

144-
self._metrics = ExporterMetrics(
146+
self._metrics = create_exporter_metrics(
145147
OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER,
146148
"logs",
147149
urlparse(self._endpoint),
148150
meter_provider,
151+
os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "")
152+
.strip()
153+
.lower()
154+
== "true",
149155
)
150156

151157
def _export(

0 commit comments

Comments
 (0)