[Layer 2] Align ai_observability OTel emission to GenAI semantic conventions (gen_ai.*)
## Problem `ai_observability` has emitted OTel spans + metrics (since the 1.3.x series) with ad-hoc names (`provider`, `operation_type`, `model`, `token_usage` as a serialized array; counters `ai_token_usage_<key>`) rather than the OTel [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). The Layer 2 envelope tracker (#3586453) needs one attribute schema across modules so the translator reads a single shape. ## Proposed approach — additive, dual-emit, no config toggle Emit `gen_ai.*` **alongside** the existing ad-hoc attributes/metrics, unconditionally, when OTel export is enabled. Ad-hoc emission stays (documented legacy; removal targeted 2.0.x). (`gen_ai.*` are Experimental in the spec.) **Spans** (`AiOtelSpansEventSubscriber`): | ad-hoc | gen_ai.* | source | note | |---|---|---|---| | `provider` | `gen_ai.provider.name` | `getProviderId()` **via mapper** | → well-known value (`openai`/`anthropic`/`gcp.vertex_ai`/`gcp.gemini`/`aws.bedrock`…); fall back to raw id | | `operation_type` | `gen_ai.operation.name` | `getOperationType()` **via mapper** | emit only well-known values (`chat`/`text_completion`/`embeddings`); omit non-standard (e.g. `text_to_image`) | | `model` | `gen_ai.request.model` | `getModelId()` | | | `token_usage[input]` | `gen_ai.usage.input_tokens` | `getTokenUsage()->input` | scalar; **preserve explicit 0** (no `array_filter`) | | `token_usage[output]` | `gen_ai.usage.output_tokens` | `->output` | scalar; preserve 0 | | — (opt) | `gen_ai.response.model` | `getRawOutput()['model']` when present | best-effort | | new | `gen_ai.response.finish_reasons` (array) | non-stream: `getRawOutput()` (OpenAI `finish_reason` / Anthropic `stop_reason`); streaming: preserved iterator finish reason | **depends on #3586473** | **Metrics** (`AiOtelMetricsEventSubscriber`): emit histogram `gen_ai.client.token.usage` with `gen_ai.token.type` (`input`/`output` only) + `gen_ai.provider.name` / `gen_ai.request.model` dimensions, alongside the legacy counters. ## Out of scope (follow-up — do not expand this issue) - Prompt/completion **content** capture — needs a research spike. `gen_ai.prompt`/ `gen_ai.completion` were removed; the current semconv replaces them with opt-in structured attributes `gen_ai.input.messages` / `gen_ai.output.messages` (+ `gen_ai.system_instructions`), all Opt-In and still Development/unsettled. - **2.0.x:** span rename to `"{gen_ai.operation.name} {gen_ai.request.model}"` + removal of legacy ad-hoc attrs/counters (breaking; dashboard migration). ## Definition of done - `gen_ai.*` span attrs emitted alongside ad-hoc, unconditionally. - Provider + operation mappers with documented fallback. - Scalar usage attrs preserve `0`. - `gen_ai.response.finish_reasons` (array) for non-stream + stream (latter after the bug issue). - `gen_ai.client.token.usage` histogram (input/output) alongside legacy counters. - Unit tests extending `AiOtelSpansEventSubscriberTest` + `AiOtelMetricsEventSubscriberTest` (both attribute sets + streaming order). - Docs: sample `gen_ai.*` span dump; ad-hoc documented legacy (removal 2.0.x). ## Related - Umbrella #3586445 · envelope tracker #3586453 · `ai_eval` OTel input #3591188 (consumes these spans) · **depends on** #3586473 (streaming-span finalization) - OTel GenAI semconv: https://opentelemetry.io/docs/specs/semconv/gen-ai/ --- AI-Generated: Yes (Claude Code; mapping verified against `drupal/ai` 1.4.0 + current OTel GenAI semconv; scope hardened via adversarial review.)
issue