@livekit/agents 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/inference/api_protos.cjs +2 -2
- package/dist/inference/api_protos.cjs.map +1 -1
- package/dist/inference/api_protos.d.cts +16 -16
- package/dist/inference/api_protos.d.ts +16 -16
- package/dist/inference/api_protos.js +2 -2
- package/dist/inference/api_protos.js.map +1 -1
- package/dist/inference/stt.cjs +42 -30
- package/dist/inference/stt.cjs.map +1 -1
- package/dist/inference/stt.d.ts.map +1 -1
- package/dist/inference/stt.js +42 -30
- package/dist/inference/stt.js.map +1 -1
- package/dist/inference/tts.cjs +2 -3
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js +2 -3
- package/dist/inference/tts.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +35 -1
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +13 -1
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/job.cjs +52 -6
- package/dist/job.cjs.map +1 -1
- package/dist/job.d.cts +2 -0
- package/dist/job.d.ts +2 -0
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +52 -6
- package/dist/job.js.map +1 -1
- package/dist/llm/llm.cjs +38 -3
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.d.cts +1 -0
- package/dist/llm/llm.d.ts +1 -0
- package/dist/llm/llm.d.ts.map +1 -1
- package/dist/llm/llm.js +38 -3
- package/dist/llm/llm.js.map +1 -1
- package/dist/log.cjs +34 -10
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +7 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +34 -11
- package/dist/log.js.map +1 -1
- package/dist/stt/stt.cjs +18 -5
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +18 -5
- package/dist/stt/stt.js.map +1 -1
- package/dist/telemetry/index.cjs +23 -2
- package/dist/telemetry/index.cjs.map +1 -1
- package/dist/telemetry/index.d.cts +4 -1
- package/dist/telemetry/index.d.ts +4 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +27 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/logging.cjs +65 -0
- package/dist/telemetry/logging.cjs.map +1 -0
- package/dist/telemetry/logging.d.cts +21 -0
- package/dist/telemetry/logging.d.ts +21 -0
- package/dist/telemetry/logging.d.ts.map +1 -0
- package/dist/telemetry/logging.js +40 -0
- package/dist/telemetry/logging.js.map +1 -0
- package/dist/telemetry/otel_http_exporter.cjs +144 -0
- package/dist/telemetry/otel_http_exporter.cjs.map +1 -0
- package/dist/telemetry/otel_http_exporter.d.cts +62 -0
- package/dist/telemetry/otel_http_exporter.d.ts +62 -0
- package/dist/telemetry/otel_http_exporter.d.ts.map +1 -0
- package/dist/telemetry/otel_http_exporter.js +120 -0
- package/dist/telemetry/otel_http_exporter.js.map +1 -0
- package/dist/telemetry/pino_otel_transport.cjs +217 -0
- package/dist/telemetry/pino_otel_transport.cjs.map +1 -0
- package/dist/telemetry/pino_otel_transport.d.cts +58 -0
- package/dist/telemetry/pino_otel_transport.d.ts +58 -0
- package/dist/telemetry/pino_otel_transport.d.ts.map +1 -0
- package/dist/telemetry/pino_otel_transport.js +189 -0
- package/dist/telemetry/pino_otel_transport.js.map +1 -0
- package/dist/telemetry/traces.cjs +225 -16
- package/dist/telemetry/traces.cjs.map +1 -1
- package/dist/telemetry/traces.d.cts +17 -0
- package/dist/telemetry/traces.d.ts +17 -0
- package/dist/telemetry/traces.d.ts.map +1 -1
- package/dist/telemetry/traces.js +211 -14
- package/dist/telemetry/traces.js.map +1 -1
- package/dist/tts/tts.cjs +68 -20
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.d.cts +2 -0
- package/dist/tts/tts.d.ts +2 -0
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +68 -20
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.cjs +6 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +5 -0
- package/dist/utils.js.map +1 -1
- package/dist/voice/agent_activity.cjs +93 -7
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +3 -0
- package/dist/voice/agent_activity.d.ts +3 -0
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +93 -7
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +122 -27
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +15 -0
- package/dist/voice/agent_session.d.ts +15 -0
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +122 -27
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +69 -22
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.cts +5 -0
- package/dist/voice/audio_recognition.d.ts +5 -0
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +69 -22
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/generation.cjs +43 -3
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +43 -3
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/report.cjs +3 -2
- package/dist/voice/report.cjs.map +1 -1
- package/dist/voice/report.d.cts +7 -1
- package/dist/voice/report.d.ts +7 -1
- package/dist/voice/report.d.ts.map +1 -1
- package/dist/voice/report.js +3 -2
- package/dist/voice/report.js.map +1 -1
- package/package.json +8 -2
- package/src/inference/api_protos.ts +2 -2
- package/src/inference/stt.ts +48 -33
- package/src/inference/tts.ts +4 -3
- package/src/ipc/job_proc_lazy_main.ts +12 -1
- package/src/job.ts +59 -10
- package/src/llm/llm.ts +48 -5
- package/src/log.ts +52 -15
- package/src/stt/stt.ts +18 -5
- package/src/telemetry/index.ts +22 -4
- package/src/telemetry/logging.ts +55 -0
- package/src/telemetry/otel_http_exporter.ts +191 -0
- package/src/telemetry/pino_otel_transport.ts +265 -0
- package/src/telemetry/traces.ts +320 -20
- package/src/tts/tts.ts +85 -24
- package/src/utils.ts +5 -0
- package/src/voice/agent_activity.ts +140 -22
- package/src/voice/agent_session.ts +174 -34
- package/src/voice/audio_recognition.ts +85 -26
- package/src/voice/generation.ts +59 -7
- package/src/voice/report.ts +10 -4
package/src/job.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
14
14
|
import type { Logger } from 'pino';
|
|
15
15
|
import type { InferenceExecutor } from './ipc/inference_executor.js';
|
|
16
16
|
import { log } from './log.js';
|
|
17
|
+
import { flushOtelLogs, setupCloudTracer, uploadSessionReport } from './telemetry/index.js';
|
|
18
|
+
import { isCloud } from './utils.js';
|
|
17
19
|
import type { AgentSession } from './voice/agent_session.js';
|
|
18
20
|
import { type SessionReport, createSessionReport } from './voice/report.js';
|
|
19
21
|
|
|
@@ -81,8 +83,6 @@ export class FunctionExistsError extends Error {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/** The job and environment context as seen by the agent, accessible by the entrypoint function. */
|
|
84
|
-
// TODO(brian): PR3 - Add @tracer.startActiveSpan('job_entrypoint') wrapper in entrypoint
|
|
85
|
-
// TODO(brian): PR5 - Add uploadSessionReport() call in cleanup/session end
|
|
86
86
|
export class JobContext {
|
|
87
87
|
#proc: JobProcess;
|
|
88
88
|
#info: RunningJobInfo;
|
|
@@ -142,6 +142,10 @@ export class JobContext {
|
|
|
142
142
|
return this.#room;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
get info(): RunningJobInfo {
|
|
146
|
+
return this.#info;
|
|
147
|
+
}
|
|
148
|
+
|
|
145
149
|
/** @returns The agent's participant if connected to the room, otherwise `undefined` */
|
|
146
150
|
get agent(): LocalParticipant | undefined {
|
|
147
151
|
return this.#room.localParticipant;
|
|
@@ -247,7 +251,6 @@ export class JobContext {
|
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
// TODO(brian): implement and check recorder io
|
|
250
|
-
// TODO(brian): PR5 - Ensure chat history serialization includes all required fields (use sessionReportToJSON helper)
|
|
251
254
|
|
|
252
255
|
return createSessionReport({
|
|
253
256
|
jobId: this.job.id,
|
|
@@ -257,6 +260,7 @@ export class JobContext {
|
|
|
257
260
|
events: targetSession._recordedEvents,
|
|
258
261
|
enableUserDataTraining: true,
|
|
259
262
|
chatHistory: targetSession.history.copy(),
|
|
263
|
+
startedAt: targetSession._startedAt,
|
|
260
264
|
});
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -270,14 +274,45 @@ export class JobContext {
|
|
|
270
274
|
|
|
271
275
|
// TODO(brian): Implement CLI/console
|
|
272
276
|
|
|
273
|
-
//
|
|
274
|
-
|
|
277
|
+
// Upload session report to LiveKit Cloud if enabled
|
|
278
|
+
const url = new URL(this.#info.url);
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
if (report.enableRecording && isCloud(url)) {
|
|
281
|
+
try {
|
|
282
|
+
await uploadSessionReport({
|
|
283
|
+
agentName: this.job.agentName,
|
|
284
|
+
cloudHostname: url.hostname,
|
|
285
|
+
report,
|
|
286
|
+
});
|
|
287
|
+
this.#logger.info(
|
|
288
|
+
{
|
|
289
|
+
jobId: report.jobId,
|
|
290
|
+
roomId: report.roomId,
|
|
291
|
+
},
|
|
292
|
+
'Session report uploaded to LiveKit Cloud',
|
|
293
|
+
);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
this.#logger.error({ error }, 'Failed to upload session report');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.#logger.debug(
|
|
300
|
+
{
|
|
301
|
+
jobId: report.jobId,
|
|
302
|
+
roomId: report.roomId,
|
|
303
|
+
eventsCount: report.events.length,
|
|
304
|
+
},
|
|
305
|
+
'Session ended, report generated',
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Explicitly clear the recorded events to avoid leaking memory
|
|
309
|
+
session._recordedEvents = [];
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
await flushOtelLogs();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.#logger.error({ error }, 'Failed to flush OTEL logs');
|
|
315
|
+
}
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
/**
|
|
@@ -316,6 +351,20 @@ export class JobContext {
|
|
|
316
351
|
|
|
317
352
|
this.#participantEntrypoints.push(callback);
|
|
318
353
|
}
|
|
354
|
+
|
|
355
|
+
async initRecording() {
|
|
356
|
+
const url = new URL(this.#info.url);
|
|
357
|
+
if (!isCloud(url)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.#logger.debug({ hostname: url.hostname }, 'Configuring session recording (cloud tracer)');
|
|
362
|
+
await setupCloudTracer({
|
|
363
|
+
roomId: this.job.room!.sid,
|
|
364
|
+
jobId: this.job.id,
|
|
365
|
+
cloudHostname: url.hostname,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
319
368
|
}
|
|
320
369
|
|
|
321
370
|
export class JobProcess {
|
package/src/llm/llm.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
|
|
5
|
+
import type { Span } from '@opentelemetry/api';
|
|
5
6
|
import { EventEmitter } from 'node:events';
|
|
6
7
|
import { APIConnectionError, APIError } from '../_exceptions.js';
|
|
7
8
|
import { log } from '../log.js';
|
|
8
9
|
import type { LLMMetrics } from '../metrics/base.js';
|
|
10
|
+
import { recordException, traceTypes, tracer } from '../telemetry/index.js';
|
|
9
11
|
import type { APIConnectOptions } from '../types.js';
|
|
10
12
|
import { AsyncIterableQueue, delay, startSoon, toError } from '../utils.js';
|
|
11
13
|
import { type ChatContext, type ChatRole, type FunctionCall } from './chat_context.js';
|
|
@@ -104,6 +106,7 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
104
106
|
#llm: LLM;
|
|
105
107
|
#chatCtx: ChatContext;
|
|
106
108
|
#toolCtx?: ToolContext;
|
|
109
|
+
#llmRequestSpan?: Span;
|
|
107
110
|
|
|
108
111
|
constructor(
|
|
109
112
|
llm: LLM,
|
|
@@ -135,12 +138,24 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
135
138
|
startSoon(() => this.mainTask().then(() => this.queue.close()));
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
private async
|
|
139
|
-
|
|
141
|
+
private _mainTaskImpl = async (span: Span) => {
|
|
142
|
+
this.#llmRequestSpan = span;
|
|
143
|
+
span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.#llm.model);
|
|
144
|
+
|
|
140
145
|
for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
|
|
141
146
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
return await tracer.startActiveSpan(
|
|
148
|
+
async (attemptSpan) => {
|
|
149
|
+
attemptSpan.setAttribute(traceTypes.ATTR_RETRY_COUNT, i);
|
|
150
|
+
try {
|
|
151
|
+
return await this.run();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
recordException(attemptSpan, toError(error));
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{ name: 'llm_request_run' },
|
|
158
|
+
);
|
|
144
159
|
} catch (error) {
|
|
145
160
|
if (error instanceof APIError) {
|
|
146
161
|
const retryInterval = this._connOptions._intervalForRetry(i);
|
|
@@ -171,7 +186,13 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
171
186
|
}
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
|
-
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
private mainTask = async () =>
|
|
192
|
+
tracer.startActiveSpan(async (span) => this._mainTaskImpl(span), {
|
|
193
|
+
name: 'llm_request',
|
|
194
|
+
endOnExit: false,
|
|
195
|
+
});
|
|
175
196
|
|
|
176
197
|
private emitError({ error, recoverable }: { error: Error; recoverable: boolean }) {
|
|
177
198
|
this.#llm.emit('error', {
|
|
@@ -188,6 +209,7 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
188
209
|
let ttft: bigint = BigInt(-1);
|
|
189
210
|
let requestId = '';
|
|
190
211
|
let usage: CompletionUsage | undefined;
|
|
212
|
+
let completionStartTime: string | undefined;
|
|
191
213
|
|
|
192
214
|
for await (const ev of this.queue) {
|
|
193
215
|
if (this.abortController.signal.aborted) {
|
|
@@ -197,6 +219,7 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
197
219
|
requestId = ev.id;
|
|
198
220
|
if (ttft === BigInt(-1)) {
|
|
199
221
|
ttft = process.hrtime.bigint() - startTime;
|
|
222
|
+
completionStartTime = new Date().toISOString();
|
|
200
223
|
}
|
|
201
224
|
if (ev.usage) {
|
|
202
225
|
usage = ev.usage;
|
|
@@ -225,6 +248,26 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
225
248
|
return (usage?.completionTokens || 0) / (durationMs / 1000);
|
|
226
249
|
})(),
|
|
227
250
|
};
|
|
251
|
+
|
|
252
|
+
if (this.#llmRequestSpan) {
|
|
253
|
+
this.#llmRequestSpan.setAttribute(traceTypes.ATTR_LLM_METRICS, JSON.stringify(metrics));
|
|
254
|
+
|
|
255
|
+
this.#llmRequestSpan.setAttributes({
|
|
256
|
+
[traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.promptTokens,
|
|
257
|
+
[traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.completionTokens,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (completionStartTime) {
|
|
261
|
+
this.#llmRequestSpan.setAttribute(
|
|
262
|
+
traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME,
|
|
263
|
+
completionStartTime,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// End the span now that metrics are collected
|
|
268
|
+
this.#llmRequestSpan.end();
|
|
269
|
+
}
|
|
270
|
+
|
|
228
271
|
this.#llm.emit('metrics_collected', metrics);
|
|
229
272
|
}
|
|
230
273
|
|
package/src/log.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { Writable } from 'node:stream';
|
|
5
|
+
import type { DestinationStream, Logger } from 'pino';
|
|
6
|
+
import { multistream, pino } from 'pino';
|
|
7
|
+
import { build as pinoPretty } from 'pino-pretty';
|
|
8
|
+
import { type PinoLogObject, emitToOtel } from './telemetry/pino_otel_transport.js';
|
|
6
9
|
|
|
7
10
|
/** @internal */
|
|
8
11
|
export type LoggerOptions = {
|
|
@@ -16,6 +19,9 @@ export let loggerOptions: LoggerOptions;
|
|
|
16
19
|
/** @internal */
|
|
17
20
|
let logger: Logger | undefined = undefined;
|
|
18
21
|
|
|
22
|
+
/** @internal */
|
|
23
|
+
let otelEnabled = false;
|
|
24
|
+
|
|
19
25
|
/** @internal */
|
|
20
26
|
export const log = () => {
|
|
21
27
|
if (!logger) {
|
|
@@ -28,19 +34,50 @@ export const log = () => {
|
|
|
28
34
|
export const initializeLogger = ({ pretty, level }: LoggerOptions) => {
|
|
29
35
|
loggerOptions = { pretty, level };
|
|
30
36
|
logger = pino(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
transport: {
|
|
34
|
-
target: 'pino-pretty',
|
|
35
|
-
options: {
|
|
36
|
-
colorize: true,
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
: {},
|
|
37
|
+
{ level: level || 'info' },
|
|
38
|
+
pretty ? pinoPretty({ colorize: true }) : process.stdout,
|
|
41
39
|
);
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom Pino destination that parses JSON logs and emits to OTEL.
|
|
44
|
+
* This receives the FULL serialized log including msg, level, time, etc.
|
|
45
|
+
*/
|
|
46
|
+
class OtelDestination extends Writable {
|
|
47
|
+
_write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void {
|
|
48
|
+
try {
|
|
49
|
+
const line = chunk.toString().trim();
|
|
50
|
+
if (line) {
|
|
51
|
+
const logObj = JSON.parse(line) as PinoLogObject;
|
|
52
|
+
emitToOtel(logObj);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore parse errors (e.g., non-JSON lines)
|
|
56
|
+
}
|
|
57
|
+
callback();
|
|
44
58
|
}
|
|
45
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Enable OTEL logging by reconfiguring the logger with multistream.
|
|
63
|
+
* Uses a custom destination that receives full JSON logs (with msg, level, time).
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export const enableOtelLogging = () => {
|
|
68
|
+
if (otelEnabled || !logger) {
|
|
69
|
+
console.warn('OTEL logging already enabled or logger not initialized');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
otelEnabled = true;
|
|
73
|
+
|
|
74
|
+
const { pretty, level } = loggerOptions;
|
|
75
|
+
|
|
76
|
+
const logLevel = level || 'info';
|
|
77
|
+
const streams: { stream: DestinationStream; level: string }[] = [
|
|
78
|
+
{ stream: pretty ? pinoPretty({ colorize: true }) : process.stdout, level: logLevel },
|
|
79
|
+
{ stream: new OtelDestination(), level: 'debug' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
logger = pino({ level: logLevel }, multistream(streams));
|
|
46
83
|
};
|
package/src/stt/stt.ts
CHANGED
|
@@ -257,7 +257,18 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent>
|
|
|
257
257
|
|
|
258
258
|
protected async monitorMetrics() {
|
|
259
259
|
for await (const event of this.queue) {
|
|
260
|
-
this.output.
|
|
260
|
+
if (!this.output.closed) {
|
|
261
|
+
try {
|
|
262
|
+
this.output.put(event);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
if (e instanceof Error && e.message.includes('Queue is closed')) {
|
|
265
|
+
this.logger.warn(
|
|
266
|
+
{ err: e },
|
|
267
|
+
'Queue closed during transcript processing (expected during disconnect)',
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
261
272
|
if (event.type !== SpeechEventType.RECOGNITION_USAGE) continue;
|
|
262
273
|
const metrics: STTMetrics = {
|
|
263
274
|
type: 'stt_metrics',
|
|
@@ -270,7 +281,9 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent>
|
|
|
270
281
|
};
|
|
271
282
|
this.#stt.emit('metrics_collected', metrics);
|
|
272
283
|
}
|
|
273
|
-
this.output.
|
|
284
|
+
if (!this.output.closed) {
|
|
285
|
+
this.output.close();
|
|
286
|
+
}
|
|
274
287
|
}
|
|
275
288
|
|
|
276
289
|
protected abstract run(): Promise<void>;
|
|
@@ -336,9 +349,9 @@ export abstract class SpeechStream implements AsyncIterableIterator<SpeechEvent>
|
|
|
336
349
|
|
|
337
350
|
/** Close both the input and output of the STT stream */
|
|
338
351
|
close() {
|
|
339
|
-
this.input.close();
|
|
340
|
-
this.queue.close();
|
|
341
|
-
this.output.close();
|
|
352
|
+
if (!this.input.closed) this.input.close();
|
|
353
|
+
if (!this.queue.closed) this.queue.close();
|
|
354
|
+
if (!this.output.closed) this.output.close();
|
|
342
355
|
this.closed = true;
|
|
343
356
|
}
|
|
344
357
|
|
package/src/telemetry/index.ts
CHANGED
|
@@ -2,9 +2,27 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export { ExtraDetailsProcessor, MetadataLogProcessor } from './logging.js';
|
|
6
|
+
export {
|
|
7
|
+
SimpleOTLPHttpLogExporter,
|
|
8
|
+
type SimpleLogRecord,
|
|
9
|
+
type SimpleOTLPHttpLogExporterConfig,
|
|
10
|
+
} from './otel_http_exporter.js';
|
|
11
|
+
export {
|
|
12
|
+
emitToOtel,
|
|
13
|
+
flushPinoLogs,
|
|
14
|
+
initPinoCloudExporter,
|
|
15
|
+
PinoCloudExporter,
|
|
16
|
+
type PinoCloudExporterConfig,
|
|
17
|
+
type PinoLogObject,
|
|
18
|
+
} from './pino_otel_transport.js';
|
|
8
19
|
export * as traceTypes from './trace_types.js';
|
|
9
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
flushOtelLogs,
|
|
22
|
+
setTracerProvider,
|
|
23
|
+
setupCloudTracer,
|
|
24
|
+
tracer,
|
|
25
|
+
uploadSessionReport,
|
|
26
|
+
type StartSpanOptions,
|
|
27
|
+
} from './traces.js';
|
|
10
28
|
export { recordException, recordRealtimeMetrics } from './utils.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
5
|
+
import type { LogRecord, LogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Metadata log processor that injects metadata (room_id, job_id) into all log records.
|
|
9
|
+
*/
|
|
10
|
+
export class MetadataLogProcessor implements LogRecordProcessor {
|
|
11
|
+
private metadata: Attributes;
|
|
12
|
+
|
|
13
|
+
constructor(metadata: Attributes) {
|
|
14
|
+
this.metadata = metadata;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onEmit(logRecord: LogRecord): void {
|
|
18
|
+
// Add metadata to log record attributes
|
|
19
|
+
if (logRecord.attributes) {
|
|
20
|
+
Object.assign(logRecord.attributes, this.metadata);
|
|
21
|
+
} else {
|
|
22
|
+
(logRecord as any).attributes = { ...this.metadata };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
shutdown(): Promise<void> {
|
|
27
|
+
return Promise.resolve();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
forceFlush(): Promise<void> {
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extra details processor that adds logger name to log records.
|
|
37
|
+
*/
|
|
38
|
+
export class ExtraDetailsProcessor implements LogRecordProcessor {
|
|
39
|
+
onEmit(logRecord: LogRecord): void {
|
|
40
|
+
const loggerName = logRecord.instrumentationScope.name;
|
|
41
|
+
if (logRecord.attributes) {
|
|
42
|
+
(logRecord.attributes as any)['logger.name'] = loggerName;
|
|
43
|
+
} else {
|
|
44
|
+
(logRecord as any).attributes = { 'logger.name': loggerName };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
shutdown(): Promise<void> {
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
forceFlush(): Promise<void> {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OTLP HTTP JSON Log Exporter for LiveKit Cloud
|
|
7
|
+
*
|
|
8
|
+
* This module provides a custom OTLP log exporter that uses HTTP with JSON format
|
|
9
|
+
* instead of the default protobuf format.
|
|
10
|
+
*/
|
|
11
|
+
import { SeverityNumber } from '@opentelemetry/api-logs';
|
|
12
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
13
|
+
|
|
14
|
+
export interface SimpleLogRecord {
|
|
15
|
+
/** Log message body */
|
|
16
|
+
body: string;
|
|
17
|
+
/** Timestamp in milliseconds since epoch */
|
|
18
|
+
timestampMs: number;
|
|
19
|
+
/** Log attributes */
|
|
20
|
+
attributes: Record<string, unknown>;
|
|
21
|
+
/** Severity number (default: UNSPECIFIED) */
|
|
22
|
+
severityNumber?: SeverityNumber;
|
|
23
|
+
/** Severity text (default: 'unspecified') */
|
|
24
|
+
severityText?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SimpleOTLPHttpLogExporterConfig {
|
|
28
|
+
/** LiveKit Cloud hostname */
|
|
29
|
+
cloudHostname: string;
|
|
30
|
+
/** Resource attributes (e.g., room_id, job_id) */
|
|
31
|
+
resourceAttributes: Record<string, string>;
|
|
32
|
+
/** Scope name for the logger */
|
|
33
|
+
scopeName: string;
|
|
34
|
+
/** Scope attributes */
|
|
35
|
+
scopeAttributes?: Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Simple OTLP HTTP Log Exporter for direct log export
|
|
40
|
+
*
|
|
41
|
+
* This is a simplified exporter that doesn't require the full SDK infrastructure.
|
|
42
|
+
* Use this when you need to send logs directly without LoggerProvider.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const exporter = new SimpleOTLPHttpLogExporter({
|
|
47
|
+
* cloudHostname: 'cloud.livekit.io',
|
|
48
|
+
* resourceAttributes: { room_id: 'xxx', job_id: 'yyy' },
|
|
49
|
+
* scopeName: 'chat_history',
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* await exporter.export([
|
|
53
|
+
* { body: 'Hello', timestampMs: Date.now(), attributes: { test: true } },
|
|
54
|
+
* ]);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export class SimpleOTLPHttpLogExporter {
|
|
58
|
+
private readonly config: SimpleOTLPHttpLogExporterConfig;
|
|
59
|
+
private jwt: string | null = null;
|
|
60
|
+
|
|
61
|
+
constructor(config: SimpleOTLPHttpLogExporterConfig) {
|
|
62
|
+
this.config = config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Export simple log records
|
|
67
|
+
*/
|
|
68
|
+
async export(records: SimpleLogRecord[]): Promise<void> {
|
|
69
|
+
if (records.length === 0) return;
|
|
70
|
+
|
|
71
|
+
await this.ensureJwt();
|
|
72
|
+
|
|
73
|
+
const endpoint = `https://${this.config.cloudHostname}/observability/logs/otlp/v0`;
|
|
74
|
+
const payload = this.buildPayload(records);
|
|
75
|
+
|
|
76
|
+
const response = await fetch(endpoint, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${this.jwt}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(payload),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
throw new Error(
|
|
88
|
+
`OTLP log export failed: ${response.status} ${response.statusText} - ${text}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async ensureJwt(): Promise<void> {
|
|
94
|
+
if (this.jwt) return;
|
|
95
|
+
|
|
96
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
97
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
98
|
+
|
|
99
|
+
if (!apiKey || !apiSecret) {
|
|
100
|
+
throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });
|
|
104
|
+
token.addObservabilityGrant({ write: true });
|
|
105
|
+
this.jwt = await token.toJwt();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private buildPayload(records: SimpleLogRecord[]): object {
|
|
109
|
+
const resourceAttrs = Object.entries(this.config.resourceAttributes).map(([key, value]) => ({
|
|
110
|
+
key,
|
|
111
|
+
value: { stringValue: value },
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (!this.config.resourceAttributes['service.name']) {
|
|
115
|
+
resourceAttrs.push({ key: 'service.name', value: { stringValue: 'livekit-agents' } });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const scopeAttrs = this.config.scopeAttributes
|
|
119
|
+
? Object.entries(this.config.scopeAttributes).map(([key, value]) => ({
|
|
120
|
+
key,
|
|
121
|
+
value: { stringValue: value },
|
|
122
|
+
}))
|
|
123
|
+
: [];
|
|
124
|
+
|
|
125
|
+
const logRecords = records.map((record) => ({
|
|
126
|
+
timeUnixNano: String(BigInt(Math.floor(record.timestampMs * 1_000_000))),
|
|
127
|
+
observedTimeUnixNano: String(BigInt(Date.now()) * BigInt(1_000_000)),
|
|
128
|
+
severityNumber: record.severityNumber ?? SeverityNumber.UNSPECIFIED,
|
|
129
|
+
severityText: record.severityText ?? 'unspecified',
|
|
130
|
+
body: { stringValue: record.body },
|
|
131
|
+
attributes: this.convertAttributes(record.attributes),
|
|
132
|
+
traceId: '',
|
|
133
|
+
spanId: '',
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
resourceLogs: [
|
|
138
|
+
{
|
|
139
|
+
resource: { attributes: resourceAttrs },
|
|
140
|
+
scopeLogs: [
|
|
141
|
+
{
|
|
142
|
+
scope: {
|
|
143
|
+
name: this.config.scopeName,
|
|
144
|
+
attributes: scopeAttrs,
|
|
145
|
+
},
|
|
146
|
+
logRecords,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private convertAttributes(
|
|
155
|
+
attrs: Record<string, unknown>,
|
|
156
|
+
): Array<{ key: string; value: unknown }> {
|
|
157
|
+
return Object.entries(attrs).map(([key, value]) => ({
|
|
158
|
+
key,
|
|
159
|
+
value: this.convertValue(value),
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private convertValue(value: unknown): unknown {
|
|
164
|
+
if (value === null || value === undefined) {
|
|
165
|
+
return { stringValue: '' };
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
return { stringValue: value };
|
|
169
|
+
}
|
|
170
|
+
if (typeof value === 'number') {
|
|
171
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === 'boolean') {
|
|
174
|
+
return { boolValue: value };
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
return { arrayValue: { values: value.map((v) => this.convertValue(v)) } };
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === 'object') {
|
|
180
|
+
return {
|
|
181
|
+
kvlistValue: {
|
|
182
|
+
values: Object.entries(value as Record<string, unknown>).map(([k, v]) => ({
|
|
183
|
+
key: k,
|
|
184
|
+
value: this.convertValue(v),
|
|
185
|
+
})),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { stringValue: String(value) };
|
|
190
|
+
}
|
|
191
|
+
}
|