Skip to content

Commit 43f2c09

Browse files
authored
Merge pull request #13 from honeycombio/codeboten/generate-config-model-from-schema
fix code-generation command and regenerate models
2 parents 43f3fe5 + 99e9570 commit 43f2c09

File tree

32 files changed

+1015
-482
lines changed

32 files changed

+1015
-482
lines changed

.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: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-exporter-otlp-proto-grpc`: Fix re-initialization of gRPC channel on UNAVAILABLE error
16+
([#4825](https://github.com/open-telemetry/opentelemetry-python/pull/4825))
17+
- `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets
18+
([#4868](https://github.com/open-telemetry/opentelemetry-python/issues/4868))
19+
- Allow loading all resource detectors by setting `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` to `*`
20+
([#4819](https://github.com/open-telemetry/opentelemetry-python/pull/4819))
1521
- `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)
22+
([#4837](https://github.com/open-telemetry/opentelemetry-python/pull/4837)).
1723
- Regenerate opentelemetry-proto code with v1.9.0 release
1824
([#4840](https://github.com/open-telemetry/opentelemetry-python/pull/4840))
1925
- Add python 3.14 support
2026
([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798))
2127
- Silence events API warnings for internal users
2228
([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847))
29+
- Prevent possible endless recursion from happening in `SimpleLogRecordProcessor.on_emit`,
30+
([#4799](https://github.com/open-telemetry/opentelemetry-python/pull/4799)) and ([#4867](https://github.com/open-telemetry/opentelemetry-python/pull/4867)).
31+
- Make ConcurrentMultiSpanProcessor fork safe
32+
([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862))
33+
- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters
34+
([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709))
35+
- `opentelemetry-sdk`: automatically generate configuration models using OTel config JSON schema
36+
([#4879](https://github.com/open-telemetry/opentelemetry-python/pull/4879))
2337

2438
## Version 1.39.0/0.60b0 (2025-12-03)
2539

@@ -87,7 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
87101
([#4654](https://github.com/open-telemetry/opentelemetry-python/pull/4654)).
88102
- Fix type checking for built-in metric exporters
89103
([#4820](https://github.com/open-telemetry/opentelemetry-python/pull/4820))
90-
104+
91105
## Version 1.38.0/0.59b0 (2025-10-16)
92106

93107
- 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-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""OTLP Exporter"""
15+
"""OTLP Exporter
16+
17+
This module provides a mixin class for OTLP exporters that send telemetry data
18+
to an OTLP-compatible receiver via gRPC. It includes a configurable reconnection
19+
logic to handle transient collector outages.
20+
21+
"""
1622

1723
import random
1824
import threading
@@ -251,20 +257,27 @@ def _get_credentials(
251257
if certificate_file:
252258
client_key_file = environ.get(client_key_file_env_key)
253259
client_certificate_file = environ.get(client_certificate_file_env_key)
254-
return _load_credentials(
260+
credentials = _load_credentials(
255261
certificate_file, client_key_file, client_certificate_file
256262
)
263+
if credentials is not None:
264+
return credentials
257265
return ssl_channel_credentials()
258266

259267

260268
# pylint: disable=no-member
261269
class OTLPExporterMixin(
262270
ABC, Generic[SDKDataT, ExportServiceRequestT, ExportResultT, ExportStubT]
263271
):
264-
"""OTLP span exporter
272+
"""OTLP gRPC exporter mixin.
273+
274+
This class provides the base functionality for OTLP exporters that send
275+
telemetry data (spans or metrics) to an OTLP-compatible receiver via gRPC.
276+
It includes a configurable reconnection mechanism to handle transient
277+
receiver outages.
265278
266279
Args:
267-
endpoint: OpenTelemetry Collector receiver endpoint
280+
endpoint: OTLP-compatible receiver endpoint
268281
insecure: Connection type
269282
credentials: ChannelCredentials object for server authentication
270283
headers: Headers to send when exporting
@@ -308,6 +321,8 @@ def __init__(
308321
if parsed_url.netloc:
309322
self._endpoint = parsed_url.netloc
310323

324+
self._insecure = insecure
325+
self._credentials = credentials
311326
self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS)
312327
if isinstance(self._headers, str):
313328
temp_headers = parse_env_headers(self._headers, liberal=True)
@@ -336,37 +351,52 @@ def __init__(
336351
)
337352
self._collector_kwargs = None
338353

339-
compression = (
354+
self._compression = (
340355
environ_to_compression(OTEL_EXPORTER_OTLP_COMPRESSION)
341356
if compression is None
342357
else compression
343358
) or Compression.NoCompression
344359

345-
if insecure:
346-
self._channel = insecure_channel(
347-
self._endpoint,
348-
compression=compression,
349-
options=self._channel_options,
350-
)
351-
else:
360+
self._channel = None
361+
self._client = None
362+
363+
self._shutdown_in_progress = threading.Event()
364+
self._shutdown = False
365+
366+
if not self._insecure:
352367
self._credentials = _get_credentials(
353-
credentials,
368+
self._credentials,
354369
_OTEL_PYTHON_EXPORTER_OTLP_GRPC_CREDENTIAL_PROVIDER,
355370
OTEL_EXPORTER_OTLP_CERTIFICATE,
356371
OTEL_EXPORTER_OTLP_CLIENT_KEY,
357372
OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE,
358373
)
374+
375+
self._initialize_channel_and_stub()
376+
377+
def _initialize_channel_and_stub(self):
378+
"""
379+
Create a new gRPC channel and stub.
380+
381+
This method is used during initialization and by the reconnection
382+
mechanism to reinitialize the channel on transient errors.
383+
"""
384+
if self._insecure:
385+
self._channel = insecure_channel(
386+
self._endpoint,
387+
compression=self._compression,
388+
options=self._channel_options,
389+
)
390+
else:
391+
assert self._credentials is not None
359392
self._channel = secure_channel(
360393
self._endpoint,
361394
self._credentials,
362-
compression=compression,
395+
compression=self._compression,
363396
options=self._channel_options,
364397
)
365398
self._client = self._stub(self._channel) # type: ignore [reportCallIssue]
366399

367-
self._shutdown_in_progress = threading.Event()
368-
self._shutdown = False
369-
370400
@abstractmethod
371401
def _translate_data(
372402
self,
@@ -388,6 +418,8 @@ def _export(
388418
deadline_sec = time() + self._timeout
389419
for retry_num in range(_MAX_RETRYS):
390420
try:
421+
if self._client is None:
422+
return self._result.FAILURE
391423
self._client.Export(
392424
request=self._translate_data(data),
393425
metadata=self._headers,
@@ -407,6 +439,26 @@ def _export(
407439
retry_info.retry_delay.seconds
408440
+ retry_info.retry_delay.nanos / 1.0e9
409441
)
442+
443+
# For UNAVAILABLE errors, reinitialize the channel to force reconnection
444+
if error.code() == StatusCode.UNAVAILABLE and retry_num == 0: # type: ignore
445+
logger.debug(
446+
"Reinitializing gRPC channel for %s exporter due to UNAVAILABLE error",
447+
self._exporting,
448+
)
449+
try:
450+
if self._channel:
451+
self._channel.close()
452+
except Exception as e:
453+
logger.debug(
454+
"Error closing channel for %s exporter to %s: %s",
455+
self._exporting,
456+
self._endpoint,
457+
str(e),
458+
)
459+
# Enable channel reconnection for subsequent calls
460+
self._initialize_channel_and_stub()
461+
410462
if (
411463
error.code() not in _RETRYABLE_ERROR_CODES # type: ignore [reportAttributeAccessIssue]
412464
or retry_num + 1 == _MAX_RETRYS
@@ -436,12 +488,19 @@ def _export(
436488
return self._result.FAILURE # type: ignore [reportReturnType]
437489

438490
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
491+
"""
492+
Shut down the exporter.
493+
494+
Args:
495+
timeout_millis: Timeout in milliseconds for shutting down the exporter.
496+
"""
439497
if self._shutdown:
440498
logger.warning("Exporter already shutdown, ignoring call")
441499
return
442500
self._shutdown = True
443501
self._shutdown_in_progress.set()
444-
self._channel.close()
502+
if self._channel:
503+
self._channel.close()
445504

446505
@property
447506
@abstractmethod

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from unittest import TestCase
2525
from unittest.mock import Mock, patch
2626

27+
import grpc
2728
from google.protobuf.duration_pb2 import ( # pylint: disable=no-name-in-module
2829
Duration,
2930
)
@@ -91,8 +92,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
9192
def _exporting(self):
9293
return "traces"
9394

94-
def shutdown(self, timeout_millis=30_000):
95-
return OTLPExporterMixin.shutdown(self, timeout_millis)
95+
def shutdown(self, timeout_millis: float = 30_000, **kwargs):
96+
return OTLPExporterMixin.shutdown(self, timeout_millis, **kwargs)
9697

9798

9899
class TraceServiceServicerWithExportParams(TraceServiceServicer):
@@ -513,6 +514,16 @@ def test_timeout_set_correctly(self):
513514
self.assertEqual(mock_trace_service.num_requests, 2)
514515
self.assertAlmostEqual(after - before, 1.4, 1)
515516

517+
def test_channel_options_set_correctly(self):
518+
"""Test that gRPC channel options are set correctly for keepalive and reconnection"""
519+
# This test verifies that the channel is created with the right options
520+
# We patch grpc.insecure_channel to ensure it is called without errors
521+
with patch(
522+
"opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel"
523+
) as mock_channel:
524+
OTLPSpanExporterForTesting(insecure=True)
525+
self.assertTrue(mock_channel.called)
526+
516527
def test_otlp_headers_from_env(self):
517528
# pylint: disable=protected-access
518529
# This ensures that there is no other header than standard user-agent.
@@ -536,3 +547,27 @@ def test_permanent_failure(self):
536547
warning.records[-1].message,
537548
"Failed to export traces to localhost:4317, error code: StatusCode.ALREADY_EXISTS",
538549
)
550+
551+
def test_unavailable_reconnects(self):
552+
"""Test that the exporter reconnects on UNAVAILABLE error"""
553+
add_TraceServiceServicer_to_server(
554+
TraceServiceServicerWithExportParams(StatusCode.UNAVAILABLE),
555+
self.server,
556+
)
557+
558+
# Spy on grpc.insecure_channel to verify it's called for reconnection
559+
with patch(
560+
"opentelemetry.exporter.otlp.proto.grpc.exporter.insecure_channel",
561+
side_effect=grpc.insecure_channel,
562+
) as mock_insecure_channel:
563+
# Mock sleep to avoid waiting
564+
with patch("time.sleep"):
565+
# We expect FAILURE because the server keeps returning UNAVAILABLE
566+
# but we want to verify reconnection attempts happened
567+
self.exporter.export([self.span])
568+
569+
# Verify that we attempted to reinitialize the channel (called insecure_channel)
570+
# Since the initial channel was created in setUp (unpatched), this call
571+
# must be from the reconnection logic.
572+
self.assertTrue(mock_insecure_channel.called)
573+
# Verify that reconnection enabled flag is set

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)

0 commit comments

Comments
 (0)