[REG-12] W0.7 - LLM span conventions - model, tokens, cost, latency - #170
Merged
Conversation
Every LLM completion now produces a span carrying enough metadata to answer which prompt produced a bad answer, what an incident's AI usage cost, and why p99 latency was bad last week. - New internal/integrations/llm/instrumented.go: instrumentedClient wraps any provider Client with a span per Complete() call, using OTel's GenAI semantic conventions (gen_ai.system, gen_ai.request.model, gen_ai.operation.name, gen_ai.usage.input_tokens/output_tokens, gen_ai.agent.name). Pinned to semconv v1.34.0 specifically - the last version in this module's vendored set that still carries gen_ai.system under that name (later revisions rename it to gen_ai.provider.name; GenAI semconv is marked "Development" stability upstream and churns between versions). - New internal/integrations/llm/pricing.go: a small per-model USD price table for tagging estimated cost directly on the span. Independent of Pro's CostTracker.RecordUsage (different repo, no shared code path) but fed the same Model/PromptTokens/CompletionTokens, so the two agree numerically without sharing implementation. - New internal/integrations/llm/callmeta.go: CallMeta carries AgentName and IncidentID through context rather than as Complete() parameters, so tagging a call doesn't require changing the Client interface or any of its three provider implementations. - Prompt/completion text is never recorded by default - only a sha256 prompt_hash goes on the span. Opt-in via OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true (the upstream OTel GenAI instrumentation convention's own env var for this), which adds span events (never attributes) carrying the actual text, for local debugging only. - Wired into llm.New() for all three providers (openai, anthropic, ollama) and into all six ai_service.go call sites that reach a Client, each tagged with its own agent name and incident ID where one is in scope. - Live-verified against a real Jaeger collector through the real llm.New() path for all three providers: correct system/model/tokens/ cost/agent/incident tags, zero span events (no PII) by default. Found and flagged (not fixed here - unrelated to span conventions): buildAnswerQuestionPrompt panics on a nil *models.Incident even though AnswerQuestion's own signature documents current as optional. Dormant today (the only caller already guards incident == nil first) but worth its own fix.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes REG-12
Summary
Every LLM completion now produces a span using OTel's GenAI semantic conventions, so a bad answer, a cost spike, or a latency regression can all be traced back to the exact call that caused it.
What changed
internal/integrations/llm/instrumented.go(new):instrumentedClientwraps any providerClientwith a span perComplete()call. Uses semconv v1.34.0 specifically - the last version in this module's vendored set that still names the attributegen_ai.system(later revisions rename it togen_ai.provider.name; GenAI semconv is marked "Development" stability upstream and the names churn between versions - v1.34.0 is the one matching what REG-12 asks for).internal/integrations/llm/pricing.go(new): a small OSS per-model USD price table for the span's estimated cost. This is a separate computation from whatever Pro'sCostTracker.RecordUsagecharges internally (different repo, no shared code) - but both are fed the same Model/PromptTokens/CompletionTokens for a call, so the two agree numerically without needing to share an implementation.internal/integrations/llm/callmeta.go(new):CallMeta(agent name + incident ID) travels via context rather than asComplete()parameters, so tagging a call never requires changing theClientinterface or touching all three provider implementations.internal/observability/genai.go(new):GenAIPromptRecordingEnabled(), gated onOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT- the real upstream OTel GenAI instrumentation convention's own env var for this - mirroringSQLStatementRecordingEnabled's shape and rationale exactly.llm.New()for all three providers (openai, anthropic, ollama) and into all sixai_service.gocall sites that reach aClient, each tagged with its own agent name ("Incident Summarizer", "Post-Mortem Drafter", ...) and incident ID where one is in scope.Acceptance criteria
gen_ai.system,gen_ai.request.model,gen_ai.usage.input_tokens,gen_ai.usage.output_tokens,gen_ai.operation.nameprompt_hash(sha256 of rendered system+user messages) as a span attributeincident.idandgen_ai.agent.namewhere applicable (the real GenAI semconv key for "named agent/persona", used instead of inventing a customagent.name)OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=truefor local debugging onlyCostTracker.RecordUsageat the handler level (pre-existing, using the same Model/Usage values this PR's spans use) - verified unaffectedFound and flagged, not fixed here
buildAnswerQuestionPromptdereferencescurrent *models.Incidentunconditionally even thoughAnswerQuestion's own signature documents it as optional (nil-able). This panics if ever called withcurrent == nil. It's dormant today - the sole caller inslack_event_handler.goalready guardsincident == nilbefore callingAnswerQuestion- but is a real defensive gap for the next caller. Out of scope for this ticket (span conventions, not this bug); flagging rather than silently fixing or silently leaving undocumented.Verification
go build ./...,go vet ./...,gofmt -lclean on all touched files-shuffle=onon all touched packagesgolangci-lint run ./...: 0 issuesinternal/integrations/llmpackage: 90.8% overall)llm.New()path, for all three providers: correctsystem/model/token counts/cost/agent.name/incident.id/prompt_hashtags, zero span events (no PII) by default