Skip to content

Demonstrate streaming span attributes in the streaming quickstart - #2691

Open
connectsudhindra-gif wants to merge 10 commits into
truera:mainfrom
connectsudhindra-gif:streaming-example-notebook
Open

Demonstrate streaming span attributes in the streaming quickstart#2691
connectsudhindra-gif wants to merge 10 commits into
truera:mainfrom
connectsudhindra-gif:streaming-example-notebook

Conversation

@connectsudhindra-gif

Copy link
Copy Markdown
Contributor

Summary

Part of #2442 (streaming LLM instrumentation), PR 3 of several. Stacked on #2690 (populates the streaming span attributes) and #2689 (adds them) -- the diff below includes both until they merge first; the last commit (Demonstrate streaming span attributes ...) is the new content here.

Extends the existing examples/quickstart/streaming_apps.ipynb (rather than adding a new notebook, since it already demonstrates exactly the right pattern -- an @instrument-decorated method consuming an OpenAI stream=True completion) with a section that queries recorded events for the new GENERATION span attributes (is_streaming, TTFT, tokens_per_second, chunks_received) and prints them.

Also fixes a comment that PR #2690 made stale: stream_options={"include_usage": True}, # not yet tracked by trulens -- that's no longer true, so it now explains what the option is for instead.

Test plan

  • Ran the notebook's actual code path end-to-end as a script against a local Ollama server (OpenAI-compatible endpoint, since I don't have an OpenAI API key handy) and confirmed all four attributes populate correctly:
    is_streaming: True
    time_to_first_token_ms: 2.44
    tokens_per_second: 22.3
    chunks_received: 4
    
  • Verified the notebook file is still valid JSON and the diff is otherwise minimal (no unrelated re-encoding of existing cells).

@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. documentation Improvements or additions to documentation labels Aug 14, 2026

@joshreini1 joshreini1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clean streaming instrumentation with comprehensive test coverage. Correctly captures TTFT, chunks, and throughput by wrapping the stream iterator. One blocker: test at line 108 is named test_chunks_received_omitted_without_usage but asserts chunks_received IS present—rename to test_tokens_per_second_omitted_without_usage. Two should-fix items: clarify TTFT docstring (measures stream-available-to-first-chunk, not request-issuance) and add minimum duration guard in tokens/sec calculation to avoid misleading metrics on very fast streams. Request changes: 1 blocker.

assert attrs[SpanAttributes.GENERATION.TOKENS_PER_SECOND] > 0


@pytest.mark.optional

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Rename to test_tokens_per_second_omitted_without_usage. The test actually verifies chunks_received IS recorded (asserts ==2) but tokens_per_second is not—the current name contradicts the assertion.

"""Whether this generation call used streaming (e.g. `stream=True`)."""

