diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2b74a3203..4cb58b0d3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4402](https://github.com/open-telemetry/opentelemetry-python/pull/4402)) - Tolerates exceptions when loading resource detectors via `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` ([#4373](https://github.com/open-telemetry/opentelemetry-python/pull/4373)) +- Disconnect gRPC client stub when shutting down `OTLPSpanExporter` + ([#4370](https://github.com/open-telemetry/opentelemetry-python/pull/4370)) - opentelemetry-sdk: fix OTLP exporting of Histograms with explicit buckets advisory ([#4434](https://github.com/open-telemetry/opentelemetry-python/pull/4434)) - opentelemetry-exporter-otlp-proto-grpc: better dependency version range for Python 3.13 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 582d083e86f..4be75c5335e 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 @@ -243,8 +243,8 @@ def __init__( ) or Compression.NoCompression if insecure: - self._client = self._stub( - insecure_channel(self._endpoint, compression=compression) + self._channel = insecure_channel( + self._endpoint, compression=compression ) else: credentials = _get_credentials( @@ -253,11 +253,10 @@ def __init__( OTEL_EXPORTER_OTLP_CLIENT_KEY, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, ) - self._client = self._stub( - secure_channel( - self._endpoint, credentials, compression=compression - ) + self._channel = secure_channel( + self._endpoint, credentials, compression=compression ) + self._client = self._stub(self._channel) self._export_lock = threading.Lock() self._shutdown = False @@ -360,6 +359,7 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: # wait for the last export if any self._export_lock.acquire(timeout=timeout_millis / 1e3) self._shutdown = True + self._channel.close() self._export_lock.release() @property diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py index 1d2bae2486d..9cd7ac38358 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_metrics_exporter.py @@ -874,6 +874,21 @@ def test_shutdown_wait_last_export(self): finally: export_thread.join() + def test_export_over_closed_grpc_channel(self): + # pylint: disable=protected-access + + add_MetricsServiceServicer_to_server( + MetricsServiceServicerSUCCESS(), self.server + ) + self.exporter.export(self.metrics["sum_int"]) + self.exporter.shutdown() + data = self.exporter._translate_data(self.metrics["sum_int"]) + with self.assertRaises(ValueError) as err: + self.exporter._client.Export(request=data) + self.assertEqual( + str(err.exception), "Cannot invoke RPC on closed channel!" + ) + def test_aggregation_temporality(self): # pylint: disable=protected-access diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py index fe0b94ac787..f29b7fc611c 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py @@ -1017,6 +1017,21 @@ def test_shutdown_wait_last_export(self): finally: export_thread.join() + def test_export_over_closed_grpc_channel(self): + # pylint: disable=protected-access + + add_TraceServiceServicer_to_server( + TraceServiceServicerSUCCESS(), self.server + ) + self.exporter.export([self.span]) + self.exporter.shutdown() + data = self.exporter._translate_data([self.span]) + with self.assertRaises(ValueError) as err: + self.exporter._client.Export(request=data) + self.assertEqual( + str(err.exception), "Cannot invoke RPC on closed channel!" + ) + def _create_span_with_status(status: SDKStatus): span = _Span(