OpenTelemetry instrumentation for the Copilot Chat extension — covers the four agent execution paths, the IOTelService abstraction, span/metric/event conventions, and the relationship between code and the user/developer monitoring docs. Use when adding/changing OTel spans, metrics, or events; instrumenting a new agent surface; touching the Copilot CLI bridge or Claude span emission; or updating `extensions/copilot/docs/monitoring/agent_monitoring*.md`.
Instrucciones de origen · Vista previa de solo lectura
name
otel
description
OpenTelemetry instrumentation for the Copilot Chat extension — covers the four agent execution paths, the IOTelService abstraction, span/metric/event conventions, and the relationship between code and the user/developer monitoring docs. Use when adding/changing OTel spans, metrics, or events; instrumenting a new agent surface; touching the Copilot CLI bridge or Claude span emission; or updating `extensions/copilot/docs/monitoring/agent_monitoring*.md`.
OpenTelemetry Instrumentation Skill
When adding, changing, or reviewing OTel telemetry in the Copilot Chat extension, always read the two source-of-truth docs first and always keep them in sync with the code you change.
1. Authoritative Documents
The extensions/copilot/docs/monitoring/ directory contains the two specs that define the OTel contract for the extension. Treat them like the layout / layer specs in vs/sessions.
Synthesized from SDK messages — extension intercepts the Claude SDK message stream in claudeMessageDispatch.ts and emits GenAI spans; LLM calls are proxied through claudeLanguageModelServer.ts (which calls chatMLFetcher, producing standard chat spans).
Extension spans
Why asymmetric? The CLI SDK runs in-process with full trace hierarchy (subagents, permissions, hooks). A bridge captures this directly. Claude runs as a separate process — internal spans are inaccessible, so the extension synthesizes spans by translating SDK messages and proxying the model API.
Three namespaces coexist on extension-emitted spans:
Namespace
Purpose
Status
gen_ai.*
OTel GenAI Semantic Conventions. Use whenever a standard key exists.
Canonical
github.copilot.*
Copilot-specific vendor namespace.
Preferred — new attributes go here.
copilot_chat.*
Original VS Code-only namespace. Several keys remain for backwards compatibility.
Legacy — keep emitting; do not add new keys here.
Dual-emit rules
When adding a new attribute that belongs to Copilot's vendor namespace, emit it under github.copilot.* only — do not introduce a copilot_chat.* twin.
When renaming an existing copilot_chat.* attribute to its github.copilot.* equivalent (e.g., copilot_chat.repo.* → github.copilot.git.*, gen_ai.usage.reasoning_tokens → gen_ai.usage.reasoning.output_tokens), dual-emit both keys indefinitely. Downstream readers (Agent Debug Log, Chronicle, SQLite span store, OTLP collectors) may depend on the legacy key.
Mark the legacy row in agent_monitoring.md with Legacy in the "Requirement" column and a pointer to the preferred key. No sunset date — legacy keys live on indefinitely.
Hash sensitive identifiers (e.g., MCP server names) with hashTelemetryValue from util/node/crypto.ts. Emit hashes unconditionally; raw values only when captureContent is enabled.
4. Service Layer & Selection
IOTelService (otelService.ts) is the only abstraction consumers should depend on — never import the OTel SDK directly outside node/otelServiceImpl.ts. Three implementations:
Class
When Used
NoopOTelService
chatLib and tests where no telemetry pipeline is needed — zero cost
Registered when OTel is disabled — no SDK is loaded, but spans/metrics/logs are still captured in-memory so the Agent Debug Log panel keeps working
Selection happens in src/extension/extension/vscode-node/services.ts: exactly one of NodeOTelService or InMemoryOTelService is bound to IOTelService per extension host based on resolveOTelConfig().enabled.
// Parent: store context keyed by something the child knowsconst ctx = this._otelService.getActiveTraceContext();
if (ctx) { this._otelService.storeTraceContext(`subagent:invocation:${id}`, ctx); }
// Child: retrieve and use as parentconst parentCtx = this._otelService.getStoredTraceContext(`subagent:invocation:${id}`);
returnthis._otelService.startActiveSpan('invoke_agent child', { parentTraceContext: parentCtx, … }, fn);
Content capture
The extension uses two conventions side-by-side; pick the right one for the attribute you're adding.
Always emit (truncated) — used for inputs/outputs that the Agent Debug Log panel needs to be useful even when OTel export is off (e.g. gen_ai.tool.call.arguments in toolsService.ts, and copilot_chat.hook_input / hook_output in chatHookService.ts). The attribute is captured unconditionally but always passed through truncateForOTel. Use this for moderate-sized, generally-non-secret arguments / results.
Gate on config.captureContent — used for full prompt / response / system-instruction bodies (e.g. gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, gen_ai.tool.definitions in chatMLFetcher.ts and the BYOK providers). These are larger and more likely to contain user secrets.
Spans whose gen_ai.operation.name is not in EXPORTABLE_OPERATION_NAMES (defined in otelServiceImpl.ts) are visible to the debug panel via onDidCompleteSpan but excluded from OTLP and SQLite exporters by DiagnosticSpanExporter and FilteredSpanExporter. Currently exportable: chat, invoke_agent, execute_tool, embeddings, execute_hook. If you add a new operation name that should reach the user's collector, update EXPORTABLE_OPERATION_NAMES and document it in agent_monitoring.md.
6. Configuration Surface (must stay in sync)
When you add or change a setting/env var/command, update all three of:
deriveCopilotCliOTelEnv / deriveClaudeOTelEnv in agentOTelEnv.ts.
The corresponding tests in src/platform/otel/common/test/agentOTelEnv.spec.ts.
7. Procedure Checklists
When adding a new span / attribute
Add the attribute key as a constant to genAiAttributes.ts (under GenAiAttr, CopilotChatAttr, or a new domain group). Never inline a raw 'copilot_chat.foo' literal.
Add it to the public barrel in index.ts if it lives in a new group.
Use IOTelService.startActiveSpan (preferred) or startSpan — never BasicTracerProvider / getTracer directly.
Pass the value through truncateForOTel (mandatory for any free-form content attribute — prevents OTLP batch failures). Decide whether the attribute should be always-emitted (debug-panel-essential, e.g. tool args, hook input/output) or gated on config.captureContent (large prompt/response bodies, system instructions); follow the existing convention for similar data.
If the new operation should reach OTLP, add its op-name to EXPORTABLE_OPERATION_NAMES in otelServiceImpl.ts.
Document the new attribute in agent_monitoring.md (under the relevant span table) and add a test in src/platform/otel/common/test/.
When adding a new metric / event
Add the helper to genAiMetrics.ts or genAiEvents.ts (mirror existing static / functional patterns).
Re-export it from index.ts.
Add the metric/event row to agent_monitoring.md ("Metrics" / "Events" sections) with all attributes documented.
Add a unit test in src/platform/otel/common/test/genAiMetrics.spec.ts or genAiEvents.spec.ts (assert the exact name + attribute keys).
When instrumenting a new agent surface
Pick a strategy: direct spans (foreground-style), bridge processor (CLI-style), or message-stream synthesis (Claude-style).
Add the new emit site to the Instrumentation Points table in agent_monitoring_arch.md and the Span Hierarchies diagrams.
If you forward OTel env vars to a child process, do it via a new derive*OTelEnv helper in agentOTelEnv.ts and add a row to the Agent-Specific Env Var Translation table.
Wire trace propagation explicitly with storeTraceContext / parentTraceContext for any subagent or async boundary; do not rely on global active context across processes.
When changing the Copilot CLI bridge
The bridge (copilotCliBridgeSpanProcessor.ts) reaches into _delegate._activeSpanProcessor._spanProcessors — internal OTel SDK v2 state. This is documented as a known risk. If you touch it:
Keep the runtime guard that degrades gracefully if the internal shape changes.
Update the ⚠ SDK Internal Access Warning block in agent_monitoring_arch.md if the access pattern changes.
Add a unit test in copilotCliBridgeSpanProcessor.spec.ts.
8. Validation
Before sending a PR that touches OTel code:
# From extensions/copilot/
npx tsc --noEmit --project tsconfig.json
# OTel + Bridge unit tests
npm test -- --grep "OTel\|Bridge"
Manual sanity checks:
The Aspire Dashboard quick-start in agent_monitoring.md still works end-to-end (one agent message → invoke_agent + chat + execute_tool spans visible at http://localhost:18888).
The Agent Debug Log panel in VS Code still shows the full span tree for foreground, Copilot CLI, and Claude sessions.
9. Known Risks & Limitations
These are documented in agent_monitoring_arch.md — preserve them:
Two TracerProviders in the same process when CLI SDK is active.
process.env mutation for the CLI SDK (only OTel-specific vars, set before LocalSessionManager ctor).
Single captureContent flag for the CLI SDK applies to both debug panel and OTLP — document any user-visible change clearly.
Claude SDK has no file exporter, and the CLI runtime only supports otlp-http.
10. Anti-Patterns to Reject
❌ Importing @opentelemetry/api (or any @opentelemetry/* package) from anywhere other than node/otelServiceImpl.ts, fileExporters.ts, or the CLI bridge processor type imports.
❌ Hard-coded attribute keys: 'copilot_chat.hook_type' instead of CopilotChatAttr.HOOK_TYPE.