TIME_TO_FIRST_TOKEN_MS = base + ".time_to_first_token_ms"
"""Milliseconds between issuing the request and receiving the first

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Change docstring from "issuing the request" to "the streaming response being available" for accuracy. The implementation measures from when handle_response gets the stream object (after HTTP returns) to when next() yields the first chunk, not from initial request send.

"Could not record streaming span attributes; the span may "
"have already ended.",
exc_info=True,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Change duration_s > 0 to duration_s > 0.01 (or similar threshold). Prevents misleadingly high tokens/sec on very fast streams (e.g. single chunk completing in 0.0001s would show 100k tokens/sec). A 10ms minimum ensures the metric represents realistic throughput.

First step toward streaming instrumentation support (truera#2442). Adds the
attribute keys the issue asks for as a pure, additive semconv change
with no behavior change, so it can land and be reverted independently
of the instrumentation work that will populate them:

- ai.observability.generation.is_streaming
- ai.observability.generation.time_to_first_token_ms
- ai.observability.generation.tokens_per_second
- ai.observability.generation.chunks_received

Follow-up PRs will wire these up for the OpenAI provider's stream=True
path and add an example notebook; LangChain streaming, the generic
async-generator pattern, mid-stream guardrail evaluation, and the
dashboard latency view are each separate, larger efforts tracked
against truera#2442 but out of scope here.
@connectsudhindra-gif
connectsudhindra-gif force-pushed the streaming-example-notebook branch from ec1740a to 3092fb9 Compare August 18, 2026 02:33
= added 9 commits August 17, 2026 21:53
…ument scope of internal-only ones

Reviewer flagged 3 blockers: the custom ai.observability.generation.*
streaming attributes don't align with the OTel GenAI spec's canonical
gen_ai.request.stream (bool) / gen_ai.response.time_to_first_chunk
(double, seconds), and the spec models per-chunk throughput as
Histogram metrics rather than span attributes.

- Added GenAIAttributes.REQUEST.STREAM (gen_ai.request.stream) and a
  new GenAIAttributes.RESPONSE class with TIME_TO_FIRST_CHUNK
  (gen_ai.response.time_to_first_chunk), matching this file's existing
  pattern of emitting gen_ai.* attributes alongside the
  ai.observability.* ones for interoperability. These are the
  canonical counterparts to IS_STREAMING and TIME_TO_FIRST_TOKEN_MS.
- TOKENS_PER_SECOND and CHUNKS_RECEIVED have no canonical counterpart:
  the spec wants these as Histogram metrics
  (gen_ai.client.operation.time_per_output_chunk), and this codebase
  has no OTel Metrics/MeterProvider pipeline to emit them through yet.
  Documented them as TruLens-internal (ai.observability.* only) rather
  than silently leaving them looking like they're meant to be
  canonical semconv.

Still additive/no-behavior-change, consistent with this PR's original
scope -- nothing populates any of these attributes yet.
…uplicate them

Reviewer's inline comments (not just the summary) explicitly objected
to keeping ai.observability.generation.is_streaming and
.time_to_first_token_ms alongside the new canonical gen_ai.* ones:
"Duplicating it under the ai.observability.* namespace creates
confusion and breaks interoperability with OTel-native tooling."

Removed GENERATION.IS_STREAMING and GENERATION.TIME_TO_FIRST_TOKEN_MS
entirely. GenAIAttributes.REQUEST.STREAM and
GenAIAttributes.RESPONSE.TIME_TO_FIRST_CHUNK (from the previous
commit) are now the sole representations. This is a breaking rename
for the stacked instrumentation PRs (truera#2690/truera#2691/truera#2692), which are
being updated to match in the same pass.

TOKENS_PER_SECOND and CHUNKS_RECEIVED are intentionally left as-is
(TruLens-internal, documented as such) rather than dropped per the
reviewer's other comment -- replying on the review thread to explain
why, since dropping them removes already-shipped, tested
functionality rather than just renaming a key.
Second step toward streaming instrumentation support (truera#2442), built on
the semconv attributes from the previous PR. Populates the new
GENERATION span attributes for the OpenAI provider's sync stream=True
path via OpenAICostComputer.handle_response, the existing hook that
already runs on every instrumented openai.*.create() return value:

- IS_STREAMING / TIME_TO_FIRST_TOKEN_MS are known synchronously (the
  existing code already blocks on the first chunk to read its model
  name for cost attribution, so timing that gives TTFT for free).
- CHUNKS_RECEIVED / TOKENS_PER_SECOND require the full stream, which
  is only consumed later by the caller -- after the span-attribute
  hook has already returned. To handle this, the stream's iterator is
  wrapped to count chunks and, once exhausted, write those two
  attributes directly onto the span that was current when the request
  was issued (captured via `trace.get_current_span()`).
  TOKENS_PER_SECOND is only set when the caller requested
  `stream_options={"include_usage": True}`, since OpenAI doesn't send
  per-chunk token counts otherwise -- no token count is fabricated
  from chunk count.

This only works when the caller consumes the stream before the
enclosing instrumented span closes (the common pattern: an
instrumented method that iterates the stream to build its return
value). If the raw stream is instead handed off elsewhere uncontained
(e.g. returned directly for pass-through streaming), the span has
already ended by the time the stream finishes, and this degrades
gracefully to a no-op for the two deferred attributes -- verified by
test.

Does not touch the AsyncOpenAI client path, which goes through a
separate, ad-hoc instrumentation function in
experimental/otel_tracing/core/session.py rather than
OpenAICostComputer.handle_response; that's a separate follow-up.
- Rename test_chunks_received_omitted_without_usage to
  test_tokens_per_second_omitted_without_usage: the test asserts
  CHUNKS_RECEIVED is present and TOKENS_PER_SECOND is omitted, the
  opposite of what the old name said.
- Remove the AsyncStream branch in _instrument_stream_span_attributes.
  handle_response only reaches this helper via the sync `__iter__`
  check, so an openai.AsyncStream can never reach it; async streaming
  is explicitly out of scope for this PR and goes through a separate
  instrumentation path.
…tream/TTFT

SpanAttributes.GENERATION.IS_STREAMING and .TIME_TO_FIRST_TOKEN_MS no
longer exist (removed upstream per review feedback). Switch to
GenAIAttributes.REQUEST.STREAM (bool) and
GenAIAttributes.RESPONSE.TIME_TO_FIRST_CHUNK (seconds, not ms -- drop
the *1000 conversion). CHUNKS_RECEIVED/TOKENS_PER_SECOND are unaffected,
still TruLens-internal.
Third step toward streaming instrumentation support (truera#2442), built on
the previous two PRs (semconv attributes + OpenAI stream=True
instrumentation).

Extends the existing streaming_apps.ipynb (rather than adding a new
notebook, since this one already demonstrates the exact pattern:
an @instrument-decorated method that consumes an OpenAI stream=True
completion) with a section that queries the recorded events for the
new GENERATION span attributes -- is_streaming, TTFT,
tokens_per_second, chunks_received -- and prints them.

Also fixes a now-stale comment ("not yet tracked by trulens" on
stream_options={"include_usage": True}) that the previous PR made
inaccurate.

Verified by running the notebook's actual code path end-to-end against
a local Ollama server (OpenAI-compatible endpoint), confirming all
four attributes are populated correctly:
  is_streaming: True
  time_to_first_token_ms: 2.44
  tokens_per_second: 22.3
  chunks_received: 4
The basic unit test job doesn't install the openai extra, so the bare
`import openai` at module scope was blowing up collection for the
whole run before pytest could even see the @pytest.mark.optional
markers on the individual tests. Defer the import behind a
try/except + OPENAI_AVAILABLE flag and skip the module via pytestmark,
matching the existing convention in test_async_openai_capabilities.py.
_instrument_stream_span_attributes divided completion_tokens by
elapsed duration with only a `duration_s > 0` guard, which lets a
near-instant stream produce a misleadingly huge tokens/sec figure.
Added a _MIN_TOKENS_PER_SECOND_DURATION_S floor below which
TOKENS_PER_SECOND is simply not recorded.

Updated the affected test to fake the clock (real synchronous mock
consumption is faster than the new duration floor).

(The TTFT docstring clarification from the original version of this
commit is dropped: TIME_TO_FIRST_TOKEN_MS no longer exists -- it was
replaced by the canonical GenAIAttributes.RESPONSE.TIME_TO_FIRST_CHUNK
per truera#2689's review feedback.)
SpanAttributes.GENERATION.IS_STREAMING and .TIME_TO_FIRST_TOKEN_MS no
longer exist. Switch the notebook's streaming-metrics cell to
GenAIAttributes.REQUEST.STREAM / .RESPONSE.TIME_TO_FIRST_CHUNK.
@connectsudhindra-gif
connectsudhindra-gif force-pushed the streaming-example-notebook branch from 3092fb9 to 31d912d Compare August 18, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants