Migrate the LLM client + streaming spans to tracing #140

Closed
opened 2026-09-29 14:57:10 +00:00 by cameron · 1 comment
Owner

Part of #135

Work

Convert the src/ai/* raw-OTel spans to tracing, drop the cx: &Context first param (36 sites: openrouter.rs 6, ollama.rs 18, llm_client.rs 5, llamacpp.rs 6), and carry the parent span across the streaming spawn boundaries.

  • Replace start_request_span / manual set_attribute with #[instrument] + tracing fields.
  • Drop cx: &Context from the LlmClient trait methods and every call site.
  • At each streaming tokio::spawn boundary (critical: insight_chat::chat_turn_stream_inner), carry the parent span (.instrument(parent) or span.enter()).
  • Keep llm.model / llm.backend / traceparent injection behavior identical in the exported spans.

Resolution

Confirmation the LLM/streaming paths emit equivalent spans with no context params and the streaming trace is no longer orphaned.

Part of #135 ## Work Convert the `src/ai/*` raw-OTel spans to `tracing`, drop the `cx: &Context` first param (36 sites: `openrouter.rs` 6, `ollama.rs` 18, `llm_client.rs` 5, `llamacpp.rs` 6), and carry the parent span across the streaming spawn boundaries. - Replace `start_request_span` / manual `set_attribute` with `#[instrument]` + `tracing` fields. - Drop `cx: &Context` from the `LlmClient` trait methods and every call site. - At each streaming `tokio::spawn` boundary (critical: `insight_chat::chat_turn_stream_inner`), carry the parent span (`.instrument(parent)` or `span.enter()`). - Keep `llm.model` / `llm.backend` / traceparent injection behavior identical in the exported spans. ## Resolution Confirmation the LLM/streaming paths emit equivalent spans with no context params and the streaming trace is no longer orphaned.
cameron added a new dependency 2026-09-29 14:57:54 +00:00
cameron added a new dependency 2026-09-29 14:57:56 +00:00
cameron self-assigned this 2026-09-29 18:03:43 +00:00
Author
Owner

Resolved on feature/tracing-migration (commit ea1d425).

What was done

  • LLM layer has no context params. cx: &Context is gone from the LlmClient trait, all three providers (openrouter / llamacpp / ollama, including Ollama's inherent generate* / generate_embedding(s) methods) and every call site. otel::start_llm_request_span(provider, operation, model) now returns a tracing::Span (llm.request, otel.kind = "client", llm.provider / llm.operation / llm.model) that parents on the caller's ambient span. Token counts, cost, http.status_code and status go through OpenTelemetrySpanExt::set_attribute / set_status, so the attribute set is unchanged. with_traceparent injects the llm.request span's own context.
  • Feature spans in src/ai/* are on tracing. All 30 raw-OTel span sites (AI handlers, TTS handlers, insight generate / agentic, RAG + context gatherers, chat turn, persona chat, daily summaries) are now #[tracing::instrument(name = ..., skip_all)] or info_span!. For the mid-function loop spans (ai.chat.loop, ai.agentic.loop), each LLM and tool await is wrapped in .instrument(loop_span.clone()). parent_cx params that only carried the parent are dropped.
  • Spawn boundaries carry the parent.
    • chat_turn_stream creates ai.chat.turn.stream eagerly inside the request span, and chat_turn_stream_inner spawns with .instrument(span).
    • ai.chat.turn.execute and ai.persona_chat.turn.execute are created before their spawns, so they now land in the dispatching request's trace instead of a fresh root.
    • The background insight jobs (generate and agentic) and the TTS job spawn with .instrument(Span::current()).
  • DAO calls are unchanged in shape. They still take &Context, now derived from the tracing span via span.context(), so they nest correctly. Removing those is #141's job.

Verification

  • New tests/otel_llm_spans.rs uses a mock OpenAI-compatible server on a multi-thread runtime. Blocking and streaming llm.request spans, called inside a tokio::spawn(...instrument(turn)), share the turn's trace_id and parent on it. Kind=Client, provider/operation/model/http.status_code all match, and the outgoing traceparent names the llm.request span. Mutation check: removing the .instrument fails the test (orphaned trace).
  • cargo clippy --all-targets: 0 warnings. Full suite green: 690 lib + 775 integration + 3 OTel regression tests + doc-tests.

Behaviour changes for #143 to check against the baseline

  • The 202 agentic insight job, async chat turns and the streaming chat turn are now one trace with their request (the baseline had them separate or orphaned).
  • Spans are now subject to EnvFilter. With RUST_LOG=warn the info-level spans (root, feature and llm.request) are disabled, so they aren't exported and no traceparent is sent. Prod runs at info, but see the new fog item.
  • tracing-opentelemetry adds its default code.* / thread.* attributes. The required attributes are unchanged.
Resolved on `feature/tracing-migration` (commit ea1d425). **What was done** - **LLM layer has no context params.** `cx: &Context` is gone from the `LlmClient` trait, all three providers (openrouter / llamacpp / ollama, including Ollama's inherent `generate*` / `generate_embedding(s)` methods) and every call site. `otel::start_llm_request_span(provider, operation, model)` now returns a `tracing::Span` (`llm.request`, `otel.kind = "client"`, `llm.provider` / `llm.operation` / `llm.model`) that parents on the caller's ambient span. Token counts, cost, `http.status_code` and status go through `OpenTelemetrySpanExt::set_attribute` / `set_status`, so the attribute set is unchanged. `with_traceparent` injects the `llm.request` span's own context. - **Feature spans in `src/ai/*` are on tracing.** All 30 raw-OTel span sites (AI handlers, TTS handlers, insight generate / agentic, RAG + context gatherers, chat turn, persona chat, daily summaries) are now `#[tracing::instrument(name = ..., skip_all)]` or `info_span!`. For the mid-function loop spans (`ai.chat.loop`, `ai.agentic.loop`), each LLM and tool await is wrapped in `.instrument(loop_span.clone())`. `parent_cx` params that only carried the parent are dropped. - **Spawn boundaries carry the parent.** - `chat_turn_stream` creates `ai.chat.turn.stream` eagerly inside the request span, and `chat_turn_stream_inner` spawns with `.instrument(span)`. - `ai.chat.turn.execute` and `ai.persona_chat.turn.execute` are created before their spawns, so they now land in the dispatching request's trace instead of a fresh root. - The background insight jobs (generate and agentic) and the TTS job spawn with `.instrument(Span::current())`. - **DAO calls are unchanged in shape.** They still take `&Context`, now derived from the tracing span via `span.context()`, so they nest correctly. Removing those is #141's job. **Verification** - New `tests/otel_llm_spans.rs` uses a mock OpenAI-compatible server on a multi-thread runtime. Blocking and streaming `llm.request` spans, called inside a `tokio::spawn(...instrument(turn))`, share the turn's trace_id and parent on it. Kind=Client, provider/operation/model/http.status_code all match, and the outgoing `traceparent` names the `llm.request` span. Mutation check: removing the `.instrument` fails the test (orphaned trace). - `cargo clippy --all-targets`: 0 warnings. Full suite green: 690 lib + 775 integration + 3 OTel regression tests + doc-tests. **Behaviour changes for #143 to check against the baseline** - The 202 agentic insight job, async chat turns and the streaming chat turn are **now one trace** with their request (the baseline had them separate or orphaned). - Spans are now subject to `EnvFilter`. With `RUST_LOG=warn` the info-level spans (root, feature and `llm.request`) are disabled, so they aren't exported and no traceparent is sent. Prod runs at `info`, but see the new fog item. - tracing-opentelemetry adds its default `code.*` / `thread.*` attributes. The required attributes are unchanged.
cameron added the wayfinder:task label 2026-09-30 00:47:28 +00:00
Sign in to join this conversation.