Skip to content

Commit 023dcf8

Browse files
authored
Merge branch 'main' into main
2 parents 2e16222 + 0018c00 commit 023dcf8

26 files changed

Lines changed: 614 additions & 185 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ jobs:
4242
# Alert with a commit comment on possible performance regression
4343
alert-threshold: '200%'
4444
comment-on-alert: true
45+
alert-comment-cc-users: "@open-telemetry/python-approvers,@open-telemetry/python-maintainers"

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets
16+
([#4868](https://github.com/open-telemetry/opentelemetry-python/issues/4868))
17+
- Allow loading all resource detectors by setting `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` to `*`
18+
([#4819](https://github.com/open-telemetry/opentelemetry-python/pull/4819))
1519
- `opentelemetry-sdk`: Fix the type hint of the `_metrics_data` property to allow `None`
16-
([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4837)
20+
([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4837)).
1721
- Regenerate opentelemetry-proto code with v1.9.0 release
1822
([#4840](https://github.com/open-telemetry/opentelemetry-python/pull/4840))
1923
- Add python 3.14 support
2024
([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798))
2125
- Silence events API warnings for internal users
2226
([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847))
27+
- Prevent possible endless recursion from happening in `SimpleLogRecordProcessor.on_emit`,
28+
([#4799](https://github.com/open-telemetry/opentelemetry-python/pull/4799)) and ([#4867](https://github.com/open-telemetry/opentelemetry-python/pull/4867)).
29+
- Make ConcurrentMultiSpanProcessor fork safe
30+
([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862))
31+
- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters
32+
([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709))
2333

2434
## Version 1.39.0/0.60b0 (2025-12-03)
2535

@@ -87,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8797
([#4654](https://github.com/open-telemetry/opentelemetry-python/pull/4654)).
8898
- Fix type checking for built-in metric exporters
8999
([#4820](https://github.com/open-telemetry/opentelemetry-python/pull/4820))
90-
100+
91101
## Version 1.38.0/0.59b0 (2025-10-16)
92102

93103
- Add `rstcheck` to pre-commit to stop introducing invalid RST

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ For more information about the maintainer role, see the [community repository](h
111111
### Approvers
112112

113113
- [Dylan Russell](https://github.com/dylanrussell), Google
114-
- [Emídio Neto](https://github.com/emdneto), PicPay
114+
- [Emídio Neto](https://github.com/emdneto), Independent
115115
- [Héctor Hernández](https://github.com/hectorhdzg), Microsoft
116116
- [Jeremy Voss](https://github.com/jeremydvoss), Microsoft
117117
- [Liudmila Molkova](https://github.com/lmolkova), Grafana Labs

RELEASING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@
3333
* Review and merge the pull request that it creates for updating the version.
3434
* Note: If you are doing a patch release in `-core` repo, you should also do an equivalent patch release in `-contrib` repo (even if there's no fix to release), otherwise tests in CI will fail.
3535

36+
### Note on `contrib.yml` Workflow Behavior
37+
38+
The [contrib.yml](https://github.com/open-telemetry/opentelemetry-python/blob/main/.github/workflows/contrib.yml) workflow in the core repository references reusable workflows from opentelemetry-python-contrib using the hard-coded `main` branch.
39+
40+
Because `uses:` statements cannot receive environment variables and workflows cannot patch or modify other workflows, this reference cannot dynamically follow release branches as we are doing in other workflows.
41+
42+
As a result, when preparing a release branch that contains a different set of instrumentations (e.g., older branches without newly added tox environments), CI may attempt to run tests that do not exist on tox in that branch. In this case:
43+
44+
* It is safe to merge the release PR even if the contrib workflow fails for this reason, or
45+
46+
* Optionally update the contrib.yml workflow to point to the corresponding release branch before running CI.
47+
3648
## Making the release
3749

3850
* Run the [Release workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/release.yml).

docs/examples/sqlcommenter/README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This is an example of how to use OpenTelemetry Python instrumention with
55
sqlcommenter to enrich database query statements with contextual information.
66
For more information on sqlcommenter concepts, see:
77

8-
* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#sql-commenter>`_
8+
* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/database-spans.md#sql-commenter>`_
99
* `sqlcommenter <https://google.github.io/sqlcommenter/>`_
1010

1111
The source files of this example are available `here <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/sqlcommenter/>`_.
@@ -120,5 +120,5 @@ References
120120
* `OpenTelemetry Project <https://opentelemetry.io/>`_
121121
* `OpenTelemetry Collector <https://github.com/open-telemetry/opentelemetry-collector>`_
122122
* `OpenTelemetry MySQL instrumentation <https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-mysql>`_
123-
* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md#sql-commenter>`_
123+
* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/database-spans.md#sql-commenter>`_
124124
* `sqlcommenter <https://google.github.io/sqlcommenter/>`_

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,26 +186,42 @@ def export(
186186
serialized_data = encode_logs(batch).SerializeToString()
187187
deadline_sec = time() + self._timeout
188188
for retry_num in range(_MAX_RETRYS):
189-
resp = self._export(serialized_data, deadline_sec - time())
190-
if resp.ok:
191-
return LogRecordExportResult.SUCCESS
192189
# multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff.
193190
backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2)
191+
try:
192+
resp = self._export(serialized_data, deadline_sec - time())
193+
if resp.ok:
194+
return LogRecordExportResult.SUCCESS
195+
except requests.exceptions.RequestException as error:
196+
reason = error
197+
retryable = isinstance(error, ConnectionError)
198+
status_code = None
199+
else:
200+
reason = resp.reason
201+
retryable = _is_retryable(resp)
202+
status_code = resp.status_code
203+
204+
if not retryable:
205+
_logger.error(
206+
"Failed to export logs batch code: %s, reason: %s",
207+
status_code,
208+
reason,
209+
)
210+
return LogRecordExportResult.FAILURE
211+
194212
if (
195-
not _is_retryable(resp)
196-
or retry_num + 1 == _MAX_RETRYS
213+
retry_num + 1 == _MAX_RETRYS
197214
or backoff_seconds > (deadline_sec - time())
198215
or self._shutdown
199216
):
200217
_logger.error(
201-
"Failed to export logs batch code: %s, reason: %s",
202-
resp.status_code,
203-
resp.text,
218+
"Failed to export logs batch due to timeout, "
219+
"max retries or shutdown."
204220
)
205221
return LogRecordExportResult.FAILURE
206222
_logger.warning(
207223
"Transient error %s encountered while exporting logs batch, retrying in %.2fs.",
208-
resp.reason,
224+
reason,
209225
backoff_seconds,
210226
)
211227
shutdown = self._shutdown_is_occuring.wait(backoff_seconds)

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -231,26 +231,41 @@ def export(
231231
serialized_data = encode_metrics(metrics_data).SerializeToString()
232232
deadline_sec = time() + self._timeout
233233
for retry_num in range(_MAX_RETRYS):
234-
resp = self._export(serialized_data, deadline_sec - time())
235-
if resp.ok:
236-
return MetricExportResult.SUCCESS
237234
# multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff.
238235
backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2)
236+
try:
237+
resp = self._export(serialized_data, deadline_sec - time())
238+
if resp.ok:
239+
return MetricExportResult.SUCCESS
240+
except requests.exceptions.RequestException as error:
241+
reason = error
242+
retryable = isinstance(error, ConnectionError)
243+
status_code = None
244+
else:
245+
reason = resp.reason
246+
retryable = _is_retryable(resp)
247+
status_code = resp.status_code
248+
249+
if not retryable:
250+
_logger.error(
251+
"Failed to export metrics batch code: %s, reason: %s",
252+
status_code,
253+
reason,
254+
)
255+
return MetricExportResult.FAILURE
239256
if (
240-
not _is_retryable(resp)
241-
or retry_num + 1 == _MAX_RETRYS
257+
retry_num + 1 == _MAX_RETRYS
242258
or backoff_seconds > (deadline_sec - time())
243259
or self._shutdown
244260
):
245261
_logger.error(
246-
"Failed to export metrics batch code: %s, reason: %s",
247-
resp.status_code,
248-
resp.text,
262+
"Failed to export metrics batch due to timeout, "
263+
"max retries or shutdown."
249264
)
250265
return MetricExportResult.FAILURE
251266
_logger.warning(
252267
"Transient error %s encountered while exporting metrics batch, retrying in %.2fs.",
253-
resp.reason,
268+
reason,
254269
backoff_seconds,
255270
)
256271
shutdown = self._shutdown_in_progress.wait(backoff_seconds)

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,26 +179,42 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
179179
serialized_data = encode_spans(spans).SerializePartialToString()
180180
deadline_sec = time() + self._timeout
181181
for retry_num in range(_MAX_RETRYS):
182-
resp = self._export(serialized_data, deadline_sec - time())
183-
if resp.ok:
184-
return SpanExportResult.SUCCESS
185182
# multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff.
186183
backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2)
184+
try:
185+
resp = self._export(serialized_data, deadline_sec - time())
186+
if resp.ok:
187+
return SpanExportResult.SUCCESS
188+
except requests.exceptions.RequestException as error:
189+
reason = error
190+
retryable = isinstance(error, ConnectionError)
191+
status_code = None
192+
else:
193+
reason = resp.reason
194+
retryable = _is_retryable(resp)
195+
status_code = resp.status_code
196+
197+
if not retryable:
198+
_logger.error(
199+
"Failed to export span batch code: %s, reason: %s",
200+
status_code,
201+
reason,
202+
)
203+
return SpanExportResult.FAILURE
204+
187205
if (
188-
not _is_retryable(resp)
189-
or retry_num + 1 == _MAX_RETRYS
206+
retry_num + 1 == _MAX_RETRYS
190207
or backoff_seconds > (deadline_sec - time())
191208
or self._shutdown
192209
):
193210
_logger.error(
194-
"Failed to export span batch code: %s, reason: %s",
195-
resp.status_code,
196-
resp.text,
211+
"Failed to export span batch due to timeout, "
212+
"max retries or shutdown."
197213
)
198214
return SpanExportResult.FAILURE
199215
_logger.warning(
200216
"Transient error %s encountered while exporting span batch, retrying in %.2fs.",
201-
resp.reason,
217+
reason,
202218
backoff_seconds,
203219
)
204220
shutdown = self._shutdown_in_progress.wait(backoff_seconds)

exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
from unittest import TestCase
2020
from unittest.mock import ANY, MagicMock, Mock, patch
2121

22+
import requests
2223
from requests import Session
24+
from requests.exceptions import ConnectionError
2325
from requests.models import Response
2426

2527
from opentelemetry.exporter.otlp.proto.common.metrics_encoder import (
@@ -555,6 +557,40 @@ def test_retry_timeout(self, mock_post):
555557
warning.records[0].message,
556558
)
557559

560+
@patch.object(Session, "post")
561+
def test_export_no_collector_available_retryable(self, mock_post):
562+
exporter = OTLPMetricExporter(timeout=1.5)
563+
msg = "Server not available."
564+
mock_post.side_effect = ConnectionError(msg)
565+
with self.assertLogs(level=WARNING) as warning:
566+
self.assertEqual(
567+
exporter.export(self.metrics["sum_int"]),
568+
MetricExportResult.FAILURE,
569+
)
570+
# Check for greater 2 because the request is on each retry
571+
# done twice at the moment.
572+
self.assertGreater(mock_post.call_count, 2)
573+
self.assertIn(
574+
f"Transient error {msg} encountered while exporting metrics batch, retrying in",
575+
warning.records[0].message,
576+
)
577+
578+
@patch.object(Session, "post")
579+
def test_export_no_collector_available(self, mock_post):
580+
exporter = OTLPMetricExporter(timeout=1.5)
581+
582+
mock_post.side_effect = requests.exceptions.RequestException()
583+
with self.assertLogs(level=WARNING) as warning:
584+
self.assertEqual(
585+
exporter.export(self.metrics["sum_int"]),
586+
MetricExportResult.FAILURE,
587+
)
588+
self.assertEqual(mock_post.call_count, 1)
589+
self.assertIn(
590+
"Failed to export metrics batch code",
591+
warning.records[0].message,
592+
)
593+
558594
@patch.object(Session, "post")
559595
def test_timeout_set_correctly(self, mock_post):
560596
resp = Response()

exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import requests
2525
from google.protobuf.json_format import MessageToDict
2626
from requests import Session
27+
from requests.exceptions import ConnectionError
2728
from requests.models import Response
2829

2930
from opentelemetry._logs import LogRecord, SeverityNumber
@@ -483,6 +484,40 @@ def test_retry_timeout(self, mock_post):
483484
warning.records[0].message,
484485
)
485486

487+
@patch.object(Session, "post")
488+
def test_export_no_collector_available_retryable(self, mock_post):
489+
exporter = OTLPLogExporter(timeout=1.5)
490+
msg = "Server not available."
491+
mock_post.side_effect = ConnectionError(msg)
492+
with self.assertLogs(level=WARNING) as warning:
493+
self.assertEqual(
494+
exporter.export(self._get_sdk_log_data()),
495+
LogRecordExportResult.FAILURE,
496+
)
497+
# Check for greater 2 because the request is on each retry
498+
# done twice at the moment.
499+
self.assertGreater(mock_post.call_count, 2)
500+
self.assertIn(
501+
f"Transient error {msg} encountered while exporting logs batch, retrying in",
502+
warning.records[0].message,
503+
)
504+
505+
@patch.object(Session, "post")
506+
def test_export_no_collector_available(self, mock_post):
507+
exporter = OTLPLogExporter(timeout=1.5)
508+
509+
mock_post.side_effect = requests.exceptions.RequestException()
510+
with self.assertLogs(level=WARNING) as warning:
511+
self.assertEqual(
512+
exporter.export(self._get_sdk_log_data()),
513+
LogRecordExportResult.FAILURE,
514+
)
515+
self.assertEqual(mock_post.call_count, 1)
516+
self.assertIn(
517+
"Failed to export logs batch code",
518+
warning.records[0].message,
519+
)
520+
486521
@patch.object(Session, "post")
487522
def test_timeout_set_correctly(self, mock_post):
488523
resp = Response()

0 commit comments

Comments
 (0)