Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions examples/src/otel_trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {
type JobContext,
ServerOptions,
cli,
defineAgent,
inference,
llm,
log,
logMetrics,
stt,
telemetry,
tts,
voice,
} from '@livekit/agents';
import * as openai from '@livekit/agents-plugin-openai';
import { type Attributes } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { fileURLToPath } from 'node:url';
import { z } from 'zod';

// This example shows how to trace the agent session with OpenTelemetry.
// It exports spans over OTLP/HTTP, so it works with any OTLP-compatible backend
// (Langfuse, Jaeger, Grafana Tempo, Honeycomb, etc.). To enable tracing, set the trace
// provider with `telemetry.setTracerProvider` at the module level or inside the entrypoint
// before `AgentSession.start()`.
//
// Configure the destination either by passing `url`/`headers` to `setupOtel`, or by leaving
// them unset and exporting the standard OTLP environment variables:
// OTEL_EXPORTER_OTLP_ENDPOINT=https://my-collector.example.com
// OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <token>
//
// Worked example - Langfuse: the endpoint is `<LANGFUSE_HOST>/api/public/otel` and auth
// is a base64-encoded `Authorization: Basic` header built from the public/secret keys:
// const auth = Buffer.from(`${publicKey}:${secretKey}`).toString('base64');
// setupOtel({
// url: `${host.replace(/\/$/, '')}/api/public/otel`,
// headers: { Authorization: `Basic ${auth}`, 'x-langfuse-ingestion-version': '4' },
// });
// Refer to their docs for latest instructions: https://langfuse.com/integrations/native/opentelemetry#opentelemetry-endpoint
function setupOtel(options?: {
metadata?: Attributes;
url?: string;
headers?: Record<string, string>;
}): NodeTracerProvider {
const traceExporter = new OTLPTraceExporter({
url: options?.url,
headers: options?.headers,
});
const traceProvider = new NodeTracerProvider({
spanProcessors: [new BatchSpanProcessor(traceExporter)],
});

traceProvider.register();
telemetry.setTracerProvider(traceProvider, { metadata: options?.metadata });
return traceProvider;
}

const logger = log().child({ example: 'otel-trace-example' });

const lookupWeather = llm.tool({
name: 'lookupWeather',
description: 'Called when the user asks for weather related information.',
parameters: z.object({
location: z.string().describe('The location they are asking for'),
}),
execute: async ({ location }) => {
logger.info({ location }, 'Looking up weather');
return 'sunny with a temperature of 70 degrees.';
},
});

class Kelly extends voice.Agent {
constructor() {
super({
instructions: 'Your name is Kelly.',
tools: [
lookupWeather,
llm.tool({
name: 'transferToAlloy',
description: 'Transfer the call to Alloy.',
parameters: z.object({}),
execute: async () => {
logger.info('Transferring the call to Alloy');
return llm.handoff({ agent: new Alloy(), returns: 'Transfer complete.' });
},
}),
],
});
}

async onEnter() {
logger.info('Kelly is entering the session');
this.session.generateReply();
}
}

class Alloy extends voice.Agent {
constructor() {
super({
instructions: 'Your name is Alloy.',
llm: new openai.realtime.RealtimeModel({ voice: 'alloy' }),
tools: [
lookupWeather,
llm.tool({
name: 'transferToKelly',
description: 'Transfer the call to Kelly.',
parameters: z.object({}),
execute: async () => {
logger.info('Transferring the call to Kelly');
return llm.handoff({ agent: new Kelly(), returns: 'Transfer complete.' });
},
}),
],
});
}

async onEnter() {
logger.info('Alloy is entering the session');
this.session.generateReply();
}
}

export default defineAgent({
entry: async (ctx: JobContext) => {
// Set up the OpenTelemetry tracer.
const traceProvider = setupOtel({
// Metadata is set as attributes on all spans created by the tracer; some backends have
// their own grouping conventions (e.g. Langfuse uses `langfuse.session.id` or `session.id`).
metadata: {
'session.id': ctx.room.name,
},
});

// Optional: add a shutdown callback to flush the trace before process exit.
ctx.addShutdownCallback(async () => {
await traceProvider.forceFlush();
});

const session = new voice.AgentSession({
llm: new llm.FallbackAdapter({
llms: [
new inference.LLM({ model: 'openai/gpt-4.1-mini' }),
new inference.LLM({ model: 'google/gemini-2.5-flash' }),
],
}),
stt: new stt.FallbackAdapter({
sttInstances: [
new inference.STT({ model: 'deepgram/nova-3' }),
new inference.STT({ model: 'cartesia/ink-whisper' }),
],
}),
tts: new tts.FallbackAdapter({
ttsInstances: [
new inference.TTS({ model: 'cartesia/sonic-3' }),
new inference.TTS({ model: 'rime/arcana' }),
],
}),
});

session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
logMetrics(ev.metrics);
});

await session.start({
agent: new Kelly(),
room: ctx.room,
});
},
});

cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) }));