From 704b5090da3903f717abc24b6f4ba72e861975c3 Mon Sep 17 00:00:00 2001 From: Yesudeep Mangalapilly Date: Tue, 10 Feb 2026 12:15:42 -0800 Subject: [PATCH] fix(py/core): guard RealtimeSpanProcessor.export() against ConnectionError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rationale: The RealtimeSpanProcessor exports spans on both start and end for real-time trace visualization. When the OTLP collector (e.g. Jaeger) is unreachable, the exporter raises ConnectionError. This exception was propagating through the OTel gRPC interceptor and crashing actual RPCs like gRPC reflection — preventing tools like grpcui from connecting. Changes: - Separate ConnectionError (expected, DEBUG) from unexpected exceptions (WARNING) in both on_start() and on_end(). - Switch from stdlib logging to the project get_logger() wrapper (structlog) to match the codebase convention. - Fix incorrect noqa: S110 (that rule is for try/except/pass blocks). --- .../genkit/core/trace/realtime_processor.py | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/py/packages/genkit/src/genkit/core/trace/realtime_processor.py b/py/packages/genkit/src/genkit/core/trace/realtime_processor.py index 30cc6b52dc..c8fcf00ea4 100644 --- a/py/packages/genkit/src/genkit/core/trace/realtime_processor.py +++ b/py/packages/genkit/src/genkit/core/trace/realtime_processor.py @@ -75,6 +75,9 @@ from opentelemetry.sdk.trace.export import SpanExporter from genkit.core._compat import override +from genkit.core.logging import get_logger + +logger = get_logger(__name__) class RealtimeSpanProcessor(SpanProcessor): @@ -119,9 +122,23 @@ def on_start(self, span: Span, parent_context: Context | None = None) -> None: span: The span that was just started. parent_context: The parent context (unused). """ - # Export the span immediately (it won't have endTime yet) - # We ignore the result - we don't want to block span creation - _ = self._exporter.export([span]) + # Export the span immediately (it won't have endTime yet). + # Catch all exceptions so a failing exporter (e.g. Jaeger not + # running) never propagates into the application call stack. + # Without this guard, a ConnectionError here bubbles through + # the OTel gRPC server interceptor and kills the actual RPC. + try: + self._exporter.export([span]) + except ConnectionError: + logger.debug( + 'RealtimeSpanProcessor: export failed on_start (collector unreachable)', + exc_info=True, + ) + except Exception: # noqa: BLE001 — must never crash the caller + logger.warning( + 'RealtimeSpanProcessor: unexpected error during export on_start', + exc_info=True, + ) @override def on_end(self, span: ReadableSpan) -> None: @@ -132,8 +149,18 @@ def on_end(self, span: ReadableSpan) -> None: Args: span: The span that just ended. """ - # Export the completed span - _ = self._exporter.export([span]) + try: + self._exporter.export([span]) + except ConnectionError: + logger.debug( + 'RealtimeSpanProcessor: export failed on_end (collector unreachable)', + exc_info=True, + ) + except Exception: # noqa: BLE001 — must never crash the caller + logger.warning( + 'RealtimeSpanProcessor: unexpected error during export on_end', + exc_info=True, + ) @override def force_flush(self, timeout_millis: int = 30000) -> bool: