Repository navigation
Conversation
Signed-off-by: SOUMYAJIT GHOSH <23051387@kiit.ac.in>
Pull request dashboard statusWaiting on the author · refreshed 2026-10-20 04:41 UTC Respond to 3 review items (e.g. link a commit, explain why not, ask a follow-up): Status above doesn't look right?
|
chrikrah
left a comment
There was a problem hiding this comment.
I read this against the issue and ran the repro on main first, so the notes below are from the code
rather than from the description. The shape looks right to me: guarding every callback rather than
only on_end covers the case the issue's own suggested diff misses, where a ConcurrentMultiSpanProcessor
is installed at the top level and the exception escapes from future.result().
Two things I could not find a test for, both behaviour this pull request changes:
force_flush. A processor that raises now setsall_flushed = Falseand the method returnsFalse
instead of propagating. The other four callbacks each got a test; this one did not, and it is the
only one of the five with a return value a caller acts on.- The submit side of
_submit_and_await. Ifself._executor.submitraises, the processor is dropped
fromfuturesand the call continues. That is reachable at interpreter exit, where
concurrent.futures.threadshuts its executor down in its ownatexithook before
TracerProvider.shutdownruns, andsubmitthen raisesRuntimeError: cannot schedule new futures after shutdown.
One question rather than a request. force_flush reports failure through its return value, but
shutdown() returns nothing, so a processor that fails to shut down is now invisible to the caller.
Given the issue is about failures that happen quietly, is that asymmetry deliberate?
One thing worth stating because a reviewer might otherwise ask: the new tests sit on
MultiSpanProcessorTestBase, so both TestSynchronousMultiSpanProcessor and
TestConcurrentMultiSpanProcessor inherit them. That matters here, because the two classes fail
differently before the fix. The synchronous loop stops at the first raiser, so later processors never
run, while the concurrent one submits every future before awaiting any and only loses the results
after the first failure.
|
Thank you @chrikrah for the detailed and thorough review! I have addressed each of the points in commit
|
|
Both land. Checked out The one that fails is
|
8c3371e to
d4f28a6
Compare
Signed-off-by: SOUMYAJIT GHOSH <23051387@kiit.ac.in>
Signed-off-by: SOUMYAJIT GHOSH <23051387@kiit.ac.in>
d4f28a6 to
2c8cd34
Compare
|
Gentle check-in on this PR — reviewer @chrikrah verified that the tests in commit |
|
Some real-world impact data in support of this fix, from testing GenAI instrumentations against a span processor that raises: from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
class BuggyProcessor(SpanProcessor):
def on_start(self, span, parent_context=None):
raise RuntimeError("PROCESSOR_FAULT") # or raise in on_end
provider = TracerProvider()
provider.add_span_processor(BuggyProcessor())
# then instrument the OpenAI SDK with this provider and make a chat completion callResult for an ordinary
So with the current SDK, a bug in any single span processor turns every traced LLM call into a failure for instrumentations that rely on the SDK not throwing, as the spec allows them to. Catching processor exceptions in |
|
Thank you @armanunix for providing this concrete real-world impact data. This directly demonstrates why the SDK MultiSpanProcessor must catch and log unhandled exceptions during on_start and on_end, as permitted by the specification, rather than allowing processor faults to crash caller workflows in GenAI and HTTP instrumentations. The implementation and unit tests in this PR are verified and ready for maintainer review. |
There was a problem hiding this comment.
🟡 Changes recommended
Concurrent force-flush failures can remain unlogged when another processor times out.
1 open finding
What changed in this PR
Improves MultiSpanProcessor resilience by containing and logging processor failures.
Changes:
- Guards processor callbacks and executor operations.
- Adds regression tests for exception handling.
- Adds a changelog entry.
| File | Description |
|---|---|
opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py |
Handles processor and executor exceptions. |
opentelemetry-sdk/tests/trace/test_span_processor.py |
Tests failure containment. |
.changelog/5626.fixed |
Documents the fix. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if not_done_futures: | ||
| return False | ||
|
|
||
| for future in done_futures: |
There was a problem hiding this comment.
Resolved in commit f186a47: removed early return on non-empty not_done_futures, marked all_flushed = False, and continue processing done_futures so completed futures are inspected and any exceptions are logged. Added regression test test_force_flush_timeout_still_inspects_completed_failures.
| try: | ||
| future = self._executor.submit(sp.force_flush, timeout_millis) | ||
| futures.append(future) | ||
| except Exception: # pylint: disable=broad-exception-caught |
There was a problem hiding this comment.
The exception potentially raised from the span processor won't be raised here, it'll be raised on the result below. Likewise the call to submit should never raise, unless the executor is shutdown which indicates a bug in the implementation.
There was a problem hiding this comment.
Agreed @herin049. The span processor exception is raised on future.result() in the result evaluation loop below, not at submit(). The try/except around submit() was originally added to guard against executor shutdown at interpreter exit (RuntimeError: cannot schedule new futures after shutdown). Rebased cleanly on upstream main, updated force_flush to inspect completed futures on timeout, and all 48 unit tests pass cleanly.
| future = self._executor.submit(func(sp), *args, **kwargs) | ||
| futures.append(future) | ||
| try: | ||
| future = self._executor.submit(func(sp), *args, **kwargs) |
Fixes open-telemetry#5624 Per the OpenTelemetry specification: 1. OnEnd(Span) and OnStart(Span) MUST NOT throw an exception. 2. OpenTelemetry implementations MUST NOT throw unhandled exceptions at runtime. Previously, SynchronousMultiSpanProcessor and ConcurrentMultiSpanProcessor did not catch exceptions raised by underlying SpanProcessor instances during on_start, _on_ending, on_end, shutdown, and force_flush. An unhandled exception in on_end during context manager exit (__exit__) would escape and replace or suppress real application exceptions, or crash the application during normal execution. Additionally, a failure in an earlier processor would prevent subsequent processors from being invoked. Wrap underlying processor calls in try-except blocks, logging exceptions with logger.exception so that application execution is never disrupted and all registered processors are reliably called. Signed-off-by: Soumyajit Ghosh <jobsoumyajit6124@gmail.com> Signed-off-by: SOUMYAJIT GHOSH <23051387@kiit.ac.in>
Signed-off-by: SOUMYAJIT GHOSH <23051387@kiit.ac.in>
…ilure - Cover force_flush exception handling in MultiSpanProcessorTestBase ensuring it returns False without propagating the exception. - Cover ConcurrentMultiSpanProcessor submission failure in _submit_and_await and force_flush when ThreadPoolExecutor is shut down. - Set all_flushed = False in ConcurrentMultiSpanProcessor.force_flush if submit raises. Signed-off-by: Soumyajit Ghosh <jobsoumyajit6124@gmail.com>
Signed-off-by: Soumyajit Ghosh <jobsoumyajit6124@gmail.com>
2c8cd34 to
f186a47
Compare
chrikrah
left a comment
There was a problem hiding this comment.
@somuai, the Copilot timeout fix holds at f186a47. On @herin049's submit point: the exit path is already tracked in #5568 and #4461, and its inline fallback was #5569, which closed stale on 2026-10-02. Commenting rather than approving, because the guard here decides what that fallback can still do.
$ python -m pytest tests/trace/test_span_processor.py -q -p no:cacheprovider # opentelemetry-sdk at f186a47, Python 3.12.6, import path asserted
48 passed
# sdk/trace/__init__.py from a517a73, tests kept
1 failed, 47 passed # test_force_flush_timeout_still_inspects_completed_failures
# sdk/trace/__init__.py from merge base 2fa016b
12 failed, 36 passed
$ python -I exit_probe.py # ConcurrentMultiSpanProcessor wrapping one processor, TracerProvider(shutdown_on_exit=True), one span, then exit
# base 2fa016b
Exception ignored in atexit callback: <bound method TracerProvider.shutdown ...>
RuntimeError: cannot schedule new futures after shutdown
# head f186a47
on_end ran
Exception while submitting task to SpanProcessor executor.
RuntimeError: cannot schedule new futures after shutdown
# head, except branch calls func(sp)(*args, **kwargs) inside its own try/except that logs
on_end ran
inner shutdown ran
$ python raise_probe.py # one processor whose on_end raises ValueError, executor shut down, one span ended
# head f186a47
caller survived
# head, bare inline call in the except branch (as in #5569)
ESCAPED to caller: ValueError processor bug
# head, inline call inside its own try/except
caller survived
non-blocking: once this merges, the except RuntimeError that #5569 relied on never fires, so a separate exit fix has to rework this guard anyway. A bare inline call would also bring #5624 back. Under its own guard it runs the inner shutdown() and the caller survives. The cost to you is one branch and one test more. I'd still fold it in here.
@herin049, would you take the guarded fallback in this PR, or keep it to logging and settle the direction on #5568, where Pr1meGG asked on 2026-10-08?

