fix: #3586473 Finalize streaming OTel spans on the terminal event

Summary

Fixes the streaming OTel span token-usage loss in #3586473.

AiOtelSpansEventSubscriber routes both PostGenerateResponseEvent and PostStreamingResponseEvent to one handler that ended the span on the first event. For streaming, the first event carries the un-consumed iterator (empty token usage); the terminal PostStreamingResponseEvent carries the reconstructed output with the real usage, but the span was already ended, so the final token usage was dropped.

Changes

  • Defer finalisation to the terminal PostStreamingResponseEvent for streaming responses; unset() the stored span after ending it (also fixes unbounded $otelSpans growth).
  • Leak guard: implement DestructableInterface (service tagged needs_destruction) so spans left open by iterators that never dispatch the terminal event (ReplayedChatMessageIterator, AssistantStreamIterator) are ended at request shutdown instead of leaking.
  • Tests: testStreamingSpanRecordsFinalTokenUsage (the bug repro) and testStreamingSpanFinalizedOnDestructWithoutTerminalEvent (the leak guard).

Out of scope (separate concerns tracked on the issue)

Metrics subscriber behaviour, the empty operationType / configuration on the terminal streaming event, and finish-reason preservation.

Verification

Validated locally against drupal/ai 1.4.0 (PHP 8.4, PHPUnit 11.5, real OpenTelemetry SDK in-memory exporter): full ai_observability Unit suite passes (17/17), phpstan clean, phpcs clean.

Related: #3586473


AI-Generated: Yes (Used Claude Code to verify the bug against drupal/ai 1.4.0, reproduce it with a real OpenTelemetry SDK, implement the fix + leak guard, and validate the suite in DDEV.)

Edited by George Kastanis

Merge request reports

Loading