diff --git a/.changelog/5285.fixed b/.changelog/5285.fixed new file mode 100644 index 00000000000..abf513c1674 --- /dev/null +++ b/.changelog/5285.fixed @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp`: handle encoding exceptions in OTLP exporters. 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 60d26683695..ead24ee3988 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 @@ -455,6 +455,17 @@ def _export( return self._result.FAILURE # type: ignore [reportReturnType] with self._metrics.export_operation(self._count_data(data)) as result: + try: + request = self._translate_data(data) + except Exception as error: + logger.exception( + "Failed to encode %s batch: %s", + self._exporting, + error, + ) + result.error = error + return self._result.FAILURE # type: ignore [reportReturnType] + # FIXME remove this check if the export type for traces # gets updated to a class that represents the proto # TracesData and use the code below instead. @@ -464,7 +475,7 @@ def _export( if self._client is None: return self._result.FAILURE self._client.Export( - request=self._translate_data(data), + request=request, metadata=self._headers, timeout=deadline_sec - time(), ) 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 50996264a09..30d868d24b5 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 @@ -462,6 +462,22 @@ def test_shutdown_interrupts_export_retry_backoff(self): "Shutdown in progress, aborting retry.", ) + def test_encoding_error_returns_failure(self): + exporter = OTLPSpanExporterForTesting( + insecure=True, meter_provider=self.meter_provider + ) + encoding_error = ValueError("encoding failed") + with patch.object( + exporter, "_translate_data", side_effect=encoding_error + ): + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.grpc.exporter", + level="ERROR", + ) as log: + result = exporter.export([self.span]) + self.assertEqual(result, SpanExportResult.FAILURE) + self.assertIn("Failed to encode traces batch", log.records[0].message) + def test_export_over_closed_grpc_channel(self): # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 56906b96501..b050859ff69 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -199,7 +199,16 @@ def export( return LogRecordExportResult.FAILURE with self._metrics.export_operation(len(batch)) as result: - serialized_data = encode_logs(batch).SerializeToString() + try: + serialized_data = encode_logs(batch).SerializeToString() + except Exception as error: + _logger.exception( + "Failed to encode logs batch: %s", + error, + ) + result.error = error + return LogRecordExportResult.FAILURE + deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index eb1e69cfe4f..24f269894a3 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -349,7 +349,17 @@ def export( for metric in scope_metrics.metrics: num_items += len(metric.data.data_points) - export_request = encode_metrics(metrics_data) + try: + export_request = encode_metrics(metrics_data) + except Exception as error: + with self._metrics.export_operation(num_items) as result: + _logger.exception( + "Failed to encode metrics batch: %s", + error, + ) + result.error = error + return MetricExportResult.FAILURE + deadline_sec = time() + self._timeout # If no batch size configured, export as single batch with retries as configured diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 2be240103c0..0b73a17816d 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -192,7 +192,18 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: return SpanExportResult.FAILURE with self._metrics.export_operation(len(spans)) as result: - serialized_data = encode_spans(spans).SerializePartialToString() + try: + serialized_data = encode_spans( + spans + ).SerializePartialToString() + except Exception as error: + _logger.exception( + "Failed to encode span batch: %s", + error, + ) + result.error = error + return SpanExportResult.FAILURE + deadline_sec = time() + self._timeout for retry_num in range(_MAX_RETRYS): # multiplying by a random number between .8 and 1.2 introduces a +/20% jitter to each backoff. diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py index 84a11e8ae90..18180b5f9aa 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py @@ -128,6 +128,20 @@ def setUp(self): ), } + @patch( + "opentelemetry.exporter.otlp.proto.http.metric_exporter.encode_metrics", + side_effect=ValueError("encoding failed"), + ) + def test_encoding_error_returns_failure(self, _mock_encode): + exporter = OTLPMetricExporter(meter_provider=self.meter_provider) + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.http.metric_exporter", + level="ERROR", + ) as log: + result = exporter.export(self.metrics["sum_int"]) + self.assertEqual(result, MetricExportResult.FAILURE) + self.assertIn("Failed to encode metrics batch", log.records[0].message) + def test_constructor_default(self): exporter = OTLPMetricExporter() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index d7f3592e288..1c7f76b80ad 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -625,6 +625,20 @@ def export_side_effect(*args, **kwargs): exporter = OTLPLogExporter(timeout=0.4) exporter.export(self._get_sdk_log_data()) + @patch( + "opentelemetry.exporter.otlp.proto.http._log_exporter.encode_logs", + side_effect=ValueError("encoding failed"), + ) + def test_encoding_error_returns_failure(self, _mock_encode): + exporter = OTLPLogExporter(meter_provider=self.meter_provider) + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.http._log_exporter", + level="ERROR", + ) as log: + result = exporter.export(self._get_sdk_log_data()) + self.assertEqual(result, LogRecordExportResult.FAILURE) + self.assertIn("Failed to encode logs batch", log.records[0].message) + @patch.object(Session, "post") def test_shutdown_interrupts_retry_backoff(self, mock_post): exporter = OTLPLogExporter(timeout=1.5) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index 1580e5a1802..d0f7fcb2ed9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -457,6 +457,20 @@ def export_side_effect(*args, **kwargs): exporter = OTLPSpanExporter(timeout=0.4) exporter.export([BASIC_SPAN]) + @patch( + "opentelemetry.exporter.otlp.proto.http.trace_exporter.encode_spans", + side_effect=ValueError("encoding failed"), + ) + def test_encoding_error_returns_failure(self, _mock_encode): + exporter = OTLPSpanExporter(meter_provider=self.meter_provider) + with self.assertLogs( + "opentelemetry.exporter.otlp.proto.http.trace_exporter", + level="ERROR", + ) as log: + result = exporter.export([BASIC_SPAN]) + self.assertEqual(result, SpanExportResult.FAILURE) + self.assertIn("Failed to encode span batch", log.records[0].message) + @patch.object(Session, "post") def test_shutdown_interrupts_retry_backoff(self, mock_post): exporter = OTLPSpanExporter(timeout=1.5)