I Built an OpenTelemetry Package for the GitHub Copilot SDK + OTel β€” Here's Why You Need It

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

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.

1 Like