From edde6707e155e16865e3a7029207d6a0d49dc382 Mon Sep 17 00:00:00 2001 From: Zhikuan Wei Date: Tue, 5 May 2026 23:34:16 -0400 Subject: [PATCH 1/5] add error message logging for grpc exporter --- .../exporter/otlp/proto/grpc/exporter.py | 6 ++- .../tests/test_otlp_exporter_mixin.py | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index b3a4463b98..a289522141 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -507,10 +507,11 @@ def _export( or self._shutdown ): logger.error( - "Failed to export %s to %s, error code: %s", + "Failed to export %s to %s, error code: %s%s", self._exporting, self._endpoint, error.code(), # type: ignore [reportAttributeAccessIssue] + f", details: {error.details()}" if error.details() else "", # type: ignore [reportAttributeAccessIssue] exc_info=error.code() == StatusCode.UNKNOWN, # type: ignore [reportAttributeAccessIssue] ) result.error = error @@ -519,11 +520,12 @@ def _export( } return self._result.FAILURE # type: ignore [reportReturnType] logger.warning( - "Transient error %s encountered while exporting %s to %s, retrying in %.2fs.", + "Transient error %s encountered while exporting %s to %s, retrying in %.2fs.%s", error.code(), # type: ignore [reportAttributeAccessIssue] self._exporting, self._endpoint, backoff_seconds, + f" Details: {error.details()}" if error.details() else "", # type: ignore [reportAttributeAccessIssue] ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) if shutdown: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 8736a6dc56..8ec3a095ff 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -108,10 +108,12 @@ def __init__( export_result: StatusCode, optional_retry_nanos: int | None = None, optional_export_sleep: float | None = None, + optional_details: str | None = None, ): self.export_result = export_result self.optional_export_sleep = optional_export_sleep self.optional_retry_nanos = optional_retry_nanos + self.optional_details = optional_details self.num_requests = 0 # pylint: disable=invalid-name,unused-argument @@ -132,6 +134,8 @@ def Export(self, request, context): ), ) ) + if self.optional_details: + context.set_details(self.optional_details) context.set_code(self.export_result) return ExportTraceServiceResponse() @@ -763,6 +767,43 @@ def test_retryable_error_codes_custom(self): ) self.assertEqual(mock_trace_service.num_requests, 1) + def test_error_details_logged_on_permanent_failure(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerWithExportParams( + StatusCode.INVALID_ARGUMENT, + optional_details="field 'resource' is missing", + ), + self.server, + ) + with self.assertLogs(level=WARNING) as warning: + self.assertEqual( + self.exporter.export([self.span]), SpanExportResult.FAILURE + ) + self.assertIn( + "field 'resource' is missing", + warning.records[-1].message, + ) + + def test_error_details_logged_on_transient_failure(self): + add_TraceServiceServicer_to_server( + TraceServiceServicerWithExportParams( + StatusCode.UNAVAILABLE, + optional_retry_nanos=100000000, # .1 seconds + optional_details="collector is restarting", + ), + self.server, + ) + exporter = OTLPSpanExporterForTesting( + insecure=True, timeout=10, meter_provider=self.meter_provider + ) + with self.assertLogs(level=WARNING) as warning: + exporter.export([self.span]) + transient_warnings = [ + r for r in warning.records if "Transient" in r.message + ] + self.assertTrue(len(transient_warnings) > 0) + self.assertIn("collector is restarting", transient_warnings[0].message) + def assert_standard_metric_attrs(self, attributes): self.assertEqual( attributes["otel.component.type"], "otlp_grpc_span_exporter" From a2b1ec549e8d6802c4dcb3f53b20cff84c1a5e5e Mon Sep 17 00:00:00 2001 From: Zhikuan Wei Date: Thu, 7 May 2026 13:21:38 -0400 Subject: [PATCH 2/5] update changes --- .../opentelemetry/exporter/otlp/proto/grpc/exporter.py | 8 ++++++-- .../tests/test_otlp_exporter_mixin.py | 10 ++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index a289522141..ad691799f4 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -511,7 +511,9 @@ def _export( self._exporting, self._endpoint, error.code(), # type: ignore [reportAttributeAccessIssue] - f", details: {error.details()}" if error.details() else "", # type: ignore [reportAttributeAccessIssue] + f", details: {error.details()}" + if error.details() + else "", exc_info=error.code() == StatusCode.UNKNOWN, # type: ignore [reportAttributeAccessIssue] ) result.error = error @@ -525,7 +527,9 @@ def _export( self._exporting, self._endpoint, backoff_seconds, - f" Details: {error.details()}" if error.details() else "", # type: ignore [reportAttributeAccessIssue] + f" Details: {error.details()}" + if error.details() + else "", ) shutdown = self._shutdown_in_progress.wait(backoff_seconds) if shutdown: diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index 8ec3a095ff..b645be81ea 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -788,7 +788,7 @@ def test_error_details_logged_on_transient_failure(self): add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams( StatusCode.UNAVAILABLE, - optional_retry_nanos=100000000, # .1 seconds + optional_retry_nanos=200000000, optional_details="collector is restarting", ), self.server, @@ -798,11 +798,9 @@ def test_error_details_logged_on_transient_failure(self): ) with self.assertLogs(level=WARNING) as warning: exporter.export([self.span]) - transient_warnings = [ - r for r in warning.records if "Transient" in r.message - ] - self.assertTrue(len(transient_warnings) > 0) - self.assertIn("collector is restarting", transient_warnings[0].message) + warning_logs = [r for r in warning.records if "Transient" in r.message] + self.assertTrue(len(warning_logs) > 0) + self.assertIn("collector is restarting", warning_logs[0].message) def assert_standard_metric_attrs(self, attributes): self.assertEqual( From eacd78a27d1faf2ad6f58f8280498cbbf3d0a4aa Mon Sep 17 00:00:00 2001 From: Zhikuan Wei Date: Fri, 8 May 2026 10:29:29 -0400 Subject: [PATCH 3/5] consolidate tests --- .../tests/test_otlp_exporter_mixin.py | 42 ++----------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py index b645be81ea..a125dfbc6d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_exporter_mixin.py @@ -593,7 +593,7 @@ def test_timeout_set_correctly(self): ) after = time.time() self.assertEqual( - "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED", + "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED, details: Deadline Exceeded", warning.records[-1].message, ) self.assertEqual(mock_trace_service.num_requests, 2) @@ -621,7 +621,8 @@ def test_permanent_failure(self): with self.assertLogs(level=WARNING) as warning: add_TraceServiceServicer_to_server( TraceServiceServicerWithExportParams( - StatusCode.ALREADY_EXISTS + StatusCode.ALREADY_EXISTS, + optional_details="resource already exists", ), self.server, ) @@ -630,7 +631,7 @@ def test_permanent_failure(self): ) self.assertEqual( warning.records[-1].message, - "Failed to export traces to localhost:4317, error code: StatusCode.ALREADY_EXISTS", + "Failed to export traces to localhost:4317, error code: StatusCode.ALREADY_EXISTS, details: resource already exists", ) metrics_data = self.metric_reader.get_metrics_data() @@ -767,41 +768,6 @@ def test_retryable_error_codes_custom(self): ) self.assertEqual(mock_trace_service.num_requests, 1) - def test_error_details_logged_on_permanent_failure(self): - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams( - StatusCode.INVALID_ARGUMENT, - optional_details="field 'resource' is missing", - ), - self.server, - ) - with self.assertLogs(level=WARNING) as warning: - self.assertEqual( - self.exporter.export([self.span]), SpanExportResult.FAILURE - ) - self.assertIn( - "field 'resource' is missing", - warning.records[-1].message, - ) - - def test_error_details_logged_on_transient_failure(self): - add_TraceServiceServicer_to_server( - TraceServiceServicerWithExportParams( - StatusCode.UNAVAILABLE, - optional_retry_nanos=200000000, - optional_details="collector is restarting", - ), - self.server, - ) - exporter = OTLPSpanExporterForTesting( - insecure=True, timeout=10, meter_provider=self.meter_provider - ) - with self.assertLogs(level=WARNING) as warning: - exporter.export([self.span]) - warning_logs = [r for r in warning.records if "Transient" in r.message] - self.assertTrue(len(warning_logs) > 0) - self.assertIn("collector is restarting", warning_logs[0].message) - def assert_standard_metric_attrs(self, attributes): self.assertEqual( attributes["otel.component.type"], "otlp_grpc_span_exporter" From 4898ff46e274438ed3e160fd8a8b107e179722f5 Mon Sep 17 00:00:00 2001 From: Zhikuan Wei Date: Fri, 8 May 2026 10:44:34 -0400 Subject: [PATCH 4/5] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91aaf9a7bc..ef30e11e02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > We are working on stabilizing the Log signal that would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for sdk developers. ## Unreleased - +- `opentelemetry-exporter-otlp-proto-grpc`: add detailed error message to the otlp grpc exporter + ([#5184](https://github.com/open-telemetry/opentelemetry-python/pull/5184)) - Apply fixes for `UP` ruff rule ([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133)) - Switch to SPDX license headers and add CI enforcement From 5eb0dec158aa2d8c7c66e614d3dff5a3ab3f8b71 Mon Sep 17 00:00:00 2001 From: Zhikuan Wei Date: Fri, 8 May 2026 10:45:07 -0400 Subject: [PATCH 5/5] add the empty line --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef30e11e02..9b5c1b9bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > We are working on stabilizing the Log signal that would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for sdk developers. ## Unreleased + - `opentelemetry-exporter-otlp-proto-grpc`: add detailed error message to the otlp grpc exporter ([#5184](https://github.com/open-telemetry/opentelemetry-python/pull/5184)) - Apply fixes for `UP` ruff rule