Description
Fixes #5624
Per the OpenTelemetry specification:
Span.End()API, therefore it should not block or throw an exception."Tracer.StartSpanAPI, therefore it should not block or throw an exception."Previously,
SynchronousMultiSpanProcessorandConcurrentMultiSpanProcessordid not catch exceptions raised by underlyingSpanProcessorinstances duringon_start,_on_ending,on_end,shutdown, andforce_flush.When a span processor raised in
on_endduring context manager exit (Span.__exit__), the exception escaped and replaced or suppressed the application's actual exception. Furthermore, a failure in an earlier processor in the list prevented subsequent processors from being called.This change wraps calls to underlying processors in
try...except Exception:blocks and logs failures vialogger.exception(...), ensuring that:Type of change
How Has This Been Tested?
test_on_end_exception_does_not_raisetoMultiSpanProcessorTestBase(runs against both Synchronous and Concurrent processors).test_on_start_exception_does_not_raisetoMultiSpanProcessorTestBase.test_on_ending_exception_does_not_raisetoMultiSpanProcessorTestBase.test_shutdown_exception_does_not_raisetoMultiSpanProcessorTestBase.test_on_end_exception_does_not_replace_application_exceptionverifying that application exceptions insidewith tracer.start_as_current_span(...)are properly propagated and never replaced.opentelemetry-sdk/tests/trace/test_span_processor.pypass.ruff checkpasses with 0 errors.Does This PR Require a Contrib Repo Change?
Checklist: