diff --git a/.gitignore b/.gitignore index 0ec569d7c..c44818c95 100644 --- a/.gitignore +++ b/.gitignore @@ -189,4 +189,8 @@ docs # vscode workspace config agents-js.code-workspace -examples/src/test_*.ts \ No newline at end of file +examples/src/test_*.ts + +# Ignore all markdown files except root README +*.md +!README.md \ No newline at end of file diff --git a/agents/package.json b/agents/package.json index dfcee3555..07740b20e 100644 --- a/agents/package.json +++ b/agents/package.json @@ -50,6 +50,12 @@ "@livekit/mutex": "^1.1.1", "@livekit/protocol": "^1.41.0", "@livekit/typed-emitter": "^3.0.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.54.0", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/sdk-trace-base": "^1.28.0", + "@opentelemetry/sdk-trace-node": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "@types/pidusage": "^2.0.5", "commander": "^12.0.0", "fluent-ffmpeg": "^2.1.3", diff --git a/agents/src/index.ts b/agents/src/index.ts index bc599b735..a92d4bf3c 100644 --- a/agents/src/index.ts +++ b/agents/src/index.ts @@ -16,6 +16,7 @@ import * as llm from './llm/index.js'; import * as metrics from './metrics/index.js'; import * as stream from './stream/index.js'; import * as stt from './stt/index.js'; +import * as telemetry from './telemetry/index.js'; import * as tokenize from './tokenize/index.js'; import * as tts from './tts/index.js'; import * as voice from './voice/index.js'; @@ -34,4 +35,4 @@ export * from './vad.js'; export * from './version.js'; export * from './worker.js'; -export { cli, inference, ipc, llm, metrics, stream, stt, tokenize, tts, voice }; +export { cli, inference, ipc, llm, metrics, stream, stt, telemetry, tokenize, tts, voice }; diff --git a/agents/src/job.ts b/agents/src/job.ts index 63c380dd8..66fa31aba 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -81,6 +81,8 @@ export class FunctionExistsError extends Error { } /** The job and environment context as seen by the agent, accessible by the entrypoint function. */ +// TODO(brian): PR3 - Add @tracer.startActiveSpan('job_entrypoint') wrapper in entrypoint +// TODO(brian): PR5 - Add uploadSessionReport() call in cleanup/session end export class JobContext { #proc: JobProcess; #info: RunningJobInfo; @@ -245,6 +247,7 @@ export class JobContext { } // TODO(brian): implement and check recorder io + // TODO(brian): PR5 - Ensure chat history serialization includes all required fields (use sessionReportToJSON helper) return createSessionReport({ jobId: this.job.id, @@ -267,7 +270,8 @@ export class JobContext { // TODO(brian): Implement CLI/console - // TODO(brian): Implement session report upload to LiveKit Cloud + // TODO(brian): PR5 - Call uploadSessionReport() if report.enableUserDataTraining is true + // TODO(brian): PR5 - Upload includes: multipart form with header (protobuf), chat_history (JSON), and audio recording (if available) this.#logger.debug('Session ended, report generated', { jobId: report.jobId, diff --git a/agents/src/llm/llm.ts b/agents/src/llm/llm.ts index aa1ac4374..1a39a0c2b 100644 --- a/agents/src/llm/llm.ts +++ b/agents/src/llm/llm.ts @@ -136,8 +136,10 @@ export abstract class LLMStream implements AsyncIterableIterator { } private async mainTask() { + // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('llm_request', ..., { endOnExit: false }) for (let i = 0; i < this._connOptions.maxRetry + 1; i++) { try { + // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('llm_request_run', ...) return await this.run(); } catch (error) { if (error instanceof APIError) { diff --git a/agents/src/log.ts b/agents/src/log.ts index dbd1a4c4e..cb8077f12 100644 --- a/agents/src/log.ts +++ b/agents/src/log.ts @@ -42,4 +42,5 @@ export const initializeLogger = ({ pretty, level }: LoggerOptions) => { if (level) { logger.level = level; } + // TODO(brian): PR4 - Add Pino bridge to OTEL LoggingHandler for structured logging integration }; diff --git a/agents/src/telemetry/index.ts b/agents/src/telemetry/index.ts new file mode 100644 index 000000000..755f123b0 --- /dev/null +++ b/agents/src/telemetry/index.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +// TODO(brian): PR2 - Add setupCloudTracer, MetadataSpanProcessor exports +// TODO(brian): PR4 - Add logging integration exports +// TODO(brian): PR5 - Add uploadSessionReport export + +export * as traceTypes from './trace_types.js'; +export { setTracerProvider, tracer, type SpanStartOptions as StartSpanOptions } from './traces.js'; +export { recordException, recordRealtimeMetrics } from './utils.js'; diff --git a/agents/src/telemetry/trace_types.ts b/agents/src/telemetry/trace_types.ts new file mode 100644 index 000000000..db76f7bc1 --- /dev/null +++ b/agents/src/telemetry/trace_types.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +// LiveKit custom attributes +export const ATTR_SPEECH_ID = 'lk.speech_id'; +export const ATTR_AGENT_LABEL = 'lk.agent_label'; +export const ATTR_START_TIME = 'lk.start_time'; +export const ATTR_END_TIME = 'lk.end_time'; +export const ATTR_RETRY_COUNT = 'lk.retry_count'; + +export const ATTR_PARTICIPANT_ID = 'lk.participant_id'; +export const ATTR_PARTICIPANT_IDENTITY = 'lk.participant_identity'; +export const ATTR_PARTICIPANT_KIND = 'lk.participant_kind'; + +// session start +export const ATTR_JOB_ID = 'lk.job_id'; +export const ATTR_AGENT_NAME = 'lk.agent_name'; +export const ATTR_ROOM_NAME = 'lk.room_name'; +export const ATTR_SESSION_OPTIONS = 'lk.session_options'; + +// assistant turn +export const ATTR_USER_INPUT = 'lk.user_input'; +export const ATTR_INSTRUCTIONS = 'lk.instructions'; +export const ATTR_SPEECH_INTERRUPTED = 'lk.interrupted'; + +// llm node +export const ATTR_CHAT_CTX = 'lk.chat_ctx'; +export const ATTR_FUNCTION_TOOLS = 'lk.function_tools'; +export const ATTR_RESPONSE_TEXT = 'lk.response.text'; +export const ATTR_RESPONSE_FUNCTION_CALLS = 'lk.response.function_calls'; + +// function tool +export const ATTR_FUNCTION_TOOL_NAME = 'lk.function_tool.name'; +export const ATTR_FUNCTION_TOOL_ARGS = 'lk.function_tool.arguments'; +export const ATTR_FUNCTION_TOOL_IS_ERROR = 'lk.function_tool.is_error'; +export const ATTR_FUNCTION_TOOL_OUTPUT = 'lk.function_tool.output'; + +// tts node +export const ATTR_TTS_INPUT_TEXT = 'lk.input_text'; +export const ATTR_TTS_STREAMING = 'lk.tts.streaming'; +export const ATTR_TTS_LABEL = 'lk.tts.label'; + +// eou detection +export const ATTR_EOU_PROBABILITY = 'lk.eou.probability'; +export const ATTR_EOU_UNLIKELY_THRESHOLD = 'lk.eou.unlikely_threshold'; +export const ATTR_EOU_DELAY = 'lk.eou.endpointing_delay'; +export const ATTR_EOU_LANGUAGE = 'lk.eou.language'; +export const ATTR_USER_TRANSCRIPT = 'lk.user_transcript'; +export const ATTR_TRANSCRIPT_CONFIDENCE = 'lk.transcript_confidence'; +export const ATTR_TRANSCRIPTION_DELAY = 'lk.transcription_delay'; +export const ATTR_END_OF_TURN_DELAY = 'lk.end_of_turn_delay'; + +// metrics +export const ATTR_LLM_METRICS = 'lk.llm_metrics'; +export const ATTR_TTS_METRICS = 'lk.tts_metrics'; +export const ATTR_REALTIME_MODEL_METRICS = 'lk.realtime_model_metrics'; + +// OpenTelemetry GenAI attributes +// OpenTelemetry specification: https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ +export const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name'; +export const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model'; +export const ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens'; +export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens'; + +// Unofficial OpenTelemetry GenAI attributes, recognized by LangFuse +// https://langfuse.com/integrations/native/opentelemetry#usage +// but not yet in the official OpenTelemetry specification. +export const ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS = 'gen_ai.usage.input_text_tokens'; +export const ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS = 'gen_ai.usage.input_audio_tokens'; +export const ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS = 'gen_ai.usage.input_cached_tokens'; +export const ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS = 'gen_ai.usage.output_text_tokens'; +export const ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS = 'gen_ai.usage.output_audio_tokens'; + +// OpenTelemetry GenAI event names (for structured logging) +export const EVENT_GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message'; +export const EVENT_GEN_AI_USER_MESSAGE = 'gen_ai.user.message'; +export const EVENT_GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message'; +export const EVENT_GEN_AI_TOOL_MESSAGE = 'gen_ai.tool.message'; +export const EVENT_GEN_AI_CHOICE = 'gen_ai.choice'; + +// Exception attributes +export const ATTR_EXCEPTION_TRACE = 'exception.stacktrace'; +export const ATTR_EXCEPTION_TYPE = 'exception.type'; +export const ATTR_EXCEPTION_MESSAGE = 'exception.message'; + +// Platform-specific attributes +export const ATTR_LANGFUSE_COMPLETION_START_TIME = 'langfuse.observation.completion_start_time'; diff --git a/agents/src/telemetry/traces.ts b/agents/src/telemetry/traces.ts new file mode 100644 index 000000000..dd2078585 --- /dev/null +++ b/agents/src/telemetry/traces.ts @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { + type Attributes, + type Context, + type Span, + type SpanOptions, + type Tracer, + type TracerProvider, + context as otelContext, + trace, +} from '@opentelemetry/api'; + +export interface SpanStartOptions { + /** Name of the span */ + name: string; + /** Optional parent context to use for this span */ + context?: Context; + /** Attributes to set on the span when it starts */ + attributes?: Attributes; + /** Whether to end the span when the function exits (default: true) */ + endOnExit?: boolean; +} + +/** + * A dynamic tracer that allows the tracer provider to be changed at runtime. + */ +class DynamicTracer { + private tracerProvider: TracerProvider; + private tracer: Tracer; + private readonly instrumentingModuleName: string; + + constructor(instrumentingModuleName: string) { + this.instrumentingModuleName = instrumentingModuleName; + this.tracerProvider = trace.getTracerProvider(); + this.tracer = trace.getTracer(instrumentingModuleName); + } + + /** + * Set a new tracer provider. This updates the underlying tracer instance. + * @param provider - The new tracer provider to use + */ + setProvider(provider: TracerProvider): void { + this.tracerProvider = provider; + this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName); + } + + /** + * Get the underlying OpenTelemetry tracer. + * Use this to access the full Tracer API when needed. + */ + getTracer(): Tracer { + return this.tracer; + } + + /** + * Start a span manually (without making it active). + * You must call span.end() when done. + * + * @param options - Span configuration including name + * @returns The created span + */ + startSpan(options: SpanStartOptions): Span { + const ctx = options.context || otelContext.active(); + const span = this.tracer.startSpan( + options.name, + { + attributes: options.attributes, + }, + ctx, + ); + + return span; + } + + /** + * Start a new span and make it active in the current context. + * The span will automatically be ended when the provided function completes (unless endOnExit=false). + * + * @param fn - The function to execute within the span context + * @param options - Span configuration including name + * @returns The result of the provided function + */ + async startActiveSpan(fn: (span: Span) => Promise, options: SpanStartOptions): Promise { + const ctx = options.context || otelContext.active(); + const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true + const opts: SpanOptions = { attributes: options.attributes }; + + return new Promise((resolve, reject) => { + this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => { + try { + const result = await fn(span); + resolve(result); + } catch (error) { + reject(error); + } finally { + if (endOnExit) { + span.end(); + } + } + }); + }); + } + + /** + * Synchronous version of startActiveSpan for non-async operations. + * + * @param fn - The function to execute within the span context + * @param options - Span configuration including name + * @returns The result of the provided function + */ + startActiveSpanSync(fn: (span: Span) => T, options: SpanStartOptions): T { + const ctx = options.context || otelContext.active(); + const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true + const opts: SpanOptions = { attributes: options.attributes }; + + return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => { + try { + return fn(span); + } finally { + if (endOnExit) { + span.end(); + } + } + }); + } +} + +/** + * The global tracer instance used throughout the agents framework. + * This tracer can have its provider updated at runtime via setTracerProvider(). + */ +export const tracer = new DynamicTracer('livekit-agents'); + +/** + * Set the tracer provider for the livekit-agents framework. + * This should be called before agent session start if using custom tracer providers. + * + * @param provider - The tracer provider to use + * + * @example + * ```typescript + * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; + * import { setTracerProvider } from '@livekit/agents/telemetry'; + * + * const provider = new NodeTracerProvider(); + * setTracerProvider(provider); + * ``` + */ +export function setTracerProvider(provider: TracerProvider): void { + tracer.setProvider(provider); +} diff --git a/agents/src/telemetry/utils.ts b/agents/src/telemetry/utils.ts new file mode 100644 index 000000000..4eba4f043 --- /dev/null +++ b/agents/src/telemetry/utils.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2025 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { type Span, SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api'; +import type { RealtimeModelMetrics } from '../metrics/base.js'; +import * as traceTypes from './trace_types.js'; +import { tracer } from './traces.js'; + +export function recordException(span: Span, error: Error): void { + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + + // Set exception attributes for better visibility + // (in case the exception event is not rendered by the backend) + span.setAttributes({ + [traceTypes.ATTR_EXCEPTION_TYPE]: error.constructor.name, + [traceTypes.ATTR_EXCEPTION_MESSAGE]: error.message, + [traceTypes.ATTR_EXCEPTION_TRACE]: error.stack || '', + }); +} + +export function recordRealtimeMetrics(span: Span, metrics: RealtimeModelMetrics): void { + const attrs: Record = { + [traceTypes.ATTR_GEN_AI_REQUEST_MODEL]: metrics.label || 'unknown', + [traceTypes.ATTR_REALTIME_MODEL_METRICS]: JSON.stringify(metrics), + [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.inputTokens, + [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.outputTokens, + [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS]: metrics.inputTokenDetails.textTokens, + [traceTypes.ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS]: metrics.inputTokenDetails.audioTokens, + [traceTypes.ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS]: metrics.inputTokenDetails.cachedTokens, + [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS]: metrics.outputTokenDetails.textTokens, + [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS]: metrics.outputTokenDetails.audioTokens, + }; + + // Add LangFuse-specific completion start time if TTFT is available + if (metrics.ttftMs !== undefined && metrics.ttftMs !== -1) { + const completionStartTime = metrics.timestamp + metrics.ttftMs; + // Convert to UTC ISO string for LangFuse compatibility + const completionStartTimeUtc = new Date(completionStartTime).toISOString(); + attrs[traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME] = completionStartTimeUtc; + } + + if (span.isRecording()) { + span.setAttributes(attrs); + } else { + const currentContext = otelContext.active(); + const spanContext = trace.setSpan(currentContext, span); + + // Create a dedicated child span for orphaned metrics + tracer.getTracer().startActiveSpan('realtime_metrics', {}, spanContext, (child) => { + try { + child.setAttributes(attrs); + } finally { + child.end(); + } + }); + } +} diff --git a/agents/src/tts/tts.ts b/agents/src/tts/tts.ts index 0fbdc033e..fc420afe6 100644 --- a/agents/src/tts/tts.ts +++ b/agents/src/tts/tts.ts @@ -157,8 +157,10 @@ export abstract class SynthesizeStream } private async mainTask() { + // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('tts_request', ..., { endOnExit: false }) for (let i = 0; i < this._connOptions.maxRetry + 1; i++) { try { + // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('tts_request_run', ...) return await this.run(); } catch (error) { if (error instanceof APIError) { @@ -385,8 +387,10 @@ export abstract class ChunkedStream implements AsyncIterableIterator { + // TODO(brian): PR3 - Add span: startSpan = tracer.startSpan('start_agent_activity', { attributes: { 'lk.agent_label': this.agent.label } }) + // TODO(brian): PR3 - Wrap prewarm calls with trace.useSpan(startSpan, endOnExit: false) const unlock = await this.lock.lock(); try { this.agent._agentActivity = this; @@ -281,6 +283,7 @@ export class AgentActivity implements RecognitionHooks { this.started = true; this._mainTask = Task.from(({ signal }) => this.mainTask(signal)); + // TODO(brian): PR3 - Wrap onEnter with tracer.startActiveSpan('on_enter', { attributes: { 'lk.agent_label': this.agent.label }, context: startSpan context }) this.createSpeechTask({ task: Task.from(() => this.agent.onEnter()), name: 'AgentActivity_onEnter', @@ -1243,6 +1246,7 @@ export class AgentActivity implements RecognitionHooks { } } + // TODO(brian): PR3 - Wrap entire pipelineReplyTask() method with tracer.startActiveSpan('agent_turn') private async pipelineReplyTask( speechHandle: SpeechHandle, chatCtx: ChatContext, @@ -2041,12 +2045,14 @@ export class AgentActivity implements RecognitionHooks { this.wakeupMainTask(); } + // TODO(brian): PR3 - Wrap entire drain() method with tracer.startActiveSpan('drain_agent_activity', { attributes: { 'lk.agent_label': this.agent.label } }) async drain(): Promise { const unlock = await this.lock.lock(); try { if (this._draining) return; this.cancelPreemptiveGeneration(); + // TODO(brian): PR3 - Wrap onExit with tracer.startActiveSpan('on_exit', { attributes: { 'lk.agent_label': this.agent.label } }) this.createSpeechTask({ task: Task.from(() => this.agent.onExit()), name: 'AgentActivity_onExit', diff --git a/agents/src/voice/agent_session.ts b/agents/src/voice/agent_session.ts index b7ee1b2cd..e0a1c33e4 100644 --- a/agents/src/voice/agent_session.ts +++ b/agents/src/voice/agent_session.ts @@ -212,6 +212,9 @@ export class AgentSession< } async start({ + // TODO(brian): PR2 - Add setupCloudTracer() call if on LiveKit Cloud with recording enabled + // TODO(brian): PR3 - Add span: this._sessionSpan = tracer.startSpan('agent_session'), store as instance property + // TODO(brian): PR4 - Add setupCloudLogger() call in setupCloudTracer() to setup OTEL logging with Pino bridge agent, room, inputOptions, @@ -445,6 +448,8 @@ export class AgentSession< return; } + // TODO(brian): PR3 - Add span: if state === 'speaking' && !this._agentSpeakingSpan, create tracer.startSpan('agent_speaking') with participant attributes + // TODO(brian): PR3 - Add span: if state !== 'speaking' && this._agentSpeakingSpan, end and clear this._agentSpeakingSpan const oldState = this._agentState; this._agentState = state; @@ -467,6 +472,8 @@ export class AgentSession< return; } + // TODO(brian): PR3 - Add span: if state === 'speaking' && !this._userSpeakingSpan, create tracer.startSpan('user_speaking') with participant attributes + // TODO(brian): PR3 - Add span: if state !== 'speaking' && this._userSpeakingSpan, end and clear this._userSpeakingSpan const oldState = this.userState; this.userState = state; diff --git a/agents/src/voice/audio_recognition.ts b/agents/src/voice/audio_recognition.ts index c4bd603e7..398cdab1d 100644 --- a/agents/src/voice/audio_recognition.ts +++ b/agents/src/voice/audio_recognition.ts @@ -57,6 +57,8 @@ export interface AudioRecognitionOptions { maxEndpointingDelay: number; } +// TODO(brian): PR3 - Add span: private _userTurnSpan?: Span, create lazily in _ensureUserTurnSpan() method (tracer.startSpan('user_turn') with participant attributes) +// TODO(brian): PR3 - Add span: 'eou_detection' span when running EOU detection (in runEOUDetection method) export class AudioRecognition { private hooks: RecognitionHooks; private stt?: STTNode; diff --git a/agents/src/voice/generation.ts b/agents/src/voice/generation.ts index 966de8802..ca0a96b2c 100644 --- a/agents/src/voice/generation.ts +++ b/agents/src/voice/generation.ts @@ -377,6 +377,7 @@ export function updateInstructions(options: { } } +// TODO(brian): PR3 - Add @tracer.startActiveSpan('llm_node') decorator/wrapper export function performLLMInference( node: LLMNode, chatCtx: ChatContext, @@ -467,6 +468,7 @@ export function performLLMInference( ]; } +// TODO(brian): PR3 - Add @tracer.startActiveSpan('tts_node') decorator/wrapper export function performTTSInference( node: TTSNode, text: ReadableStream, @@ -650,6 +652,7 @@ export function performAudioForwarding( ]; } +// TODO(brian): PR3 - Add @tracer.startActiveSpan('function_tool') wrapper for each tool execution export function performToolExecutions({ session, speechHandle, diff --git a/agents/src/voice/report.ts b/agents/src/voice/report.ts index 1580df29e..375b0def7 100644 --- a/agents/src/voice/report.ts +++ b/agents/src/voice/report.ts @@ -40,6 +40,11 @@ export function createSessionReport(opts: SessionReportOptions): SessionReport { }; } +// TODO(brian): PR5 - Add uploadSessionReport() function that creates multipart form with: +// - header: protobuf MetricsRecordingHeader (room_id, duration, start_time) +// - chat_history: JSON serialized chat history (use sessionReportToJSON) +// - audio: audio recording file if available (ogg format) +// - Uploads to LiveKit Cloud observability endpoint with JWT auth export function sessionReportToJSON(report: SessionReport): Record { const events: Record[] = []; diff --git a/examples/package.json b/examples/package.json index 153c76fb7..63c5ea8fe 100644 --- a/examples/package.json +++ b/examples/package.json @@ -36,6 +36,12 @@ "@livekit/agents-plugin-silero": "workspace:*", "@livekit/noise-cancellation-node": "^0.1.9", "@livekit/rtc-node": "^0.13.11", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.54.0", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/sdk-trace-base": "^1.28.0", + "@opentelemetry/sdk-trace-node": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "livekit-server-sdk": "^2.13.3", "ws": "^8.18.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ff5271e8..5e2e207f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,24 @@ importers: '@livekit/typed-emitter': specifier: ^3.0.0 version: 3.0.0 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.54.0 + version: 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.38.0 '@types/pidusage': specifier: ^2.0.5 version: 2.0.5 @@ -218,6 +236,24 @@ importers: '@livekit/rtc-node': specifier: ^0.13.11 version: 0.13.13 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.54.0 + version: 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.28.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.38.0 livekit-server-sdk: specifier: ^2.13.3 version: 2.13.3 @@ -1634,6 +1670,116 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.54.2': + resolution: {integrity: sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.27.0': + resolution: {integrity: sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.54.2': + resolution: {integrity: sha512-BgWKKyD/h2zpISdmYHN/sapwTjvt1P4p5yx4xeBV8XAEqh4OQUhOtSGFG80+nPQ1F8of3mKOT1DDoDbJp1u25w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.54.2': + resolution: {integrity: sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.54.2': + resolution: {integrity: sha512-2tIjahJlMRRUz0A2SeE+qBkeBXBFkSjR0wqJ08kuOqaL8HNGan5iZf+A8cfrfmZzPUuMKCyY9I+okzFuFs6gKQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.27.0': + resolution: {integrity: sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.54.2': + resolution: {integrity: sha512-yIbYqDLS/AtBbPjCjh6eSToGNRMqW2VR8RrKEy+G+J7dFG7pKoptTH5T+XlKPleP9NY8JZYIpgJBlI+Osi0rFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.27.0': + resolution: {integrity: sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.27.0': + resolution: {integrity: sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.27.0': + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.38.0': + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5447,6 +5593,117 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@opentelemetry/api-logs@0.54.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-trace-otlp-http@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + protobufjs: 7.4.0 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.2 + + '@opentelemetry/semantic-conventions@1.27.0': {} + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.38.0': {} + '@pkgjs/parseargs@0.11.0': optional: true