The GitHub Copilot SDK lets you embed GitHubβs LLM models β GPT-4o, Claude, and others β directly into your Node.js applications. You get a programmatic API to create sessions, send prompts, define tools, and stream responses.
But hereβs what you donβt get out of the box: any visibility into whatβs happening inside those LLM calls once theyβre running in production.
- How many tokens are my sessions burning?
- Whatβs the P95 latency of model responses?
- Are tool calls failing silently?
- Can I trace a user prompt through the full lifecycle β model call β tool execution β response?
I needed answers to all of these. So I built @theharithsa/github_copilot_otel β a reusable OpenTelemetry instrumentation package that plugs into any Copilot SDK agent and gives you full AI observability with zero custom instrumentation code.
What It Does
The package subscribes to Copilot SDK session events and automatically generates:
Distributed Traces following the OpenTelemetry GenAI semantic conventions:
invoke_agent (SERVER) β one per session
βββ chat claude-sonnet-4-5 (CLIENT) β per LLM call
βββ execute_tool get_time (CLIENT) β per tool execution
βββ chat claude-sonnet-4-5 (CLIENT) β subsequent LLM call
βββ ...
Metrics for cost tracking and performance monitoring:
| Metric | Type | What It Tracks |
|---|---|---|
copilot_sdk.llm.tokens.total |
Counter | Token usage by model, direction, and type |
copilot_sdk.llm.latency |
Histogram | LLM response latency in milliseconds |
copilot_sdk.tools.executed |
Counter | Tool executions by name and outcome |
Structured Logs emitted at every key event β user messages, LLM completions, tool executions, and errors β each carrying trace_id and span_id attributes for direct trace-to-log correlation.
The Architecture
Hereβs how the package fits into an application:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your App β
β β
β βββββββββββββββββ ββββββββββββββββββββββββββββββββββββ β
β β Copilot SDK βββββΆβ @theharithsa/github_copilot_otel β β
β β Session Eventsβ β β β
β β β β invoke_agent (root span) β β
β β user.message β β βββ chat claude-sonnet (span) β β
β β assistant.* β β βββ execute_tool get_time β β
β β tool.* β β βββ chat claude-sonnet (span) β β
β β session.* β β βββ metrics + logs β β
β βββββββββββββββββ ββββββββββββββββ¬ββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββ
β OTLP/HTTP (protobuf)
βΌ
βββββββββββββββββββ
β Any OTel β
β Backend β
β (Dynatrace, β
β Grafana, etc) β
βββββββββββββββββββ
The Copilot SDK fires events like user.message, assistant.usage, tool.execution_start, tool.execution_complete, and session.error. The package listens to all of these and translates them into proper OpenTelemetry spans, metrics, and log records β using the exact attribute names that observability platforms expect for AI/GenAI dashboards.
How to Use It
Install
npm install @theharithsa/github_copilot_otel @opentelemetry/api
@opentelemetry/api is a peer dependency so all OTel instrumentation in your app shares the same global instance.
Set Environment Variables
# Your observability backend's OTLP endpoint
DYNATRACE_OTLP_URL=https://abc123.live.dynatrace.com/api/v2/otlp
DYNATRACE_OTLP_TOKEN=dt0c01.xxxx.xxxxxxxx
# GitHub token for Copilot SDK
GH_TOKEN=ghp_xxxx
# Optional
OTEL_SERVICE_NAME=my-copilot-agent
Wire It Up
Hereβs a complete working example β this is the actual test app I use outside the package repo:
import {
initTelemetry,
shutdownTelemetry,
} from "@theharithsa/github_copilot_otel";
// Initialize telemetry BEFORE importing the SDK
// so that any auto-instrumented HTTP calls are captured.
initTelemetry();
import { CopilotClient, defineTool } from "@github/copilot-sdk";
import {
subscribeSessionTelemetry,
} from "@theharithsa/github_copilot_otel";
// Define tools
const getCurrentTime = defineTool("get_current_time", {
description: "Get the current date and time",
handler: async () => new Date().toISOString(),
});
async function main() {
const client = new CopilotClient({
githubToken: process.env.GH_TOKEN,
});
await client.start();
const model = process.env.PROVIDER_MODEL
|| "claude-sonnet-4-5-20250929";
const session = await client.createSession({
model,
tools: [getCurrentTime],
availableTools: ["get_current_time"],
systemMessage: {
mode: "append",
content: "You are a helpful assistant.",
},
streaming: true,
onPermissionRequest: async () => ({ kind: "approved" }),
});
// One line to subscribe to all session telemetry
const cleanup = subscribeSessionTelemetry(
session,
session.sessionId,
model,
);
// Send a message
const prompt = process.argv[2] || "What time is it?";
let content = "";
session.on("assistant.message_delta", (event) => {
content += event.data.deltaContent;
});
await session.sendAndWait({ prompt });
console.log(`Response: ${content}`);
// Cleanup
cleanup();
await session.destroy();
await client.stop();
await shutdownTelemetry();
}
main().catch(console.error);
The key call is subscribeSessionTelemetry(session, session.sessionId, model) β that single line wires up all the trace, metric, and log instrumentation for the entire session lifecycle.
What You See in Your Observability Platform
Once telemetry is flowing, you get:
Traces
A root invoke_agent span for each session, with child spans for every LLM call (chat {model}) and tool execution (execute_tool {toolName}). Each span carries GenAI semantic convention attributes like gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.request.model, and gen_ai.provider.name.
If youβre using Dynatrace, these spans light up the AI Observability app automatically because they emit the exact attributes it filters on: gen_ai.provider.name and llm.request.type = "chat".
Metrics
Counters and histograms that let you build dashboards for:
- Token consumption over time (broken down by model, input vs. output)
- LLM response latency percentiles
- Tool execution success/failure rates
Logs with Trace Correlation
Every session event emits a structured log record with explicit trace_id and span_id attributes. This means you can:
- Query logs by trace ID:
fetch logs | filter trace_id == "abc123..." - Click from a log entry directly to the correlated trace
- See the full context of what happened during a session β the userβs prompt, the modelβs token usage, which tools ran, and any errors β in a single correlated view
Why I Built It as a Separate Package
The Copilot SDK agent code and the instrumentation code serve different purposes. The agent code is your application logic β tools, prompts, business rules. The instrumentation code is infrastructure β it shouldnβt live mixed into your app.
By packaging it as @theharithsa/github_copilot_otel, anyone building a Copilot SDK agent can npm install it and get production-grade observability immediately. No need to learn the GenAI semantic conventions, figure out how to configure OTLP exporters, or wire up span hierarchies manually.
The package lives in its own repo at github.com/theharithsa/github_copilot_otel. The consumer app that imports it can live anywhere β it just needs @theharithsa/github_copilot_otel and @opentelemetry/api as dependencies.
Whatβs Inside
The package is two files:
telemetry.ts β bootstraps the OpenTelemetry Node SDK with OTLP/HTTP protobuf exporters for traces, metrics, and logs. Handles all the Dynatrace-specific configuration (delta temporality for metrics, auth headers, endpoint routing to /v1/traces, /v1/metrics, /v1/logs). Exposes initTelemetry(), shutdownTelemetry(), getTracer(), getMeter(), and getLogger().
instrumentation.ts β subscribes to Copilot SDK session events and translates them into OTel spans, metrics, and logs. Creates the span hierarchy (invoke_agent β chat / execute_tool), records token counters and latency histograms, emits structured logs with trace correlation, and handles cleanup of orphaned spans on session shutdown.
Handling the OTel SDK 2.x Breaking Change
One gotcha I hit while building this: @opentelemetry/resources v2.x changed Resource from a class to a type-only export. If you try new Resource({...}) you get:
error TS2693: 'Resource' only refers to a type,
but is being used as a value here.
The fix is to use resourceFromAttributes() instead:
import { resourceFromAttributes } from "@opentelemetry/resources";
const resource = resourceFromAttributes({
[ATTR_SERVICE_NAME]: "my-service",
});
This package already handles this, so you donβt have to.
Get Started
npm install @theharithsa/github_copilot_otel @opentelemetry/api
- npm: @theharithsa/github_copilot_otel
- GitHub: theharithsa/github_copilot_otel
- License: MIT
If youβre building with the GitHub Copilot SDK and want observability without the boilerplate, give it a try. PRs and issues are welcome.
Built with TypeScript, OpenTelemetry JS SDK 2.x, and tested with Dynatrace AI Observability.