Skip to content

util-genai: record cancellation as a failure, not a success - #520

Merged
lmolkova merged 1 commit into
open-telemetry:mainfrom
ordemri:fix/util-genai-cancellation
Sep 4, 2026
Merged

util-genai: record cancellation as a failure, not a success#520
lmolkova merged 1 commit into
open-telemetry:mainfrom
ordemri:fix/util-genai-cancellation

Conversation

@ordemri

@ordemri ordemri commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Description

GenAIInvocation.__exit__ called fail() only when the in-flight exception derived from
Exception, so asyncio.CancelledError took the stop() branch: a cancelled operation was exported
with StatusCode.UNSET, no error.type, and a duration metric with no error dimension —
indistinguishable from one that completed. This guards on BaseException instead, matching the guard
the stream wrappers in stream.py already use.

fail() already accepts a BaseException, so nothing else changed. __exit__ still returns None
— suppressing CancelledError would break asyncio cancellation for every caller — and two tests
cover that, one of them cancelling a real task mid-await.

Known gaps

  • The wider guard also catches KeyboardInterrupt, SystemExit and GeneratorExit. GeneratorExit
    is not reachable from any shipped instrumentation today — no with <invocation>: block in this
    repository contains a yield — but it is covered by a test. Happy to narrow this; it is one line.
  • This fixes the context-manager path only. Packages that finalize in their own except Exception:
    handler are worse off than unfixed, not equal: on cancellation neither branch runs and the span
    is never ended, so nothing is exported at all. Happy to follow up separately.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How has this been tested?

New tests in util/opentelemetry-util-genai/tests/test_invocation_base_exception.py, written before
the fix and confirmed red against unmodified main: 6 failed, 342 passed — five with
AssertionError: <StatusCode.UNSET: 0> != <StatusCode.ERROR: 2>, one with KeyError: 'error.type'.
The file's other tests passed before and after, so it discriminates rather than failing wholesale.

  • tox -e py310-test-util-genai — 348 passed
  • tox -e py314-test-util-genai — 348 passed
  • tox -e py310-test-instrumentation-genai-agno-oldest — 20 passed
  • tox -e py310-test-instrumentation-genai-agno-latest — 20 passed
  • tox -e py314-test-instrumentation-genai-agno-latest — 20 passed
  • google-genai and smolagents oldest / latest — passed
  • Conformance environments not verified: they skip locally because the weaver binary is not
    installed, so no assertion ran. Relying on CI for those.

Checklist

  • Followed the style guidelines of this project
  • Changelog updated
  • Unit tests added
  • Documentation updated — not applicable

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes util-genai’s invocation context-manager finalization so cancellations and other BaseException cases are recorded as failures (via fail()), preventing cancelled operations from being exported indistinguishably from successful ones.

Changes:

  • Update GenAIInvocation.__exit__ to treat any non-None exc_value: BaseException as failure, aligning behavior with existing stream wrapper guards.
  • Add new unit tests covering CancelledError, KeyboardInterrupt, SystemExit, GeneratorExit, and ensuring cancellation is not suppressed.
  • Add a towncrier changelog fragment describing the bug fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py Treat BaseException in __exit__ as failure to correctly mark cancellation/interruption as errors.
util/opentelemetry-util-genai/tests/test_invocation_base_exception.py New tests verifying __exit__ failure semantics for BaseException and that cancellation still propagates.
util/opentelemetry-util-genai/.changelog/PRNUMBER.fixed Changelog fragment documenting the cancellation-as-failure fix.
Suppressed comments (2)

util/opentelemetry-util-genai/tests/test_invocation_base_exception.py:162

  • test_cancellation_of_a_task_still_cancels_it can hang for up to 60s (or indefinitely on started.wait()) if cancellation doesn’t propagate as expected. Adding a short asyncio.wait_for timeout makes the test fail fast instead of stalling the suite.
            task = asyncio.ensure_future(work())
            await started.wait()
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task

util/opentelemetry-util-genai/tests/test_invocation_base_exception.py:119

  • This assertion hardcodes the CancelledError module path (asyncio.exceptions...), which can change between Python versions. Compute the expected error.type using fq_exception_type to avoid version-specific failures.
        self.assertEqual(
            span.attributes["error.type"], "asyncio.exceptions.CancelledError"
        )

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread util/opentelemetry-util-genai/tests/test_invocation_base_exception.py Outdated
Comment thread util/opentelemetry-util-genai/.changelog/520.fixed
@ordemri
ordemri force-pushed the fix/util-genai-cancellation branch from 02d832e to 009e789 Compare September 1, 2026 08:17
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 1, 2026

Copy link
Copy Markdown

Pull request dashboard status

Merged · refreshed 2026-09-04 22:19 UTC

Status above doesn't look right?
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@lmolkova lmolkova left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Comment thread util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py Outdated
`GenAIInvocation.__exit__` finalized the invocation through `fail()` only
when the in-flight exception derived from `Exception`. `asyncio.CancelledError`,
`KeyboardInterrupt`, `SystemExit` and `GeneratorExit` derive from
`BaseException` but not from `Exception`, so they took the `stop()` branch: a
cancelled operation was exported with `StatusCode.UNSET`, no `error.type`, and
a duration metric carrying no error dimension.

Guard on `BaseException` instead, matching the guard the stream wrappers in
`stream.py` already use (`if exc_val is not None:`).

`fail()` already accepts a `BaseException` and routes it through
`Error.from_exception`, so no other change is needed; `error.type` resolves via
`fq_exception_type` and stays consistent with the exception event.

`__exit__` still returns `None`, so nothing is suppressed: suppressing
`CancelledError` would break asyncio cancellation for every caller. Tests cover
that explicitly, including a real cancelled task that must remain cancelled.

Assisted-by: Claude Opus 5
@ordemri
ordemri force-pushed the fix/util-genai-cancellation branch from 009e789 to 39fbbab Compare September 2, 2026 11:23
@lmolkova
lmolkova added this pull request to the merge queue Sep 4, 2026
Merged via the queue into open-telemetry:main with commit 59e6efe Sep 4, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants