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
PostStreamingResponseEventfor streaming responses;unset()the stored span after ending it (also fixes unbounded$otelSpansgrowth). - Leak guard: implement
DestructableInterface(service taggedneeds_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) andtestStreamingSpanFinalizedOnDestructWithoutTerminalEvent(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.)