Skip to content

Commit f503053

Browse files
committed
Merge remote-tracking branch 'origin/main' into shutdown_refactor
2 parents f2583e0 + 6ed676a commit f503053

File tree

36 files changed

+177
-186
lines changed

36 files changed

+177
-186
lines changed

.github/workflows/benchmarks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66

77
jobs:
88
sdk-benchmarks:
9-
runs-on: self-hosted
9+
runs-on: equinix-bare-metal
1010
steps:
1111
- name: Checkout Core Repo @ SHA - ${{ github.sha }}
1212
uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
had completed/failed was removed.
1313
([#4564](https://github.com/open-telemetry/opentelemetry-python/pull/4564)).
1414

15+
- Update OTLP gRPC/HTTP exporters: the export timeout is now inclusive of all retries and backoffs.
16+
A +/-20% jitter was added to all backoffs. A pointless 32 second sleep that occurred after all retries
17+
had completed/failed was removed.
18+
([#4564](https://github.com/open-telemetry/opentelemetry-python/pull/4564)).
19+
- Update ConsoleLogExporter.export to handle LogRecord's containing bytes type
20+
in the body ([#4614](https://github.com/open-telemetry/opentelemetry-python/pull/4614/)).
21+
- opentelemetry-sdk: Fix invalid `type: ignore` that causes mypy to ignore the whole file
22+
([#4618](https://github.com/open-telemetry/opentelemetry-python/pull/4618))
23+
- Add `span_exporter` property back to `BatchSpanProcessor` class
24+
([#4621](https://github.com/open-telemetry/opentelemetry-python/pull/4621))
25+
- Fix license field in pyproject.toml files
26+
([#4625](https://github.com/open-telemetry/opentelemetry-python/pull/4625))
27+
1528
## Version 1.34.0/0.55b0 (2025-06-04)
1629

1730
- typecheck: add sdk/resources and drop mypy

docs/examples/error_handler/error_handler_0/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ name = "error-handler-0"
77
dynamic = ["version"]
88
description = "This is just an error handler example package"
99
readme = "README.rst"
10-
license = {text = "Apache-2.0"}
10+
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
1414
]
1515
classifiers = [
1616
"Development Status :: 4 - Beta",
1717
"Intended Audience :: Developers",
18-
"License :: OSI Approved :: Apache Software License",
1918
"Programming Language :: Python",
2019
"Programming Language :: Python :: 3",
2120
"Programming Language :: Python :: 3.9",

docs/examples/error_handler/error_handler_1/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ name = "error-handler-1"
77
dynamic = ["version"]
88
description = "This is just an error handler example package"
99
readme = "README.rst"
10-
license = {text = "Apache-2.0"}
10+
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
1414
]
1515
classifiers = [
1616
"Development Status :: 4 - Beta",
1717
"Intended Audience :: Developers",
18-
"License :: OSI Approved :: Apache Software License",
1918
"Programming Language :: Python",
2019
"Programming Language :: Python :: 3",
2120
"Programming Language :: Python :: 3.9",

exporter/opentelemetry-exporter-opencensus/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "opentelemetry-exporter-opencensus"
77
dynamic = ["version"]
88
description = "OpenCensus Exporter"
99
readme = "README.rst"
10-
license = {text = "Apache-2.0"}
10+
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
@@ -17,7 +17,6 @@ classifiers = [
1717
"Framework :: OpenTelemetry",
1818
"Framework :: OpenTelemetry :: Exporters",
1919
"Intended Audience :: Developers",
20-
"License :: OSI Approved :: Apache Software License",
2120
"Programming Language :: Python",
2221
"Programming Language :: Python :: 3",
2322
"Programming Language :: Python :: 3.9",

exporter/opentelemetry-exporter-otlp-proto-common/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "opentelemetry-exporter-otlp-proto-common"
77
dynamic = ["version"]
88
description = "OpenTelemetry Protobuf encoding"
99
readme = "README.rst"
10-
license = {text = "Apache-2.0"}
10+
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
@@ -17,7 +17,6 @@ classifiers = [
1717
"Framework :: OpenTelemetry",
1818
"Framework :: OpenTelemetry :: Exporters",
1919
"Intended Audience :: Developers",
20-
"License :: OSI Approved :: Apache Software License",
2120
"Programming Language :: Python",
2221
"Programming Language :: Python :: 3",
2322
"Programming Language :: Python :: 3.9",

exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "opentelemetry-exporter-otlp-proto-grpc"
77
dynamic = ["version"]
88
description = "OpenTelemetry Collector Protobuf over gRPC Exporter"
99
readme = "README.rst"
10-
license = {text = "Apache-2.0"}
10+
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [
1313
{ name = "OpenTelemetry Authors", email = "[email protected]" },
@@ -17,7 +17,6 @@ classifiers = [
1717
"Framework :: OpenTelemetry",
1818
"Framework :: OpenTelemetry :: Exporters",
1919
"Intended Audience :: Developers",
20-
"License :: OSI Approved :: Apache Software License",
2120
"Programming Language :: Python",
2221
"Programming Language :: Python :: 3",
2322
"Programming Language :: Python :: 3.9",

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

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -293,52 +293,55 @@ def _export(
293293
# FIXME remove this check if the export type for traces
294294
# gets updated to a class that represents the proto
295295
# TracesData and use the code below instead.
296-
retry_info = RetryInfo()
297-
deadline_sec = time() + self._timeout
298-
for retry_num in range(1, _MAX_RETRYS + 1):
299-
backoff_seconds = 2 ** (retry_num - 1) * random.uniform(
300-
0.8, 1.2
301-
)
302-
try:
303-
self._client.Export(
304-
request=self._translate_data(data),
305-
metadata=self._headers,
306-
timeout=self._timeout,
307-
)
308-
return self._result.SUCCESS
309-
except RpcError as error:
310-
retry_info_bin = dict(error.trailing_metadata()).get(
311-
"google.rpc.retryinfo-bin"
312-
)
313-
if retry_info_bin is not None:
314-
retry_info.ParseFromString(retry_info_bin)
315-
backoff_seconds = (
316-
retry_info.retry_delay.seconds
317-
+ retry_info.retry_delay.nanos / 1.0e9
296+
with self._export_lock:
297+
deadline_sec = time() + self._timeout
298+
for retry_num in range(_MAX_RETRYS):
299+
try:
300+
self._client.Export(
301+
request=self._translate_data(data),
302+
metadata=self._headers,
303+
timeout=deadline_sec - time(),
304+
)
305+
return self._result.SUCCESS
306+
except RpcError as error:
307+
retry_info_bin = dict(error.trailing_metadata()).get(
308+
"google.rpc.retryinfo-bin"
318309
)
319-
if (
320-
error.code() not in _RETRYABLE_ERROR_CODES
321-
or retry_num == _MAX_RETRYS
322-
or backoff_seconds > (deadline_sec - time())
323-
or self._shutdown
324-
):
325-
logger.error(
326-
"Failed to export %s to %s, error code: %s",
310+
# multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff.
311+
backoff_seconds = 2**retry_num * random.uniform(0.8, 1.2)
312+
if retry_info_bin is not None:
313+
retry_info = RetryInfo()
314+
retry_info.ParseFromString(retry_info_bin)
315+
backoff_seconds = (
316+
retry_info.retry_delay.seconds
317+
+ retry_info.retry_delay.nanos / 1.0e9
318+
)
319+
if (
320+
error.code() not in _RETRYABLE_ERROR_CODES
321+
or retry_num + 1 == _MAX_RETRYS
322+
or backoff_seconds > (deadline_sec - time())
323+
or self._shutdown
324+
):
325+
logger.error(
326+
"Failed to export %s to %s, error code: %s",
327+
self._exporting,
328+
self._endpoint,
329+
error.code(),
330+
exc_info=error.code() == StatusCode.UNKNOWN,
331+
)
332+
return self._result.FAILURE
333+
logger.warning(
334+
"Transient error %s encountered while exporting %s to %s, retrying in %.2fs.",
335+
error.code(),
327336
self._exporting,
328337
self._endpoint,
329-
error.code(),
330-
exc_info=error.code() == StatusCode.UNKNOWN,
338+
backoff_seconds,
331339
)
332-
return self._result.FAILURE
333-
logger.warning(
334-
"Transient error %s encountered while exporting logs batch, retrying in %.2fs.",
335-
error.code(),
336-
backoff_seconds,
337-
)
338340
shutdown = self._shutdown_is_occuring.wait(backoff_seconds)
339341
if shutdown:
340342
logger.warning("Shutdown in progress, aborting retry.")
341343
break
344+
# Not possible to reach here but the linter is complaining.
342345
return self._result.FAILURE
343346

344347
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:

exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tomli==2.0.1
1111
typing_extensions==4.10.0
1212
wrapt==1.16.0
1313
zipp==3.19.2
14+
googleapis-common-protos==1.63.2
1415
-e opentelemetry-api
1516
-e tests/opentelemetry-test-utils
1617
-e exporter/opentelemetry-exporter-otlp-proto-common

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

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
import threading
1616
import time
17+
import unittest
1718
from concurrent.futures import ThreadPoolExecutor
1819
from logging import WARNING, getLogger
20+
from platform import system
1921
from typing import Any, Optional, Sequence
2022
from unittest import TestCase
2123
from unittest.mock import Mock, patch
@@ -90,33 +92,32 @@ class TraceServiceServicerWithExportParams(TraceServiceServicer):
9092
def __init__(
9193
self,
9294
export_result: StatusCode,
93-
optional_retry_millis: Optional[int] = None,
95+
optional_retry_nanos: Optional[int] = None,
9496
optional_export_sleep: Optional[float] = None,
9597
):
9698
self.export_result = export_result
9799
self.optional_export_sleep = optional_export_sleep
98-
self.optional_retry_millis = optional_retry_millis
100+
self.optional_retry_nanos = optional_retry_nanos
99101
self.num_requests = 0
100102

101103
# pylint: disable=invalid-name,unused-argument
102104
def Export(self, request, context):
103105
self.num_requests += 1
104106
if self.optional_export_sleep:
105107
time.sleep(self.optional_export_sleep)
106-
if self.export_result != StatusCode.OK:
107-
if self.optional_retry_millis:
108-
context.set_trailing_metadata(
108+
if self.export_result != StatusCode.OK and self.optional_retry_nanos:
109+
context.set_trailing_metadata(
110+
(
109111
(
110-
(
111-
"google.rpc.retryinfo-bin",
112-
RetryInfo(
113-
retry_delay=Duration(
114-
nanos=self.optional_retry_millis
115-
)
116-
).SerializeToString(),
117-
),
118-
)
112+
"google.rpc.retryinfo-bin",
113+
RetryInfo(
114+
retry_delay=Duration(
115+
nanos=self.optional_retry_nanos
116+
)
117+
).SerializeToString(),
118+
),
119119
)
120+
)
120121
context.set_code(self.export_result)
121122

122123
return ExportTraceServiceResponse()
@@ -360,9 +361,14 @@ def test_export_over_closed_grpc_channel(self):
360361
str(err.exception), "Cannot invoke RPC on closed channel!"
361362
)
362363

364+
@unittest.skipIf(
365+
system() == "Windows",
366+
"For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.",
367+
)
363368
def test_retry_info_is_respected(self):
364369
mock_trace_service = TraceServiceServicerWithExportParams(
365-
StatusCode.UNAVAILABLE, optional_retry_millis=200
370+
StatusCode.UNAVAILABLE,
371+
optional_retry_nanos=200000000, # .2 seconds
366372
)
367373
add_TraceServiceServicer_to_server(
368374
mock_trace_service,
@@ -376,9 +382,13 @@ def test_retry_info_is_respected(self):
376382
)
377383
after = time.time()
378384
self.assertEqual(mock_trace_service.num_requests, 6)
379-
# 200 * 5 = 1 second, plus some wiggle room so the test passes consistently.
380-
self.assertTrue(after - before < 1.1)
385+
# 1 second plus wiggle room so the test passes consistently.
386+
self.assertAlmostEqual(after - before, 1, 1)
381387

388+
@unittest.skipIf(
389+
system() == "Windows",
390+
"For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.",
391+
)
382392
def test_retry_not_made_if_would_exceed_timeout(self):
383393
mock_trace_service = TraceServiceServicerWithExportParams(
384394
StatusCode.UNAVAILABLE
@@ -398,34 +408,39 @@ def test_retry_not_made_if_would_exceed_timeout(self):
398408
# First call at time 0, second at time 1, third at time 3, fourth would exceed timeout.
399409
self.assertEqual(mock_trace_service.num_requests, 3)
400410
# There's a +/-20% jitter on each backoff.
401-
self.assertTrue(after - before < 3.7)
411+
self.assertTrue(2.35 < after - before < 3.65)
402412

413+
@unittest.skipIf(
414+
system() == "Windows",
415+
"For gRPC + windows there's some added delay in the RPCs which breaks the assertion over amount of time passed.",
416+
)
403417
def test_timeout_set_correctly(self):
404418
mock_trace_service = TraceServiceServicerWithExportParams(
405-
StatusCode.OK, optional_export_sleep=0.5
419+
StatusCode.UNAVAILABLE, optional_export_sleep=0.25
406420
)
407421
add_TraceServiceServicer_to_server(
408422
mock_trace_service,
409423
self.server,
410424
)
411-
exporter = OTLPSpanExporterForTesting(insecure=True, timeout=0.4)
412-
# Should timeout. Deadline should be set to now + timeout.
413-
# That is 400 millis from now, and export sleeps for 500 millis.
425+
exporter = OTLPSpanExporterForTesting(insecure=True, timeout=1.4)
426+
# Should timeout after 1.4 seconds. First attempt takes .25 seconds
427+
# Then a 1 second sleep, then deadline exceeded after .15 seconds,
428+
# mid way through second call.
414429
with self.assertLogs(level=WARNING) as warning:
415-
self.assertEqual(
416-
exporter.export([self.span]),
417-
SpanExportResult.FAILURE,
418-
)
430+
before = time.time()
431+
# Eliminate the jitter.
432+
with patch("random.uniform", return_value=1):
433+
self.assertEqual(
434+
exporter.export([self.span]),
435+
SpanExportResult.FAILURE,
436+
)
437+
after = time.time()
419438
self.assertEqual(
420439
"Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED",
421440
warning.records[-1].message,
422441
)
423-
self.assertEqual(mock_trace_service.num_requests, 1)
424-
exporter = OTLPSpanExporterForTesting(insecure=True, timeout=0.8)
425-
self.assertEqual(
426-
exporter.export([self.span]),
427-
SpanExportResult.SUCCESS,
428-
)
442+
self.assertEqual(mock_trace_service.num_requests, 2)
443+
self.assertAlmostEqual(after - before, 1.4, 1)
429444

430445
def test_otlp_headers_from_env(self):
431446
# pylint: disable=protected-access

0 commit comments

Comments
 (0)