From c783eeaeea2841bee60ff288a557819b1544f3f9 Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Thu, 23 Jul 2026 19:33:36 +0100 Subject: [PATCH 1/6] feat: add pii-redactable telemetry --- .changeset/redact-pii-telemetry.md | 5 + agents/src/beta/tools/end_call.ts | 6 +- agents/src/bin/livekit-agents.ts | 2 +- agents/src/cli.ts | 16 +-- agents/src/cli_client.ts | 4 +- agents/src/console.ts | 4 +- agents/src/download.ts | 19 ++- agents/src/http_server.ts | 2 +- agents/src/inference/_warmup.ts | 2 +- agents/src/inference/eot/detector.ts | 4 +- agents/src/inference/eot/runner.ts | 5 +- agents/src/inference/eot/transports.ts | 7 +- .../interruption/interruption_detector.ts | 2 +- .../inference/interruption/ws_transport.ts | 12 +- agents/src/inference/stt.ts | 18 ++- agents/src/inference/tts.ts | 13 +- agents/src/inference/vad.ts | 5 +- agents/src/ipc/job_proc_lazy_main.ts | 18 +-- agents/src/ipc/supervised_proc.ts | 16 +-- agents/src/job.ts | 11 +- agents/src/llm/fallback_adapter.ts | 19 ++- agents/src/llm/llm.ts | 9 +- agents/src/llm/realtime.ts | 6 +- agents/src/log.test.ts | 42 ++++++ agents/src/log.ts | 8 +- agents/src/stream/multi_input_stream.ts | 2 +- agents/src/stt/fallback_adapter.ts | 43 +++--- agents/src/stt/stream_adapter.ts | 2 +- agents/src/stt/stt.ts | 11 +- agents/src/telemetry/index.ts | 7 +- agents/src/telemetry/trace_types.test.ts | 125 ++++++++++++++++++ agents/src/telemetry/trace_types.ts | 30 +++-- agents/src/telemetry/utils.test.ts | 116 ++++++++++++++++ agents/src/telemetry/utils.ts | 32 ++++- agents/src/tts/fallback_adapter.ts | 17 ++- agents/src/tts/tts.ts | 18 ++- agents/src/utils.ts | 6 +- agents/src/vad.ts | 2 +- agents/src/voice/agent.ts | 9 +- agents/src/voice/agent_activity.ts | 76 ++++++----- agents/src/voice/agent_session.ts | 15 ++- agents/src/voice/amd.ts | 30 +++-- agents/src/voice/audio_recognition.ts | 52 +++++--- .../src/voice/audio_recognition_span.test.ts | 4 +- .../src/voice/avatar/avatar_session.test.ts | 2 +- agents/src/voice/avatar/avatar_session.ts | 4 +- agents/src/voice/background_audio.ts | 2 +- agents/src/voice/generation.ts | 28 ++-- agents/src/voice/keyterm_detection.ts | 20 ++- agents/src/voice/recorder_io/recorder_io.ts | 18 ++- agents/src/voice/remote_session.ts | 20 +-- agents/src/voice/report.ts | 2 +- agents/src/voice/room_io/_output.ts | 6 +- agents/src/voice/room_io/room_io.ts | 10 +- agents/src/voice/tool_executor.ts | 8 +- .../src/voice/transcription/synchronizer.ts | 2 +- agents/src/worker.ts | 30 +++-- agents/src/workflows/warm_transfer.ts | 17 ++- examples/src/frontdesk/calendar_api.test.ts | 33 +++-- examples/src/frontdesk/calendar_api.ts | 15 ++- examples/src/idle_user_timeout_example.ts | 2 +- examples/src/instructions_per_modality.ts | 5 +- examples/src/liveavatar_avatar.ts | 4 +- examples/src/play_local_audio_file.ts | 9 +- examples/src/realtime_streaming_transcript.ts | 4 +- examples/src/realtime_with_tts.ts | 2 +- examples/src/telephony_amd.ts | 2 +- examples/src/warm_transfer.ts | 7 +- plugins/anam/src/api.ts | 18 +-- plugins/anam/src/avatar.ts | 12 +- plugins/assemblyai/src/stt.ts | 43 +++++- plugins/azure/src/stt.ts | 4 +- plugins/baseten/src/stt.ts | 25 +++- plugins/bey/src/avatar.ts | 4 +- plugins/cartesia/src/stt.ts | 23 ++-- plugins/cartesia/src/tts.ts | 43 ++++-- plugins/deepgram/src/stt.ts | 32 ++++- plugins/deepgram/src/stt_v2.ts | 16 ++- plugins/deepgram/src/tts.ts | 13 +- plugins/did/src/api.ts | 4 +- plugins/elevenlabs/src/stt.ts | 34 ++++- plugins/elevenlabs/src/tts.ts | 21 ++- plugins/fishaudio/src/tts.ts | 13 +- plugins/google/src/llm.ts | 16 ++- plugins/google/src/realtime/realtime_api.ts | 72 +++++++--- plugins/inworld/src/stt.ts | 23 +++- plugins/inworld/src/tts.ts | 16 ++- plugins/krisp/src/_krisp.ts | 21 +-- plugins/lemonslice/src/avatar.ts | 10 +- plugins/lemonslice/src/meeting/audio.ts | 4 +- plugins/lemonslice/src/meeting/chat.ts | 7 +- plugins/liveavatar/src/api.ts | 4 +- plugins/liveavatar/src/avatar.ts | 8 +- plugins/livekit/src/hf_utils.ts | 4 +- plugins/livekit/src/turn_detector/base.ts | 18 ++- .../livekit/src/turn_detector/multilingual.ts | 2 +- plugins/minimax/src/tts.ts | 12 +- plugins/neuphonic/src/tts.ts | 26 +++- plugins/openai/src/realtime/realtime_model.ts | 67 +++++++--- plugins/phonic/src/realtime/realtime_model.ts | 24 +++- plugins/protoface/src/avatar.ts | 5 +- plugins/resemble/src/tts.ts | 38 ++++-- plugins/rime/src/tts.ts | 2 +- plugins/runway/src/avatar.ts | 16 ++- plugins/sarvam/src/stt.ts | 58 ++++++-- plugins/sarvam/src/tts.ts | 2 +- plugins/soniox/src/stt.ts | 8 +- plugins/soniox/src/tts.ts | 24 +++- plugins/tavus/src/api.test.ts | 11 +- plugins/tavus/src/api.ts | 11 +- plugins/trugen/src/avatar.ts | 4 +- plugins/xai/src/stt.ts | 38 +++++- plugins/xai/src/tts.ts | 10 +- 113 files changed, 1411 insertions(+), 559 deletions(-) create mode 100644 .changeset/redact-pii-telemetry.md create mode 100644 agents/src/telemetry/trace_types.test.ts create mode 100644 agents/src/telemetry/utils.test.ts diff --git a/.changeset/redact-pii-telemetry.md b/.changeset/redact-pii-telemetry.md new file mode 100644 index 000000000..c9fda497a --- /dev/null +++ b/.changeset/redact-pii-telemetry.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents': patch +--- + +Tag content-bearing telemetry with `lk.pii.*` and redact trace exception details when job redaction is enabled. Dashboards and queries using the previous sensitive trace keys must migrate to their `lk.pii.*` replacements. diff --git a/agents/src/beta/tools/end_call.ts b/agents/src/beta/tools/end_call.ts index d444dd17e..523d82479 100644 --- a/agents/src/beta/tools/end_call.ts +++ b/agents/src/beta/tools/end_call.ts @@ -150,7 +150,9 @@ export function createEndCallTool({ jobCtx.shutdown(String(event.reason)); }) - .catch((error) => log().error({ error }, 'error during end call shutdown')); + .catch((error) => + log().error({ 'lk.pii.error': error }, 'error during end call shutdown'), + ); ctx.speechHandle.addDoneCallback(() => { if (!(llm instanceof RealtimeModel) || !llm.capabilities.autoToolReplyGeneration) { @@ -159,7 +161,7 @@ export function createEndCallTool({ } void delayedSessionShutdown(session, signal).catch((error) => - log().error({ error }, 'error during delayed session shutdown'), + log().error({ 'lk.pii.error': error }, 'error during delayed session shutdown'), ); }); diff --git a/agents/src/bin/livekit-agents.ts b/agents/src/bin/livekit-agents.ts index 0f6649f30..4c10e3a4a 100644 --- a/agents/src/bin/livekit-agents.ts +++ b/agents/src/bin/livekit-agents.ts @@ -23,6 +23,6 @@ program }); program.parseAsync(process.argv).catch((error) => { - console.error(`Fatal error: ${error}`); + console.error('Fatal error', error instanceof Error ? error.name : 'UnknownError'); process.exit(1); }); diff --git a/agents/src/cli.ts b/agents/src/cli.ts index 713550530..9e1010620 100644 --- a/agents/src/cli.ts +++ b/agents/src/cli.ts @@ -24,9 +24,6 @@ type CliArgs = { cliAddr?: string; }; -const formatErrorMessage = (error: unknown): string => - error instanceof Error ? error.message : String(error); - const runServer = async (args: CliArgs) => { initializeLogger({ pretty: !args.production, level: args.opts.logLevel }); const logger = log(); @@ -45,7 +42,7 @@ const runServer = async (args: CliArgs) => { if (args.room) { server.event.once('worker_registered', () => { - logger.info(`connecting to room ${args.room}`); + logger.info({ roomName: args.room }, 'connecting to room'); server.simulateJob(args.room!, args.participantIdentity); }); } @@ -240,7 +237,7 @@ export const runApp = (opts: ServerOptions) => { connectAddr: commandOptions.connectAddr, record: commandOptions.record === true, }).catch((error) => { - log().fatal(`console mode failed: ${formatErrorMessage(error)}`); + log().fatal({ 'lk.pii.error': error }, 'console mode failed'); process.exit(1); }); }); @@ -264,14 +261,15 @@ export const runApp = (opts: ServerOptions) => { const failures: PluginDownloadFailure[] = []; for (const plugin of Plugin.registeredPlugins) { - logger.info(`Downloading files for ${plugin.title}`); + logger.info({ plugin: plugin.title }, 'Downloading plugin files'); try { await plugin.downloadFiles(); - logger.info(`Finished downloading files for ${plugin.title}`); + logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); } catch (error) { failures.push({ plugin, error }); logger.error( - `Failed to download files for ${plugin.title}: ${formatErrorMessage(error)}`, + { 'lk.pii.error': error, plugin: plugin.title }, + 'failed to download plugin files', ); } } @@ -286,7 +284,7 @@ export const runApp = (opts: ServerOptions) => { process.exit(0); }) .catch((error) => { - logger.fatal(`Error during file downloads: ${error}`); + logger.fatal({ 'lk.pii.error': error }, 'error during file downloads'); process.exit(1); }); }); diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index 7942e4c32..0b360e786 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -61,12 +61,12 @@ export class CLIClient { // Best-effort: log and move on if the CLI isn't listening. socket.on('error', (err) => { - logger.debug(`dev channel unavailable: ${err.message}`); + logger.debug({ 'lk.pii.error': err }, 'dev channel unavailable'); }); this.#socket = socket; } catch (err) { - logger.debug(`dev channel unavailable: ${(err as Error).message}`); + logger.debug({ 'lk.pii.error': err }, 'dev channel unavailable'); } } diff --git a/agents/src/console.ts b/agents/src/console.ts index f648730b8..e98563129 100644 --- a/agents/src/console.ts +++ b/agents/src/console.ts @@ -158,7 +158,7 @@ export async function runConsole({ try { await fn(); } catch (error) { - logger.error({ error }, `error in ${step}`); + logger.error({ 'lk.pii.error': error, step }, 'error during teardown'); } }; @@ -179,7 +179,7 @@ export async function runConsole({ const results = await Promise.allSettled(jobCtx.shutdownCallbacks.map((cb) => cb())); for (const result of results) { if (result.status === 'rejected') { - logger.error({ error: result.reason }, 'error while running shutdown callback'); + logger.error({ 'lk.pii.error': result.reason }, 'error while running shutdown callback'); } } } diff --git a/agents/src/download.ts b/agents/src/download.ts index 3f4123ed9..eeea457af 100644 --- a/agents/src/download.ts +++ b/agents/src/download.ts @@ -138,7 +138,8 @@ export const main = async (cwd: string = process.cwd()): Promise => { return 0; } logger.info( - `discovered ${packages.length} plugin package(s): ${packages.map((p) => p.name).join(', ')}`, + { pluginCount: packages.length, plugins: packages.map((p) => p.name) }, + 'discovered plugin packages', ); let importFailures = 0; @@ -148,24 +149,30 @@ export const main = async (cwd: string = process.cwd()): Promise => { await import(pathToFileURL(entryAbs).href); } catch (error) { importFailures += 1; - logger.error(`failed to import ${pkg.name}: ${formatErrorMessage(error)}`); + logger.error({ 'lk.pii.error': error, package: pkg.name }, 'failed to import plugin package'); } } const failures: PluginDownloadFailure[] = []; for (const plugin of Plugin.registeredPlugins) { - logger.info(`Downloading files for ${plugin.title}`); + logger.info({ plugin: plugin.title }, 'Downloading plugin files'); try { await plugin.downloadFiles(); - logger.info(`Finished downloading files for ${plugin.title}`); + logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); } catch (error) { failures.push({ plugin, error }); - logger.error(`Failed to download files for ${plugin.title}: ${formatErrorMessage(error)}`); + logger.error( + { 'lk.pii.error': error, plugin: plugin.title }, + 'failed to download plugin files', + ); } } if (failures.length > 0) { - logger.fatal(formatDownloadFailureMessage(failures)); + logger.fatal( + { 'lk.pii.error': formatDownloadFailureMessage(failures) }, + 'plugin file downloads failed', + ); } return failures.length > 0 || importFailures > 0 ? 1 : 0; }; diff --git a/agents/src/http_server.ts b/agents/src/http_server.ts index 54ebaf0a5..441dce690 100644 --- a/agents/src/http_server.ts +++ b/agents/src/http_server.ts @@ -59,7 +59,7 @@ export class HTTPServer { if (err) reject(err); const address = this.app.address(); if (typeof address! !== 'string') { - this.#logger.info(`Server is listening on port ${address!.port}`); + this.#logger.info({ port: address!.port }, 'Server is listening'); } resolve(); }); diff --git a/agents/src/inference/_warmup.ts b/agents/src/inference/_warmup.ts index 0c77e6814..f3ed6927e 100644 --- a/agents/src/inference/_warmup.ts +++ b/agents/src/inference/_warmup.ts @@ -32,7 +32,7 @@ function getNative(): typeof import('@livekit/local-inference') | undefined { return nativeMod; } catch (err) { log().warn( - { err: err instanceof Error ? err.message : String(err) }, + { 'lk.pii.error': err }, '@livekit/local-inference native binding not loadable; local VAD/EOT paths disabled', ); return undefined; diff --git a/agents/src/inference/eot/detector.ts b/agents/src/inference/eot/detector.ts index 805b65c6c..8c91e473d 100644 --- a/agents/src/inference/eot/detector.ts +++ b/agents/src/inference/eot/detector.ts @@ -290,7 +290,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _fallBackToLocal(reason: Error): void { if (!this._warnedCloudFailure) { this._detLogger.warn( - { reason: reason.message }, + { 'lk.pii.error': reason }, 'cloud turn detector failed; falling back to local mini model', ); this._warnedCloudFailure = true; @@ -320,7 +320,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _onLocalFailure(reason: Error): void { if (!this._warnedLocalFailure) { this._detLogger.warn( - { reason: reason.message }, + { 'lk.pii.error': reason }, 'local audio turn detector failed; defaulting to 1.0 and retrying on next turn', ); this._warnedLocalFailure = true; diff --git a/agents/src/inference/eot/runner.ts b/agents/src/inference/eot/runner.ts index d79ed7ad5..d0ef890aa 100644 --- a/agents/src/inference/eot/runner.ts +++ b/agents/src/inference/eot/runner.ts @@ -57,10 +57,7 @@ export default class EotRunner extends InferenceRunner Typ this.logger.debug( { - baseUrl: this.options.baseUrl, + 'lk.pii.base_url': this.options.baseUrl, detectionIntervalInS: this.options.detectionIntervalInS, audioPrefixDurationInS: this.options.audioPrefixDurationInS, maxAudioDurationInS: this.options.maxAudioDurationInS, diff --git a/agents/src/inference/interruption/ws_transport.ts b/agents/src/inference/interruption/ws_transport.ts index a1b252660..fdfee12d7 100644 --- a/agents/src/inference/interruption/ws_transport.ts +++ b/agents/src/inference/interruption/ws_transport.ts @@ -184,7 +184,10 @@ export function createWsTransport( message = wsMessageSchema.parse(JSON.parse(data.toString())); } catch (err) { logger.warn( - { data: data.toString(), err: err instanceof Error ? err.message : String(err) }, + { + 'lk.pii.data': data.toString(), + 'lk.pii.error': err instanceof Error ? err.message : String(err), + }, 'Failed to parse WebSocket message', ); return; @@ -195,7 +198,10 @@ export function createWsTransport( handleMessage(message); } catch (err) { logger.warn( - { type: message.type, err: err instanceof Error ? err.message : String(err) }, + { + type: message.type, + 'lk.pii.error': err instanceof Error ? err.message : String(err), + }, 'Failed to handle WebSocket message', ); } @@ -208,7 +214,7 @@ export function createWsTransport( }); socket.on('close', (code: number, reason: Buffer) => { - logger.debug({ code, reason: reason.toString() }, 'WebSocket closed'); + logger.debug({ code, 'lk.pii.reason': reason.toString() }, 'WebSocket closed'); }); } diff --git a/agents/src/inference/stt.ts b/agents/src/inference/stt.ts index 3cfc8e852..8bbf846e7 100644 --- a/agents/src/inference/stt.ts +++ b/agents/src/inference/stt.ts @@ -727,7 +727,10 @@ export class SpeechStream extends BaseSpeechStream { try { this.activeWs.send(JSON.stringify({ type: 'session.update', settings })); } catch (e) { - this.#logger.debug({ err: e }, 'failed to send session.update, ws may be closing'); + this.#logger.debug( + { 'lk.pii.error': e }, + 'failed to send session.update, ws may be closing', + ); } } } @@ -774,7 +777,7 @@ export class SpeechStream extends BaseSpeechStream { }); ws.on('error', (e) => { - this.#logger.error({ error: e }, 'WebSocket error'); + this.#logger.error({ 'lk.pii.error': e }, 'WebSocket error'); resourceCleanup(); reject(e); }); @@ -887,7 +890,10 @@ export class SpeechStream extends BaseSpeechStream { const parseResult = await sttServerEventSchema.safeParseAsync(result.value); if (!parseResult.success) { this.#logger.warn( - { error: parseResult.error, rawData: result.value }, + { + 'lk.pii.error': parseResult.error, + 'lk.pii.raw_data': result.value, + }, 'Failed to parse STT server event', ); continue; @@ -919,7 +925,7 @@ export class SpeechStream extends BaseSpeechStream { this.processTranscript(event, SpeechEventType.PREFLIGHT_TRANSCRIPT); break; case 'error': - this.#logger.error({ error: event }, 'Received error from LiveKit STT'); + this.#logger.error({ 'lk.pii.event': event }, 'Received error from LiveKit STT'); resourceCleanup(); throw new APIError(`LiveKit STT returned error: ${JSON.stringify(event)}`); } @@ -1071,11 +1077,11 @@ export class SpeechStream extends BaseSpeechStream { if (e instanceof Error && e.message.includes('Queue is closed')) { // Expected behavior on disconnect, log as warning this.#logger.warn( - { err: e }, + { 'lk.pii.error': e }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ err: e }, 'Error putting transcript to queue'); + this.#logger.error({ 'lk.pii.error': e }, 'Error putting transcript to queue'); } } } diff --git a/agents/src/inference/tts.ts b/agents/src/inference/tts.ts index 166edb62d..ba48a7a04 100644 --- a/agents/src/inference/tts.ts +++ b/agents/src/inference/tts.ts @@ -521,7 +521,10 @@ export class TTS extends BaseTTS { }; } - this.#logger.debug({ url }, 'inference.TTS creating new websocket connection (pool miss)'); + this.#logger.debug( + { 'lk.pii.url': url }, + 'inference.TTS creating new websocket connection (pool miss)', + ); const socket = await connectWs(url, headers, timeout); socket.send(JSON.stringify(params)); return socket; @@ -686,17 +689,17 @@ export class SynthesizeStream extends BaseSynthesizeSt // writer.write returns a promise; avoid unhandled rejections if stream is closed void eventChannel.write(validatedEvent).catch((error) => { this.#logger.debug( - { error }, + { 'lk.pii.error': error }, 'Failed writing TTS event to stream channel (likely closed)', ); }); } catch (e) { - this.#logger.error({ error: e }, 'Error parsing WebSocket message'); + this.#logger.error({ 'lk.pii.error': e }, 'Error parsing WebSocket message'); } }; const onError = (e: Error) => { - this.#logger.error({ error: e }, 'WebSocket error'); + this.#logger.error({ 'lk.pii.error': e }, 'WebSocket error'); void resourceCleanup(); try { // If the ws is misbehaving, hard-stop it immediately to avoid buffering. @@ -839,7 +842,7 @@ export class SynthesizeStream extends BaseSynthesizeSt return; case 'error': this.#logger.error( - { serverEvent }, + { 'lk.pii.server_event': serverEvent }, 'Received error message from LiveKit TTS WebSocket', ); await resourceCleanup(); diff --git a/agents/src/inference/vad.ts b/agents/src/inference/vad.ts index d6b65ae6e..ffe558648 100644 --- a/agents/src/inference/vad.ts +++ b/agents/src/inference/vad.ts @@ -136,10 +136,7 @@ class InferenceVADStream extends BaseVADStream { this._windowSamples = mod.VAD_WINDOW_SAMPLES; } this._pumpTask = this._pump().catch((err) => { - this._logger.error( - { err: err instanceof Error ? err.message : String(err) }, - 'VAD pump failed', - ); + this._logger.error({ 'lk.pii.error': err }, 'VAD pump failed'); }); } diff --git a/agents/src/ipc/job_proc_lazy_main.ts b/agents/src/ipc/job_proc_lazy_main.ts index 5812c168f..acd93c348 100644 --- a/agents/src/ipc/job_proc_lazy_main.ts +++ b/agents/src/ipc/job_proc_lazy_main.ts @@ -31,7 +31,7 @@ const safeSend = (msg: IPCMessage): boolean => { if (error instanceof Error && error.message.includes('Channel closed')) { log().debug({ msgCase: msg.case }, 'IPC channel closed, message not sent'); } else { - log().error({ error, msgCase: msg.case }, 'IPC send failed unexpectedly'); + log().error({ 'lk.pii.error': error, msgCase: msg.case }, 'IPC send failed unexpectedly'); } return false; } @@ -64,7 +64,9 @@ class InfClient implements InferenceExecutor { const fut = this.#requests[msg.value.requestId]; delete this.#requests[msg.value.requestId]; if (!fut) { - this.#logger.child({ resp: msg.value }).warn('received unexpected inference response'); + this.#logger + .child({ 'lk.pii.response': msg.value }) + .warn('received unexpected inference response'); return; } fut.resolve(msg.value); @@ -161,7 +163,7 @@ const startJob = ( clearTimeout(unconnectedTimeout); }); } catch (error) { - logger.error({ error }, 'error in entry function'); + logger.error({ 'lk.pii.error': error }, 'error in entry function'); shutdown = true; safeSend({ case: 'exiting', @@ -181,7 +183,7 @@ const startJob = ( void sessionClosePromise.catch((sessionCloseError) => logger.debug( - { error: sessionCloseError }, + { 'lk.pii.error': sessionCloseError }, 'AgentSession.close() rejected after shutdown timeout', ), ); @@ -196,7 +198,7 @@ const startJob = ( try { await ctx._onSessionEnd(); } catch (error) { - logger.error({ error }, 'error in ctx._onSessionEnd'); + logger.error({ 'lk.pii.error': error }, 'error in ctx._onSessionEnd'); } await room.disconnect(); @@ -207,7 +209,7 @@ const startJob = ( shutdownTasks.push(callback()); } await ThrowsPromise.all(shutdownTasks).catch((error) => - logger.error({ error }, 'error while shutting down the job'), + logger.error({ 'lk.pii.error': error }, 'error while shutting down the job'), ); safeSend({ case: 'done', value: undefined }); @@ -264,7 +266,7 @@ const startJob = ( let logger = log().child({ pid: proc.pid }); process.on('unhandledRejection', (reason) => { - logger.debug({ error: reason }, 'Unhandled promise rejection'); + logger.debug({ 'lk.pii.error': reason }, 'Unhandled promise rejection'); }); logger.debug('initializing job runner'); @@ -333,7 +335,7 @@ const startJob = ( await dispose(); logger.debug('native resources disposed'); } catch (error) { - logger.warn({ error }, 'failed to dispose native resources'); + logger.warn({ 'lk.pii.error': error }, 'failed to dispose native resources'); } logger.debug('Job process shutdown'); diff --git a/agents/src/ipc/supervised_proc.ts b/agents/src/ipc/supervised_proc.ts index 727516a70..6a6db2247 100644 --- a/agents/src/ipc/supervised_proc.ts +++ b/agents/src/ipc/supervised_proc.ts @@ -131,19 +131,19 @@ export abstract class SupervisedProc { if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) { this.#logger - .child(this.memoryLoggingFields(memoryMB)) - .error(`${this.processKind} process exceeded memory limit, killing it`); + .child({ ...this.memoryLoggingFields(memoryMB), processKind: this.processKind }) + .error('process exceeded memory limit, killing it'); this.close(); } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) { if (this.shouldEmitMemoryWarning(memoryMB)) { const advisory = this.#opts.memoryLimitMB <= 0; this.#logger - .child(this.memoryLoggingFields(memoryMB)) - .warn( - `${this.processKind} process memory usage is above the warning threshold${ - advisory ? ' (advisory only, the process will not be terminated)' : '' - }`, - ); + .child({ + ...this.memoryLoggingFields(memoryMB), + processKind: this.processKind, + advisory, + }) + .warn('process memory usage is above the warning threshold'); } } }, MEMORY_MONITOR_INTERVAL); diff --git a/agents/src/job.ts b/agents/src/job.ts index 58825dbe7..6516a4ac9 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -387,7 +387,10 @@ export class JobContext> { await client.deleteRoom(targetRoomName); this.#logger.info({ roomName: targetRoomName }, 'room deleted'); } catch (error) { - this.#logger.warn({ error, roomName: targetRoomName }, 'error while deleting room'); + this.#logger.warn( + { 'lk.pii.error': error, roomName: targetRoomName }, + 'error while deleting room', + ); } } @@ -444,7 +447,7 @@ export class JobContext> { JSON.stringify(sessionReportToJSON(report), null, 2), ); } catch (error) { - this.#logger.error({ error }, 'failed to save the session report'); + this.#logger.error({ 'lk.pii.error': error }, 'failed to save the session report'); } } @@ -469,7 +472,7 @@ export class JobContext> { 'Session report uploaded to LiveKit Cloud', ); } catch (error) { - this.#logger.error({ error }, 'Failed to upload session report'); + this.#logger.error({ 'lk.pii.error': error }, 'Failed to upload session report'); } } } @@ -489,7 +492,7 @@ export class JobContext> { try { await flushOtelLogs(); } catch (error) { - this.#logger.error({ error }, 'Failed to flush OTEL logs'); + this.#logger.error({ 'lk.pii.error': error }, 'Failed to flush OTEL logs'); } } diff --git a/agents/src/llm/fallback_adapter.ts b/agents/src/llm/fallback_adapter.ts index fdacac36b..821ef8ba5 100644 --- a/agents/src/llm/fallback_adapter.ts +++ b/agents/src/llm/fallback_adapter.ts @@ -233,9 +233,12 @@ class FallbackLLMStream extends LLMStream { } catch (error) { if (error instanceof APIError) { if (checkRecovery) { - this._log.warn({ llm: llm.label(), error }, 'recovery failed'); + this._log.warn({ llm: llm.label(), 'lk.pii.error': error }, 'recovery failed'); } else { - this._log.warn({ llm: llm.label(), error }, 'failed, switching to next LLM'); + this._log.warn( + { llm: llm.label(), 'lk.pii.error': error }, + 'failed, switching to next LLM', + ); } throw error; } @@ -252,9 +255,12 @@ class FallbackLLMStream extends LLMStream { // Unexpected error if (checkRecovery) { - this._log.error({ llm: llm.label(), error }, 'recovery unexpected error'); + this._log.error({ llm: llm.label(), 'lk.pii.error': error }, 'recovery unexpected error'); } else { - this._log.error({ llm: llm.label(), error }, 'unexpected error, switching to next LLM'); + this._log.error( + { llm: llm.label(), 'lk.pii.error': error }, + 'unexpected error, switching to next LLM', + ); } throw error; } finally { @@ -364,7 +370,10 @@ class FallbackLLMStream extends LLMStream { // Check if we sent data before failing if (textSent || toolCallsSent.length > 0) { - const extra = { textSent, toolCallsSent }; + const extra = { + 'lk.pii.response.text': textSent, + 'lk.pii.response.function_calls': toolCallsSent, + }; if (!this.adapter.retryOnChunkSent) { this._log.error( diff --git a/agents/src/llm/llm.ts b/agents/src/llm/llm.ts index ae9bde31c..2efd91c43 100644 --- a/agents/src/llm/llm.ts +++ b/agents/src/llm/llm.ts @@ -221,8 +221,13 @@ export abstract class LLMStream implements AsyncIterableIterator { } else { this.emitError({ error, recoverable: true }); this.logger.warn( - { llm: this.#llm.label(), attempt: i + 1, error }, - `failed to generate LLM completion, retrying in ${retryInterval}ms`, + { + llm: this.#llm.label(), + attempt: i + 1, + retryIntervalMs: retryInterval, + 'lk.pii.error': error, + }, + 'failed to generate LLM completion, retrying', ); } diff --git a/agents/src/llm/realtime.ts b/agents/src/llm/realtime.ts index 63db3a42e..a4393cf3d 100644 --- a/agents/src/llm/realtime.ts +++ b/agents/src/llm/realtime.ts @@ -194,21 +194,21 @@ export abstract class RealtimeSession extends EventEmitter { try { await this.updateInstructions(instructions); } catch (error) { - this.logger.error(error, 'failed to update the instructions'); + this.logger.error({ 'lk.pii.error': error }, 'failed to update the instructions'); } } if (chatCtx !== undefined) { try { await this.updateChatCtx(chatCtx); } catch (error) { - this.logger.error(error, 'failed to update the chat context'); + this.logger.error({ 'lk.pii.error': error }, 'failed to update the chat context'); } } if (tools !== undefined) { try { await this.updateTools(tools); } catch (error) { - this.logger.error(error, 'failed to update the tools'); + this.logger.error({ 'lk.pii.error': error }, 'failed to update the tools'); } } } diff --git a/agents/src/log.test.ts b/agents/src/log.test.ts index 0acdcd81a..f42608b46 100644 --- a/agents/src/log.test.ts +++ b/agents/src/log.test.ts @@ -39,4 +39,46 @@ describe('OTEL logging', () => { expect(messages).toContain('log from fresh logger'); }); }); + + it('exports sensitive content as a redactable attribute instead of log body text', async () => { + initializeLogger({ pretty: false, level: 'info' }); + const emitSpy = vi.spyOn(PinoCloudExporter.prototype, 'emit').mockImplementation(() => {}); + + initPinoCloudExporter({ + cloudHostname: 'example.livekit.cloud', + roomId: 'RM_test', + jobId: 'AJ_test', + }); + enableOtelLogging(); + + log().info({ 'lk.pii.user_input': 'secret transcript' }, 'received user input'); + log().error({ 'lk.pii.error': new Error('secret provider error') }, 'provider failed'); + + await vi.waitFor(() => { + const record = emitSpy.mock.calls + .map(([logObj]) => logObj) + .find((logObj) => { + return logObj.msg === 'received user input'; + }); + expect(record).toMatchObject({ + msg: 'received user input', + 'lk.pii.user_input': 'secret transcript', + }); + expect(record?.msg).not.toContain('secret transcript'); + + const errorRecord = emitSpy.mock.calls + .map(([logObj]) => logObj) + .find((logObj) => { + return logObj.msg === 'provider failed'; + }); + expect(errorRecord).toMatchObject({ + msg: 'provider failed', + 'lk.pii.error': { + type: 'Error', + message: 'secret provider error', + }, + }); + expect(errorRecord?.msg).not.toContain('secret provider error'); + }); + }); }); diff --git a/agents/src/log.ts b/agents/src/log.ts index 5f250901f..8de08d6cf 100644 --- a/agents/src/log.ts +++ b/agents/src/log.ts @@ -48,7 +48,13 @@ const createLogger = ({ pretty, level }: LoggerOptions): Logger => { ]; return pino( - { level: logLevel, serializers: { error: pino.stdSerializers.err } }, + { + level: logLevel, + serializers: { + error: pino.stdSerializers.err, + 'lk.pii.error': pino.stdSerializers.err, + }, + }, multistream(streams), ); }; diff --git a/agents/src/stream/multi_input_stream.ts b/agents/src/stream/multi_input_stream.ts index c5a92c980..9e259be09 100644 --- a/agents/src/stream/multi_input_stream.ts +++ b/agents/src/stream/multi_input_stream.ts @@ -158,7 +158,7 @@ export class MultiInputStream { return; } - this.logger.error({ error: e }, 'Error pumping input stream from MultiInputStream'); + this.logger.error({ 'lk.pii.error': e }, 'Error pumping input stream from MultiInputStream'); } finally { try { reader.releaseLock(); diff --git a/agents/src/stt/fallback_adapter.ts b/agents/src/stt/fallback_adapter.ts index f733545bb..b3e1000b7 100644 --- a/agents/src/stt/fallback_adapter.ts +++ b/agents/src/stt/fallback_adapter.ts @@ -233,13 +233,16 @@ export class FallbackAdapter extends STT { try { await stt.recognize(frame, controller.signal); status.available = true; - this._logger.info({ stt: stt.label }, `${stt.label} recovered`); + this._logger.info({ stt: stt.label }, 'STT recovered'); this.emitAvailabilityChanged(stt, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: stt.label, err: e }, `${stt.label} recovery failed`); + this._logger.warn({ stt: stt.label, 'lk.pii.error': e }, 'STT recovery failed'); } else { - this._logger.debug({ stt: stt.label, err: e }, `${stt.label} recovery unexpected error`); + this._logger.debug( + { stt: stt.label, 'lk.pii.error': e }, + 'STT recovery unexpected error', + ); } } }); @@ -278,13 +281,13 @@ export class FallbackAdapter extends STT { } catch (e) { if (e instanceof APIError) { this._logger.warn( - { stt: stt.label, err: e }, - `${stt.label} failed, switching to next STT`, + { stt: stt.label, 'lk.pii.error': e }, + 'STT failed, switching to next STT', ); } else { this._logger.warn( - { stt: stt.label, err: e }, - `${stt.label} unexpected error, switching to next STT`, + { stt: stt.label, 'lk.pii.error': e }, + 'STT unexpected error, switching to next STT', ); } if (status.available) { @@ -392,18 +395,15 @@ class FallbackSpeechStream extends SpeechStream { } if (!gotTranscript) return; status.available = true; - this._logger.info({ stt: sttInstance.label }, `${sttInstance.label} recovered`); + this._logger.info({ stt: sttInstance.label }, 'STT recovered'); this.fallbackAdapter.emitAvailabilityChanged(sttInstance, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn( - { stt: sttInstance.label, err: e }, - `${sttInstance.label} recovery failed`, - ); + this._logger.warn({ stt: sttInstance.label, 'lk.pii.error': e }, 'STT recovery failed'); } else { this._logger.debug( - { stt: sttInstance.label, err: e }, - `${sttInstance.label} recovery unexpected error`, + { stt: sttInstance.label, 'lk.pii.error': e }, + 'STT recovery unexpected error', ); } } finally { @@ -452,7 +452,7 @@ class FallbackSpeechStream extends SpeechStream { if (typeof item === 'symbol') current.flush(); else current.pushFrame(item); } catch (e) { - this._logger.debug({ err: e }, 'error forwarding input to main stream'); + this._logger.debug({ 'lk.pii.error': e }, 'error forwarding input to main stream'); } } } @@ -525,20 +525,17 @@ class FallbackSpeechStream extends SpeechStream { status.available = false; this.fallbackAdapter.emitAvailabilityChanged(sttInstance, false); } - this._logger.warn( - { stt: sttInstance.label }, - `${sttInstance.label} failed, switching to next STT`, - ); + this._logger.warn({ stt: sttInstance.label }, 'STT failed, switching to next STT'); } catch (e) { if (e instanceof APIError) { this._logger.warn( - { stt: sttInstance.label, err: e }, - `${sttInstance.label} failed, switching to next STT`, + { stt: sttInstance.label, 'lk.pii.error': e }, + 'STT failed, switching to next STT', ); } else { this._logger.warn( - { stt: sttInstance.label, err: e }, - `${sttInstance.label} unexpected error, switching to next STT`, + { stt: sttInstance.label, 'lk.pii.error': e }, + 'STT unexpected error, switching to next STT', ); } if (status.available) { diff --git a/agents/src/stt/stream_adapter.ts b/agents/src/stt/stream_adapter.ts index 648efc76b..2181d6b64 100644 --- a/agents/src/stt/stream_adapter.ts +++ b/agents/src/stt/stream_adapter.ts @@ -121,7 +121,7 @@ export class StreamAdapterWrapper extends SpeechStream { } else { logger = logger.child({ error }); } - logger.error(`${this.label}: provider recognize task failed`); + logger.error({ stt: this.label }, 'provider recognize task failed'); continue; } } diff --git a/agents/src/stt/stt.ts b/agents/src/stt/stt.ts index b6f157519..d4b1323f9 100644 --- a/agents/src/stt/stt.ts +++ b/agents/src/stt/stt.ts @@ -380,8 +380,13 @@ export abstract class SpeechStream implements AsyncIterableIterator // Don't emit error event for recoverable errors during retry loop // to avoid ERR_UNHANDLED_ERROR or premature session termination this.logger.warn( - { stt: this.#stt.label, attempt: i + 1, error }, - `failed to recognize speech, retrying in ${retryInterval}ms`, + { + stt: this.#stt.label, + attempt: i + 1, + retryIntervalMs: retryInterval, + 'lk.pii.error': error, + }, + 'failed to recognize speech, retrying', ); } @@ -432,7 +437,7 @@ export abstract class SpeechStream implements AsyncIterableIterator } catch (e) { if (e instanceof Error && e.message.includes('Queue is closed')) { this.logger.warn( - { err: e }, + { 'lk.pii.error': e }, 'Queue closed during transcript processing (expected during disconnect)', ); } diff --git a/agents/src/telemetry/index.ts b/agents/src/telemetry/index.ts index a69227e26..cb3e25383 100644 --- a/agents/src/telemetry/index.ts +++ b/agents/src/telemetry/index.ts @@ -29,4 +29,9 @@ export { type SpanProcessorLike, type StartSpanOptions, } from './traces.js'; -export { recordException, recordRealtimeMetrics } from './utils.js'; +export { + REDACTED_EXCEPTION_MESSAGE, + recordException, + recordRealtimeMetrics, + type RecordExceptionOptions, +} from './utils.js'; diff --git a/agents/src/telemetry/trace_types.test.ts b/agents/src/telemetry/trace_types.test.ts new file mode 100644 index 000000000..e19323867 --- /dev/null +++ b/agents/src/telemetry/trace_types.test.ts @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it } from 'vitest'; +import * as traceTypes from './trace_types.js'; + +const PII_SEGMENT_RE = /(^|\.)pii(\.|$)/i; + +// Keys that carry no conversational content, tool payloads, or other user data. +const SAFE_KEYS = new Set([ + // Correlation IDs and session metadata + 'lk.speech_id', + 'lk.agent_label', + 'lk.start_time', + 'lk.end_time', + 'lk.retry_count', + 'lk.provider_request_ids', + 'lk.participant_id', + 'lk.participant_identity', + 'lk.participant_kind', + 'lk.job_id', + 'lk.agent_name', + 'lk.room_name', + 'lk.session_options', + 'lk.generation_id', + 'lk.parent_generation_id', + 'lk.interrupted', + // LLM node metadata + 'lk.function_tools', + 'lk.provider_tools', + 'lk.tool_sets', + 'lk.response.ttft', + // Function tool metadata + 'lk.function_tool.id', + 'lk.function_tool.name', + 'lk.function_tool.is_error', + // TTS metadata + 'lk.tts.streaming', + 'lk.tts.label', + 'lk.response.ttfb', + // EOU detection + 'lk.eou.probability', + 'lk.eou.unlikely_threshold', + 'lk.eou.endpointing_delay', + 'lk.eou.language', + 'lk.eou.source', + 'lk.eou.from_cache', + 'lk.eou.detection_delay', + 'lk.transcript_confidence', + 'lk.transcription_delay', + 'lk.end_of_turn_delay', + // Metrics + 'lk.llm_metrics', + 'lk.tts_metrics', + 'lk.realtime_model_metrics', + 'lk.e2e_latency', + // OpenTelemetry GenAI attributes and event names + 'gen_ai.operation.name', + 'gen_ai.request.model', + 'gen_ai.provider.name', + 'gen_ai.usage.input_tokens', + 'gen_ai.usage.output_tokens', + 'gen_ai.usage.input_text_tokens', + 'gen_ai.usage.input_audio_tokens', + 'gen_ai.usage.input_cached_tokens', + 'gen_ai.usage.output_text_tokens', + 'gen_ai.usage.output_audio_tokens', + 'gen_ai.system.message', + 'gen_ai.user.message', + 'gen_ai.assistant.message', + 'gen_ai.tool.message', + 'gen_ai.choice', + // OpenTelemetry exception attributes + 'exception.stacktrace', + 'exception.type', + 'exception.message', + // Vendor metadata + 'langfuse.observation.completion_start_time', + // Answering machine detection + 'lk.amd.category', + 'lk.amd.reason', + 'lk.amd.is_machine', + 'lk.amd.interrupt_on_machine', + 'lk.amd.speech_duration', + 'lk.amd.delay', + // Adaptive interruption + 'lk.is_interruption', + 'lk.interruption.probability', + 'lk.interruption.total_duration', + 'lk.interruption.prediction_duration', + 'lk.interruption.detection_delay', +]); + +function declaredKeys(): Record { + return Object.fromEntries( + Object.entries(traceTypes).filter((entry): entry is [string, string] => { + return typeof entry[1] === 'string'; + }), + ); +} + +describe('telemetry key PII classification', () => { + it('classifies every declared key as safe or PII-bearing', () => { + const unclassified = Object.fromEntries( + Object.entries(declaredKeys()).filter( + ([, value]) => !SAFE_KEYS.has(value) && !PII_SEGMENT_RE.test(value), + ), + ); + + expect(unclassified).toEqual({}); + }); + + it('does not mark safe keys as PII-bearing', () => { + const conflicting = [...SAFE_KEYS].filter((key) => PII_SEGMENT_RE.test(key)).sort(); + + expect(conflicting).toEqual([]); + }); + + it('does not retain stale safe-list entries', () => { + const declared = new Set(Object.values(declaredKeys())); + const stale = [...SAFE_KEYS].filter((key) => !declared.has(key)).sort(); + + expect(stale).toEqual([]); + }); +}); diff --git a/agents/src/telemetry/trace_types.ts b/agents/src/telemetry/trace_types.ts index 1f79eca63..3ef3c8eda 100644 --- a/agents/src/telemetry/trace_types.ts +++ b/agents/src/telemetry/trace_types.ts @@ -2,6 +2,16 @@ // // SPDX-License-Identifier: Apache-2.0 +/** + * Span attribute and event name constants for LiveKit Agents telemetry. + * + * Attributes carrying conversational content, tool payloads, or other user data must include a + * dot-delimited `pii` segment (`lk.pii.`). PII-enabled projects have these attributes + * stripped at the LiveKit Cloud collector, and the segment is the marker it honors. Such content + * must not be embedded in span names, event names, or log message bodies because those are not + * redactable. + */ + // LiveKit custom attributes export const ATTR_SPEECH_ID = 'lk.speech_id'; export const ATTR_AGENT_LABEL = 'lk.agent_label'; @@ -32,29 +42,29 @@ export const ATTR_SESSION_OPTIONS = 'lk.session_options'; // assistant turn export const ATTR_AGENT_TURN_ID = 'lk.generation_id'; export const ATTR_AGENT_PARENT_TURN_ID = 'lk.parent_generation_id'; -export const ATTR_USER_INPUT = 'lk.user_input'; -export const ATTR_INSTRUCTIONS = 'lk.instructions'; +export const ATTR_USER_INPUT = 'lk.pii.user_input'; +export const ATTR_INSTRUCTIONS = 'lk.pii.instructions'; export const ATTR_SPEECH_INTERRUPTED = 'lk.interrupted'; // llm node -export const ATTR_CHAT_CTX = 'lk.chat_ctx'; +export const ATTR_CHAT_CTX = 'lk.pii.chat_ctx'; export const ATTR_FUNCTION_TOOLS = 'lk.function_tools'; export const ATTR_PROVIDER_TOOLS = 'lk.provider_tools'; export const ATTR_TOOL_SETS = 'lk.tool_sets'; -export const ATTR_RESPONSE_TEXT = 'lk.response.text'; -export const ATTR_RESPONSE_FUNCTION_CALLS = 'lk.response.function_calls'; +export const ATTR_RESPONSE_TEXT = 'lk.pii.response.text'; +export const ATTR_RESPONSE_FUNCTION_CALLS = 'lk.pii.response.function_calls'; /** Time to first token in seconds. */ export const ATTR_RESPONSE_TTFT = 'lk.response.ttft'; // function tool export const ATTR_FUNCTION_TOOL_ID = 'lk.function_tool.id'; 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_ARGS = 'lk.pii.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'; +export const ATTR_FUNCTION_TOOL_OUTPUT = 'lk.pii.function_tool.output'; // tts node -export const ATTR_TTS_INPUT_TEXT = 'lk.input_text'; +export const ATTR_TTS_INPUT_TEXT = 'lk.pii.input_text'; export const ATTR_TTS_STREAMING = 'lk.tts.streaming'; export const ATTR_TTS_LABEL = 'lk.tts.label'; /** Time to first byte in seconds. */ @@ -72,7 +82,7 @@ export const ATTR_EOU_SOURCE = 'lk.eou.source'; export const ATTR_EOU_FROM_CACHE = 'lk.eou.from_cache'; /** Latest input-audio creation time → prediction receive time (ms). */ export const ATTR_EOU_DETECTION_DELAY = 'lk.eou.detection_delay'; -export const ATTR_USER_TRANSCRIPT = 'lk.user_transcript'; +export const ATTR_USER_TRANSCRIPT = 'lk.pii.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'; @@ -86,7 +96,7 @@ export const ATTR_AMD_INTERRUPT_ON_MACHINE = 'lk.amd.interrupt_on_machine'; export const ATTR_AMD_SPEECH_DURATION = 'lk.amd.speech_duration'; /** Time between speech end and the AMD verdict emission (milliseconds). */ export const ATTR_AMD_DELAY = 'lk.amd.delay'; -export const ATTR_AMD_TRANSCRIPT = 'lk.amd.transcript'; +export const ATTR_AMD_TRANSCRIPT = 'lk.pii.amd.transcript'; // Adaptive Interruption attributes export const ATTR_IS_INTERRUPTION = 'lk.is_interruption'; diff --git a/agents/src/telemetry/utils.test.ts b/agents/src/telemetry/utils.test.ts new file mode 100644 index 000000000..d640d57c2 --- /dev/null +++ b/agents/src/telemetry/utils.test.ts @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { Span } from '@opentelemetry/api'; +import { SpanStatusCode } from '@opentelemetry/api'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { type JobContext, runWithJobContext } from '../job.js'; +import * as traceTypes from './trace_types.js'; +import { + REDACTED_EXCEPTION_MESSAGE, + type RecordExceptionOptions, + recordException, +} from './utils.js'; + +function fakeSpan() { + return { + addEvent: vi.fn(), + recordException: vi.fn(), + setAttributes: vi.fn(), + setStatus: vi.fn(), + }; +} + +function captureException( + span: ReturnType, + options?: RecordExceptionOptions, +): void { + const error = new Error('secret transcript'); + recordException(span as unknown as Span, error, options); +} + +describe('recordException', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('preserves exception details when redaction is explicitly disabled', () => { + const span = fakeSpan(); + + captureException(span, { redacted: false }); + + expect(span.recordException).toHaveBeenCalledOnce(); + expect(span.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: 'secret transcript', + }); + expect(span.setAttributes).toHaveBeenCalledWith({ + [traceTypes.ATTR_EXCEPTION_TYPE]: 'Error', + [traceTypes.ATTR_EXCEPTION_MESSAGE]: 'secret transcript', + [traceTypes.ATTR_EXCEPTION_TRACE]: expect.stringContaining('secret transcript'), + }); + }); + + it('omits exception details when redaction is explicitly enabled', () => { + const span = fakeSpan(); + + captureException(span, { redacted: true }); + + const attrs = { + [traceTypes.ATTR_EXCEPTION_TYPE]: 'Error', + [traceTypes.ATTR_EXCEPTION_MESSAGE]: REDACTED_EXCEPTION_MESSAGE, + }; + expect(span.recordException).not.toHaveBeenCalled(); + expect(span.addEvent).toHaveBeenCalledWith('exception', attrs); + expect(span.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: REDACTED_EXCEPTION_MESSAGE, + }); + expect(span.setAttributes).toHaveBeenCalledWith(attrs); + expect( + JSON.stringify([ + span.addEvent.mock.calls, + span.setStatus.mock.calls, + span.setAttributes.mock.calls, + ]), + ).not.toContain('secret transcript'); + }); + + it('uses the current job redaction setting by default', () => { + const span = fakeSpan(); + const context = { + job: { enableRedaction: true }, + } as unknown as JobContext; + + runWithJobContext(context, () => captureException(span)); + + expect(span.recordException).not.toHaveBeenCalled(); + expect(span.setAttributes).toHaveBeenCalledWith({ + [traceTypes.ATTR_EXCEPTION_TYPE]: 'Error', + [traceTypes.ATTR_EXCEPTION_MESSAGE]: REDACTED_EXCEPTION_MESSAGE, + }); + }); + + it('preserves exception details by default without a job context', () => { + const span = fakeSpan(); + + captureException(span); + + expect(span.recordException).toHaveBeenCalledOnce(); + }); + + it('allows an explicit false override for a redacted job', () => { + const span = fakeSpan(); + const context = { + job: { enableRedaction: true }, + } as unknown as JobContext; + + runWithJobContext(context, () => captureException(span, { redacted: false })); + + expect(span.recordException).toHaveBeenCalledOnce(); + expect(span.setStatus).toHaveBeenCalledWith({ + code: SpanStatusCode.ERROR, + message: 'secret transcript', + }); + }); +}); diff --git a/agents/src/telemetry/utils.ts b/agents/src/telemetry/utils.ts index 4eba4f043..c945eaaae 100644 --- a/agents/src/telemetry/utils.ts +++ b/agents/src/telemetry/utils.ts @@ -2,11 +2,41 @@ // // SPDX-License-Identifier: Apache-2.0 import { type Span, SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api'; +import { getJobContext } from '../job.js'; 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 { +export const REDACTED_EXCEPTION_MESSAGE = 'exception details redacted'; + +export interface RecordExceptionOptions { + /** + * Whether to omit exception messages and stack traces from telemetry. Defaults to the current + * job's redaction setting. + */ + redacted?: boolean; +} + +export function recordException( + span: Span, + error: Error, + options: RecordExceptionOptions = {}, +): void { + const redacted = options.redacted ?? getJobContext(false)?.job.enableRedaction ?? false; + if (redacted) { + const attrs = { + [traceTypes.ATTR_EXCEPTION_TYPE]: error.constructor.name, + [traceTypes.ATTR_EXCEPTION_MESSAGE]: REDACTED_EXCEPTION_MESSAGE, + }; + span.addEvent('exception', attrs); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: REDACTED_EXCEPTION_MESSAGE, + }); + span.setAttributes(attrs); + return; + } + span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, diff --git a/agents/src/tts/fallback_adapter.ts b/agents/src/tts/fallback_adapter.ts index 903231bd7..375785c8b 100644 --- a/agents/src/tts/fallback_adapter.ts +++ b/agents/src/tts/fallback_adapter.ts @@ -157,7 +157,8 @@ export class FallbackAdapter extends TTS { const tts = this.ttsInstances[index]!; if (this.sampleRate !== tts.sampleRate) { this._logger.debug( - `resampling ${tts.label} from ${tts.sampleRate}Hz to ${this.sampleRate}Hz`, + { tts: tts.label, inputSampleRate: tts.sampleRate, outputSampleRate: this.sampleRate }, + 'resampling TTS audio', ); return new AudioResampler(tts.sampleRate, this.sampleRate, tts.numChannels); } @@ -207,7 +208,10 @@ export class FallbackAdapter extends TTS { if (controller.signal.aborted) { return; } - this._logger.debug({ tts: tts.label, error }, 'TTS recovery failed, will retry'); + this._logger.debug( + { tts: tts.label, 'lk.pii.error': error }, + 'TTS recovery failed, will retry', + ); // Retry recovery after delay (matches Python's retry behavior) const timeoutId = setTimeout(() => { this._recoveryTimeouts.delete(index); @@ -384,7 +388,10 @@ class FallbackChunkedStream extends ChunkedStream { return; } catch (error) { if (error instanceof APIError || error instanceof APIConnectionError) { - this._logger.warn({ tts: tts.label, error }, 'TTS failed, switching to next instance'); + this._logger.warn( + { tts: tts.label, 'lk.pii.error': error }, + 'TTS failed, switching to next instance', + ); this.adapter.markUnAvailable(i); } else { throw error; @@ -432,7 +439,7 @@ class FallbackSynthesizeStream extends SynthesizeStream { this.tokenBuffer.push(input); } } catch (error) { - this._logger.debug({ error }, 'Error reading input LLM stream'); + this._logger.debug({ 'lk.pii.error': error }, 'Error reading input LLM stream'); throw error; } finally { this.tokenBuffer.push(SynthesizeStream.END_OF_STREAM); @@ -599,7 +606,7 @@ class FallbackSynthesizeStream extends SynthesizeStream { if (error instanceof APIError || error instanceof APIConnectionError) { this._logger.warn( - { tts: originalTts.label, error }, + { tts: originalTts.label, 'lk.pii.error': error }, 'TTS failed, switching to next instance', ); this.adapter.markUnAvailable(i); diff --git a/agents/src/tts/tts.ts b/agents/src/tts/tts.ts index 6851bc64d..757fb27f7 100644 --- a/agents/src/tts/tts.ts +++ b/agents/src/tts/tts.ts @@ -296,8 +296,13 @@ export abstract class SynthesizeStream // Don't emit error event for recoverable errors during retry loop // to avoid ERR_UNHANDLED_ERROR or premature session termination this.logger.warn( - { tts: this.#tts.label, attempt: i + 1, error }, - `failed to synthesize speech, retrying in ${retryInterval}ms`, + { + tts: this.#tts.label, + attempt: i + 1, + retryIntervalMs: retryInterval, + 'lk.pii.error': error, + }, + 'failed to synthesize speech, retrying', ); } @@ -701,8 +706,13 @@ export abstract class ChunkedStream implements AsyncIterableIterator 0) { diff --git a/agents/src/utils.ts b/agents/src/utils.ts index 85e351e9c..9baad6654 100644 --- a/agents/src/utils.ts +++ b/agents/src/utils.ts @@ -516,7 +516,7 @@ export class Task { try { callback(); } catch (error) { - this.#logger.error({ error }, 'Task done callback failed'); + this.#logger.error({ 'lk.pii.error': error }, 'Task done callback failed'); } } this.doneCallbacks.clear(); @@ -550,7 +550,7 @@ export class Task { private async runTask() { const run = async () => { if (this.name) { - this.#logger.debug(`Task.runTask: task ${this.name} started`); + this.#logger.debug({ taskName: this.name }, 'Task.runTask: task started'); } return await this.fn(this.controller); }; @@ -566,7 +566,7 @@ export class Task { }) .finally(() => { if (this.name) { - this.#logger.debug(`Task.runTask: task ${this.name} done`); + this.#logger.debug({ taskName: this.name }, 'Task.runTask: task done'); } }); } diff --git a/agents/src/vad.ts b/agents/src/vad.ts index c0a2e8c16..68039beac 100644 --- a/agents/src/vad.ts +++ b/agents/src/vad.ts @@ -150,7 +150,7 @@ export abstract class VADStream implements AsyncIterableIterator { await this.inputWriter.write(value); } } catch (e) { - this.logger.error(`Error pumping deferred stream: ${e}`); + this.logger.error({ 'lk.pii.error': e }, 'error pumping deferred stream'); throw e; } finally { reader.releaseLock(); diff --git a/agents/src/voice/agent.ts b/agents/src/voice/agent.ts index 05536663b..322161e68 100644 --- a/agents/src/voice/agent.ts +++ b/agents/src/voice/agent.ts @@ -667,7 +667,10 @@ export class AgentTask extends Agent extends Agent { - this.logger.warn({ err }, 'error while disabling interruption detection'); + this.logger.warn({ 'lk.pii.error': err }, 'error while disabling interruption detection'); }); } this.logger.info( { - error: error?.message, + 'lk.pii.error': error?.message, label: error?.label, }, 'adaptive interruption disabled due to unrecoverable error, falling back to VAD-based interruption', @@ -4795,7 +4808,10 @@ export class AgentActivity implements RecognitionHooks { void activity .onToolsetToolsChanged() .catch((error) => - activity.logger.error({ error }, 'error re-advertising toolset tools'), + activity.logger.error( + { 'lk.pii.error': error }, + 'error re-advertising toolset tools', + ), ); }, }), @@ -4803,7 +4819,7 @@ export class AgentActivity implements RecognitionHooks { ); for (const output of outputs) { if (output.status === 'rejected') { - this.logger.error({ error: output.reason }, 'error setting up toolset'); + this.logger.error({ 'lk.pii.error': output.reason }, 'error setting up toolset'); } } } @@ -4812,7 +4828,7 @@ export class AgentActivity implements RecognitionHooks { const outputs = await Promise.allSettled(toolsets.map((ts) => ts.aclose())); for (const output of outputs) { if (output.status === 'rejected') { - this.logger.error({ error: output.reason }, 'error closing toolset'); + this.logger.error({ 'lk.pii.error': output.reason }, 'error closing toolset'); } } } diff --git a/agents/src/voice/agent_session.ts b/agents/src/voice/agent_session.ts index c317d019d..a937599d7 100644 --- a/agents/src/voice/agent_session.ts +++ b/agents/src/voice/agent_session.ts @@ -793,7 +793,11 @@ export class AgentSession< // Log used IO configuration this.logger.debug( - `using audio io: ${this.input.audio ? '`' + this.input.audio.constructor.name + '`' : '(none)'} -> \`AgentSession\` -> ${this.output.audio ? '`' + this.output.audio.constructor.name + '`' : '(none)'}`, + { + audioInput: this.input.audio?.constructor.name, + audioOutput: this.output.audio?.constructor.name, + }, + 'using audio io', ); if ( @@ -810,7 +814,8 @@ export class AgentSession< } this.logger.debug( - `using transcript io: \`AgentSession\` -> ${this.output.transcription ? '`' + this.output.transcription.constructor.name + '`' : '(none)'}`, + { transcriptionOutput: this.output.transcription?.constructor.name }, + 'using transcript io', ); this.started = true; @@ -1148,7 +1153,7 @@ export class AgentSession< throw error; } this.logger.debug( - { error }, + { 'lk.pii.error': error }, 'generateReply scheduling raced with handoff drain; retrying on next activity', ); return nextActivity.generateReply({ userMessage, ...options, inputDetails }); @@ -1709,7 +1714,7 @@ export class AgentSession< try { await this.activity.interrupt({ force: true }).await; } catch (error) { - this.logger.warn({ error }, 'Error interrupting activity'); + this.logger.warn({ 'lk.pii.error': error }, 'Error interrupting activity'); } } @@ -1775,6 +1780,6 @@ export class AgentSession< await this._roomIO?.close(); this._roomIO = undefined; - this.logger.info({ reason, error }, 'AgentSession closed'); + this.logger.info({ reason, 'lk.pii.error': error }, 'AgentSession closed'); } } diff --git a/agents/src/voice/amd.ts b/agents/src/voice/amd.ts index 572ff9522..a17f9e3e5 100644 --- a/agents/src/voice/amd.ts +++ b/agents/src/voice/amd.ts @@ -230,8 +230,8 @@ function warnIfNotEvaluated( if (lower === c || c.includes(lower) || lower.includes(c)) return; } log().warn( - `${modelKind} model ${modelName} hasn't been evaluated with our benchmark, ` + - 'it might not be compatible with amd. Set `suppressCompatibilityWarning: true` to silence this warning.', + { modelKind, model: modelName }, + 'model has not been evaluated with the AMD benchmark and may be incompatible', ); } @@ -459,14 +459,14 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) try { await this.stt.close(); } catch (e) { - this._log.warn({ err: e }, 'AMD failed to close owned STT'); + this._log.warn({ 'lk.pii.error': e }, 'AMD failed to close owned STT'); } } if (this.llmOwned) { try { await this.llm.aclose(); } catch (e) { - this._log.warn({ err: e }, 'AMD failed to close owned LLM'); + this._log.warn({ 'lk.pii.error': e }, 'AMD failed to close owned LLM'); } } } @@ -544,7 +544,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) this.sttPumpTask = Task.from(({ signal }) => this.runSTTPump(signal)); this.sttPumpTask.result.catch((err) => { if (this.settled) return; - this._log.warn({ err }, 'AMD dedicated STT pump exited with error'); + this._log.warn({ 'lk.pii.error': err }, 'AMD dedicated STT pump exited with error'); }); } @@ -610,7 +610,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } } catch (err) { if (this.settled) return; - this._log.debug({ err }, 'AMD dedicated STT receive pump error'); + this._log.debug({ 'lk.pii.error': err }, 'AMD dedicated STT receive pump error'); } })(); @@ -697,7 +697,10 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) // Otherwise the SIP participant disconnected before going active: no audio // remains, so fall through and let the no-speech timer settle AMD instead // of stranding it until the detection timeout. - this._log.debug({ err }, 'AMD SIP answer wait failed; starting to listen'); + this._log.debug( + { 'lk.pii.error': err }, + 'AMD SIP answer wait failed; starting to listen', + ); } if (!this.settled) { @@ -712,7 +715,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) if (this.trackGateAbort?.signal.aborted) { return; } - this._log.debug({ err }, 'AMD listening gate failed; starting immediately'); + this._log.debug({ 'lk.pii.error': err }, 'AMD listening gate failed; starting immediately'); if (!this.settled) { this.startListening(); } @@ -818,7 +821,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) isMachine: result.isMachine, speechDurationMs: result.speechDurationMs, delayMs: result.delayMs, - transcript: result.transcript, + 'lk.pii.transcript': result.transcript, }, 'amd prediction', ); @@ -839,7 +842,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } )._onAmdPrediction?.(result); } catch (err) { - this._log.debug({ err }, 'AMD: session host failed to handle amd_prediction'); + this._log.debug({ 'lk.pii.error': err }, 'AMD: session host failed to handle amd_prediction'); } this.emit('amd_prediction', result); } @@ -869,7 +872,10 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) return false; } this._log.debug( - { category: this.verdictResult.category, transcript: info.newTranscript }, + { + category: this.verdictResult.category, + 'lk.pii.transcript': info.newTranscript, + }, 'skipping auto reply: AMD already returned a machine verdict', ); return true; @@ -1318,7 +1324,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) abortSignal: new AbortController().signal, }); } catch (error) { - this._log.warn({ error, toolName: tc.name }, 'AMD tool execution failed'); + this._log.warn({ 'lk.pii.error': error, toolName: tc.name }, 'AMD tool execution failed'); } } diff --git a/agents/src/voice/audio_recognition.ts b/agents/src/voice/audio_recognition.ts index d11ed4680..211d81440 100644 --- a/agents/src/voice/audio_recognition.ts +++ b/agents/src/voice/audio_recognition.ts @@ -64,7 +64,7 @@ import { setParticipantSpanAttributes, } from './utils.js'; -// Maximum number of chat items included in the `lk.chat_ctx` attribute of the +// Maximum number of chat items included in the `lk.pii.chat_ctx` attribute of the // `eou_detection` span (mirrors Python's `_EOU_MAX_HISTORY_TURNS`). const EOU_MAX_HISTORY_TURNS = 6; @@ -459,7 +459,7 @@ export class AudioRecognition { if (!this.warnedTurnDetectorPushFailure) { this.warnedTurnDetectorPushFailure = true; this.logger.warn( - { err: err instanceof Error ? err.message : String(err) }, + { 'lk.pii.error': err }, 'audio EOT stream pushAudio failed; dropping frames for this turn', ); } @@ -644,14 +644,14 @@ export class AudioRecognition { this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error(`Error running VAD task: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error running VAD task'); }); this.interruptionTask = Task.from(({ signal }) => this.createInterruptionTask(this.interruptionDetection, signal), ); this.interruptionTask.result.catch((err) => { - this.logger.error(`Error running interruption task: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error running interruption task'); }); // Open (or adopt) the audio EOT detector stream now that the activity is @@ -983,9 +983,7 @@ export class AudioRecognition { await this.interruptionStreamChannel.write(frame); return true; } catch (e: unknown) { - this.logger.warn( - `could not forward interruption sentinel: ${e instanceof Error ? e.message : String(e)}`, - ); + this.logger.warn({ 'lk.pii.error': e }, 'could not forward interruption sentinel'); } } return false; @@ -1108,7 +1106,7 @@ export class AudioRecognition { this.logger.debug( { - user_transcript: transcript, + 'lk.pii.user_transcript': transcript, language: this.lastLanguage, }, 'received user transcript', @@ -1132,7 +1130,7 @@ export class AudioRecognition { if (this.vadBaseTurnDetection || this.userTurnCommitted) { if (transcriptChanged) { this.logger.debug( - { transcript: this.audioTranscript }, + { 'lk.pii.transcript': this.audioTranscript }, 'triggering preemptive generation (FINAL_TRANSCRIPT)', ); this.hooks.onPreemptiveGeneration({ @@ -1176,7 +1174,7 @@ export class AudioRecognition { this.logger.debug( { - user_transcript: preflightTranscript, + 'lk.pii.user_transcript': preflightTranscript, language: this.lastLanguage, }, 'received user preflight transcript', @@ -1198,7 +1196,7 @@ export class AudioRecognition { const confidenceVals = [...this.finalTranscriptConfidence, preflightConfidence]; this.logger.debug( { - transcript: + 'lk.pii.transcript': this.audioPreflightTranscript.length > 100 ? this.audioPreflightTranscript.slice(0, 100) + '...' : this.audioPreflightTranscript, @@ -1216,7 +1214,10 @@ export class AudioRecognition { } break; case SpeechEventType.INTERIM_TRANSCRIPT: - this.logger.debug({ transcript: ev.alternatives?.[0]?.text }, 'interim transcript'); + this.logger.debug( + { 'lk.pii.transcript': ev.alternatives?.[0]?.text }, + 'interim transcript', + ); this.hooks.onInterimTranscript( ev, this.hasUserVad || this.turnDetectionMode === 'stt' ? this.speaking : undefined, @@ -1361,7 +1362,7 @@ export class AudioRecognition { this.logger.debug( { stt: this.stt, - audioTranscript: this.audioTranscript, + 'lk.pii.audio_transcript': this.audioTranscript, turnDetectionMode: this.turnDetectionMode, }, 'running EOU detection', @@ -1416,7 +1417,10 @@ export class AudioRecognition { if (turnDetector) { if (!(await turnDetector.supportsLanguage(this.lastLanguage))) { // Unsupported language: produce no span and emit no prediction event. - this.logger.debug(`Turn detector does not support language ${this.lastLanguage}`); + this.logger.debug( + { language: this.lastLanguage }, + 'turn detector does not support language', + ); } else { await tracer.startActiveSpan( async (span) => { @@ -1632,7 +1636,7 @@ export class AudioRecognition { return; } - this.logger.debug({ transcript: this.audioTranscript }, 'end of user turn'); + this.logger.debug({ 'lk.pii.transcript': this.audioTranscript }, 'end of user turn'); const confidenceAvg = this.finalTranscriptConfidence.length > 0 @@ -1777,12 +1781,12 @@ export class AudioRecognition { this.sttForwardTask = Task.from(({ signal }) => this.forwardInputAudioToStt(pipeline, signal)); this.sttForwardTask.result.catch((err) => { - this.logger.error(`Error forwarding audio to STT pipeline: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error forwarding audio to STT pipeline'); }); this.sttConsumerTask = Task.from(({ signal }) => this.consumeSttEvents(pipeline, signal)); this.sttConsumerTask.result.catch((err) => { - this.logger.error(`Error running STT task: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error running STT task'); }); } @@ -2054,8 +2058,12 @@ export class AudioRecognition { ), ); this.logger.warn( - { model: interruptionDetection.label, attempt: numRetries }, - `failed to detect interruption, retrying in ${retryInterval}ms`, + { + model: interruptionDetection.label, + attempt: numRetries, + retryIntervalMs: retryInterval, + }, + 'failed to detect interruption, retrying', ); numRetries++; await delay(retryInterval, { signal }); @@ -2071,7 +2079,7 @@ export class AudioRecognition { } finally { await cleanup(); await forwardTask?.catch((e) => { - this.logger.debug({ err: e }, 'interruption task exited with error'); + this.logger.debug({ 'lk.pii.error': e }, 'interruption task exited with error'); }); } } @@ -2165,7 +2173,7 @@ export class AudioRecognition { }; void restartStt().catch((err) => { - this.logger.error(`Error resetting STT task: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error resetting STT task'); }); } @@ -2181,7 +2189,7 @@ export class AudioRecognition { if (this.closed) return; this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error(`Error running VAD task: ${err}`); + this.logger.error({ 'lk.pii.error': err }, 'error running VAD task'); }); }); } diff --git a/agents/src/voice/audio_recognition_span.test.ts b/agents/src/voice/audio_recognition_span.test.ts index fe6fc86fe..365519e70 100644 --- a/agents/src/voice/audio_recognition_span.test.ts +++ b/agents/src/voice/audio_recognition_span.test.ts @@ -180,7 +180,7 @@ describe('AudioRecognition user_turn span', () => { expect(userTurn.attributes['gen_ai.provider.name']).toBe('deepgram'); // end-of-turn attributes - expect(userTurn.attributes['lk.user_transcript']).toContain('hello'); + expect(userTurn.attributes['lk.pii.user_transcript']).toContain('hello'); expect(userTurn.attributes['lk.transcript_confidence']).toBeGreaterThan(0); }); @@ -258,7 +258,7 @@ describe('AudioRecognition user_turn span', () => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items = JSON.parse(String(eou.attributes['lk.chat_ctx'])).items as any[]; + const items = JSON.parse(String(eou.attributes['lk.pii.chat_ctx'])).items as any[]; expect(items.length).toBeLessThanOrEqual(6); // only non-empty, non-system messages survive; the pending user transcript is last expect(items.every((i) => i.type === 'message')).toBe(true); diff --git a/agents/src/voice/avatar/avatar_session.test.ts b/agents/src/voice/avatar/avatar_session.test.ts index 2fe387f25..c683b0922 100644 --- a/agents/src/voice/avatar/avatar_session.test.ts +++ b/agents/src/voice/avatar/avatar_session.test.ts @@ -180,7 +180,7 @@ describe('AvatarSession base', () => { expect(removeParticipantMock).toHaveBeenCalledWith('test-room', 'avatar-identity'); expect(warn).toHaveBeenCalledWith( - { error, identity: 'avatar-identity' }, + { 'lk.pii.error': error, identity: 'avatar-identity' }, 'failed to remove avatar participant', ); expect(debug).not.toHaveBeenCalledWith( diff --git a/agents/src/voice/avatar/avatar_session.ts b/agents/src/voice/avatar/avatar_session.ts index 9aadd6b73..0b29da470 100644 --- a/agents/src/voice/avatar/avatar_session.ts +++ b/agents/src/voice/avatar/avatar_session.ts @@ -137,7 +137,7 @@ export class AvatarSession extends (EventEmitter as new () => TypedEmitter TypedEmitter { if (!abortController.signal.aborted) { - this.#logger.warn({ error: String(error) }, 'failed while waiting for avatar participant'); + this.#logger.warn({ 'lk.pii.error': error }, 'failed while waiting for avatar participant'); } }); } diff --git a/agents/src/voice/background_audio.ts b/agents/src/voice/background_audio.ts index ea723e371..4aff206bc 100644 --- a/agents/src/voice/background_audio.ts +++ b/agents/src/voice/background_audio.ts @@ -376,7 +376,7 @@ export class BackgroundAudioPlayer { ); this.publication = publication; - this.#logger.debug(`Background audio track published: ${this.publication.sid}`); + this.#logger.debug({ trackSid: this.publication.sid }, 'Background audio track published'); } private async runMixerTask(): Promise { diff --git a/agents/src/voice/generation.ts b/agents/src/voice/generation.ts index 5e951dd8c..dbec023f1 100644 --- a/agents/src/voice/generation.ts +++ b/agents/src/voice/generation.ts @@ -346,7 +346,7 @@ export class _JsOutput { callId: this.toolCall.callId, function: this.toolCall.name, }, - `AI function ${this.toolCall.name} returned an invalid output`, + 'AI function returned an invalid output', ); return _SanitizedOutput.create({ toolCall: FunctionCall.create({ ...this.toolCall }), @@ -431,9 +431,10 @@ export function createToolOutput(params: { logger.error( { callId: toolCall.callId, - output: finalOutput, + function: toolCall.name, + 'lk.pii.output': finalOutput, }, - `AI function ${toolCall.name} returned an invalid output`, + 'AI function returned an invalid output', ); return ToolExecutionOutput.create({ toolCall: FunctionCall.create({ ...toolCall }), @@ -653,7 +654,7 @@ export function performLLMInference( return; } // surface inference silent errors even when this task's rejection is never awaited - logger.error({ error }, 'error in llm node'); + logger.error({ 'lk.pii.error': error }, 'error in llm node'); throw error; } finally { span.setAttribute(traceTypes.ATTR_RESPONSE_TEXT, data.generatedText); @@ -965,7 +966,7 @@ async function forwardAudio( let resampler: AudioResampler | null = null; const cancelReader = () => { void reader.cancel().catch((error) => { - logger.debug({ error }, 'failed to cancel TTS stream reader after abort'); + logger.debug({ 'lk.pii.error': error }, 'failed to cancel TTS stream reader after abort'); }); }; signal?.addEventListener('abort', cancelReader, { once: true }); @@ -1172,7 +1173,7 @@ export function performToolExecutions({ function: toolCall.name, speech_id: speechHandle.id, }, - `unknown AI function ${toolCall.name}`, + 'unknown AI function', ); toolCompleted( createToolOutput({ @@ -1188,8 +1189,9 @@ export function performToolExecutions({ { function: toolCall.name, speech_id: speechHandle.id, + toolType: typeof tool, }, - `unknown tool type: ${typeof tool}`, + 'unknown tool type', ); continue; } @@ -1220,11 +1222,11 @@ export function performToolExecutions({ logger.error( { function: toolCall.name, - arguments: toolCall.args, + 'lk.pii.arguments': toolCall.args, speech_id: speechHandle.id, - error: error.message, + 'lk.pii.error': error.message, }, - `tried to call AI function ${toolCall.name} with invalid arguments`, + 'tried to call AI function with invalid arguments', ); // Surface argument-validation errors to the LLM via ToolError so it can correct // its arguments instead of looping on the same invalid call. The argument schema @@ -1250,7 +1252,7 @@ export function performToolExecutions({ logger.info( { function: toolCall.name, - arguments: parsedArgs, + 'lk.pii.arguments': parsedArgs, speech_id: speechHandle.id, }, 'Executing LLM tool call', @@ -1287,7 +1289,7 @@ export function performToolExecutions({ { function: toolCall.name, speech_id: speechHandle.id, - error: toError(rawError).message, + 'lk.pii.error': toError(rawError).message, }, 'exception occurred while executing tool', ); @@ -1341,7 +1343,7 @@ export function performToolExecutions({ logger.debug( { function: toolCall.name, - arguments: parsedArgs, + 'lk.pii.arguments': parsedArgs, speech_id: speechHandle.id, }, 'executing mock tool', diff --git a/agents/src/voice/keyterm_detection.ts b/agents/src/voice/keyterm_detection.ts index 317a3ef66..204411127 100644 --- a/agents/src/voice/keyterm_detection.ts +++ b/agents/src/voice/keyterm_detection.ts @@ -139,7 +139,7 @@ function resolveDetectionLLM(configured?: LLM | string): LLM | undefined { return InferenceLLM.fromModelString(model); } catch { // never let detection setup break the session - log().warn(`keyterm detection: could not create detection LLM ${model}; skipping`); + log().warn({ model }, 'keyterm detection: could not create detection LLM; skipping'); return undefined; } } @@ -532,7 +532,7 @@ export async function detectKeyterms( } if (raced === timedOut) { stream.close(); - log().warn(`keyterm detection: pass timed out after ${timeout}ms; skipping`); + log().warn({ timeoutMs: timeout }, 'keyterm detection: pass timed out; skipping'); return [[], [], []]; } const result = parseToolCall(raced.toolCalls); @@ -625,14 +625,12 @@ export function parseToolCall(toolCalls: FunctionCall[]): [string[], string[], s function debugDump(userMsg: string, result: [string[], string[], string[]]): void { const [pending, confirm, remove] = result; log().debug( - [ - '──────── keyterm detection ────────', - userMsg, - '──── output ────', - `pending: ${JSON.stringify(pending)}`, - `confirm: ${JSON.stringify(confirm)}`, - `remove: ${JSON.stringify(remove)}`, - '───────────────────────────────────', - ].join('\n'), + { + 'lk.pii.user_message': userMsg, + 'lk.pii.pending_keyterms': pending, + 'lk.pii.confirmed_keyterms': confirm, + 'lk.pii.removed_keyterms': remove, + }, + 'keyterm detection result', ); } diff --git a/agents/src/voice/recorder_io/recorder_io.ts b/agents/src/voice/recorder_io/recorder_io.ts index d60856341..364ec53c4 100644 --- a/agents/src/voice/recorder_io/recorder_io.ts +++ b/agents/src/voice/recorder_io/recorder_io.ts @@ -156,7 +156,10 @@ export class RecorderIO { await this.outChan.write([]); } catch (err) { if (!isWritableStreamClosedError(err)) { - this.logger.error({ err }, 'Error writing final RecorderIO input buffer'); + this.logger.error( + { 'lk.pii.error': err }, + 'Error writing final RecorderIO input buffer', + ); } } } @@ -194,12 +197,12 @@ export class RecorderIO { const inputBuf = this.inRecord!.takeBuf(this.outRecord?._lastSpeechEndTime); this.inChan.write(inputBuf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ err }, 'Error writing RecorderIO input buffer'); + this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO input buffer'); } }); this.outChan.write(buf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO output buffer'); } }); } @@ -255,7 +258,7 @@ export class RecorderIO { break; } - this.logger.error({ err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO output buffer'); } } } @@ -286,7 +289,7 @@ export class RecorderIO { if (isFfmpegTeardownError(err)) { resolve(); } else { - this.logger.error({ err }, 'FFmpeg encoding error'); + this.logger.error({ 'lk.pii.error': err }, 'FFmpeg encoding error'); reject(err); } }) @@ -357,7 +360,8 @@ export class RecorderIO { const diff = Math.abs(leftSamples.length - rightSamples.length); if (leftSamples.length < rightSamples.length) { this.logger.warn( - `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`, + { missingSamples: diff }, + 'input is shorter; silence has been prepended to align the input channel', ); const padded = new Float32Array(rightSamples.length); padded.set(leftSamples, diff); @@ -428,7 +432,7 @@ export class RecorderIO { await this.ffmpegPromise; } } catch (err) { - this.logger.error({ err }, 'Error in encode task'); + this.logger.error({ 'lk.pii.error': err }, 'Error in encode task'); } finally { try { inReader.releaseLock(); diff --git a/agents/src/voice/remote_session.ts b/agents/src/voice/remote_session.ts index 71d6a2d0c..d7262cc9c 100644 --- a/agents/src/voice/remote_session.ts +++ b/agents/src/voice/remote_session.ts @@ -128,7 +128,7 @@ export class RoomSessionTransport extends SessionTransport { return; } this.readStream(reader).catch((e) => { - log().warn({ error: e }, 'failed to read binary stream message'); + log().warn({ 'lk.pii.error': e }, 'failed to read binary stream message'); }); }; @@ -149,7 +149,7 @@ export class RoomSessionTransport extends SessionTransport { this.enqueue(msg); } catch (e) { if (!this.closed) { - log().warn({ error: e }, 'failed to parse binary stream message'); + log().warn({ 'lk.pii.error': e }, 'failed to parse binary stream message'); } } } @@ -171,7 +171,7 @@ export class RoomSessionTransport extends SessionTransport { await writer.write(new Uint8Array(data)); await writer.close(); } catch (e) { - log().warn({ error: e }, 'failed to send binary stream message'); + log().warn({ 'lk.pii.error': e }, 'failed to send binary stream message'); } } @@ -183,7 +183,7 @@ export class RoomSessionTransport extends SessionTransport { try { this.room.unregisterByteStreamHandler(TOPIC_SESSION_MESSAGES); } catch (e) { - log().debug({ error: e }, 'byte stream handler already unregistered'); + log().debug({ 'lk.pii.error': e }, 'byte stream handler already unregistered'); } this.handlerRegistered = false; } @@ -342,7 +342,7 @@ export class TcpSessionTransport extends SessionTransport { try { msg = pb.AgentSessionMessage.fromBinary(payload); } catch (e) { - log().warn({ error: e }, 'failed to parse TCP session message'); + log().warn({ 'lk.pii.error': e }, 'failed to parse TCP session message'); continue; } yield msg; @@ -350,7 +350,7 @@ export class TcpSessionTransport extends SessionTransport { } } catch (e) { if (!this.closed) { - log().warn({ error: e }, 'TCP session transport read error'); + log().warn({ 'lk.pii.error': e }, 'TCP session transport read error'); } } } @@ -734,7 +734,7 @@ export class SessionHost { } } catch (e) { if (this.started) { - log().warn({ error: e }, 'error processing session message'); + log().warn({ 'lk.pii.error': e }, 'error processing session message'); } } } @@ -919,7 +919,7 @@ export class SessionHost { try { await this.handleRequest(req); } catch (e) { - log().warn({ error: e, requestId: req.requestId }, 'error handling session request'); + log().warn({ 'lk.pii.error': e, requestId: req.requestId }, 'error handling session request'); try { const resp = new pb.AgentSessionMessage({ message: { @@ -932,7 +932,7 @@ export class SessionHost { }); await this.transport.sendMessage(resp); } catch (e) { - log().debug({ error: e }, 'failed to send error response'); + log().debug({ 'lk.pii.error': e }, 'failed to send error response'); } } } @@ -1256,7 +1256,7 @@ export class RemoteSession extends (EventEmitter as new () => TypedEventEmitter< } } catch (e) { if (this.started) { - this._logger.warn({ error: e }, 'error in RemoteSession recv loop'); + this._logger.warn({ 'lk.pii.error': e }, 'error in RemoteSession recv loop'); } } } diff --git a/agents/src/voice/report.ts b/agents/src/voice/report.ts index 134839ca2..97e37b056 100644 --- a/agents/src/voice/report.ts +++ b/agents/src/voice/report.ts @@ -113,7 +113,7 @@ function jsToPythonFieldName(key: string): string { * through; `extra` dicts are emitted verbatim (Python keeps `extra` as a free-form dict, so its * provider-supplied keys must not be converted). * - * @internal Exported so other Python-facing serialization boundaries (e.g. the `lk.chat_ctx` + * @internal Exported so other Python-facing serialization boundaries (e.g. the `lk.pii.chat_ctx` * span attribute) can emit the same snake_case wire shape as the session report. */ export function toSnakeCaseDeep(value: unknown): unknown { diff --git a/agents/src/voice/room_io/_output.ts b/agents/src/voice/room_io/_output.ts index a78caa9fe..e7123d926 100644 --- a/agents/src/voice/room_io/_output.ts +++ b/agents/src/voice/room_io/_output.ts @@ -207,7 +207,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error(error, 'failed to publish transcription'); + this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); } } @@ -274,7 +274,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error(error, 'failed to publish transcription'); + this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); } } } @@ -333,7 +333,7 @@ export class ParticipantLegacyTranscriptionOutput extends BaseParticipantTranscr }); } } catch (error) { - this.logger.error(error, 'failed to publish transcription'); + this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); } } diff --git a/agents/src/voice/room_io/room_io.ts b/agents/src/voice/room_io/room_io.ts index ed5315bf7..d4db364bb 100644 --- a/agents/src/voice/room_io/room_io.ts +++ b/agents/src/voice/room_io/room_io.ts @@ -286,7 +286,7 @@ export class RoomIO { private onUserInputTranscribed = (ev: UserInputTranscribedEvent) => { this.userTranscriptWriter.write(ev).catch((error) => { - this.logger.error({ error }, 'Failed to write transcript event to stream'); + this.logger.error({ 'lk.pii.error': error }, 'failed to write transcript event to stream'); }); }; @@ -365,7 +365,7 @@ export class RoomIO { }; readText().catch((error) => { - this.logger.error({ error }, 'Error reading text input'); + this.logger.error({ 'lk.pii.error': error }, 'error reading text input'); }); }; @@ -384,7 +384,7 @@ export class RoomIO { } } } catch (error) { - this.logger.error({ error }, 'Error processing transcript stream'); + this.logger.error({ 'lk.pii.error': error }, 'error processing transcript stream'); } } @@ -509,7 +509,7 @@ export class RoomIO { this.textStreamHandlerRegistered = true; } catch (error) { if (this.inputOptions.textEnabled) { - this.logger.warn(`text stream handler for topic "${TOPIC_CHAT}" already set, ignoring`); + this.logger.warn({ topic: TOPIC_CHAT }, 'text stream handler already set, ignoring'); } } } @@ -616,7 +616,7 @@ export class RoomIO { try { await this.userTranscriptWriter.close(); } catch (e) { - this.logger.debug({ error: e }, 'userTranscriptWriter already closed or errored'); + this.logger.debug({ 'lk.pii.error': e }, 'userTranscriptWriter already closed or errored'); } await this.forwardUserTranscriptTask?.cancelAndWait(); diff --git a/agents/src/voice/tool_executor.ts b/agents/src/voice/tool_executor.ts index b2e69c6be..f20f30cd3 100644 --- a/agents/src/voice/tool_executor.ts +++ b/agents/src/voice/tool_executor.ts @@ -355,9 +355,7 @@ export class ToolExecutor { if (settled) return; log().error( { tool: call.name, callId: call.callId, timeoutMs: DRAIN_TOOL_TIMEOUT_MS }, - `tool ${call.name} was cancelled but its execute() is still running after the deadline; it likely ` + - 'does not honor the abort signal. Observe the provided abortSignal in execute() so ' + - 'cancellation actually stops the work.', + 'cancelled tool is still running after the deadline; its execute() may not honor the abort signal', ); }, DRAIN_TOOL_TIMEOUT_MS); timer.unref?.(); @@ -431,7 +429,7 @@ export class ToolExecutor { this._replyTaskDone = false; this._replyTask = this.deliverReply(ctx.session).catch((error) => { log().warn( - { error }, + { 'lk.pii.error': error }, 'deliverReply failed; async tool result may not trigger a follow-up reply', ); }); @@ -513,7 +511,7 @@ export class ToolExecutor { { function: runCtx.functionCall.name, callId: runCtx.functionCall.callId, - error: exception, + 'lk.pii.error': exception, }, 'exception occurred while executing tool after a progress update', ); diff --git a/agents/src/voice/transcription/synchronizer.ts b/agents/src/voice/transcription/synchronizer.ts index 005b88727..c2d5b8489 100644 --- a/agents/src/voice/transcription/synchronizer.ts +++ b/agents/src/voice/transcription/synchronizer.ts @@ -205,7 +205,7 @@ class SegmentSynchronizerImpl { this.outputStreamWriter.close(); }) .catch((error) => { - this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl'); + this.logger.error({ 'lk.pii.error': error }, 'mainTask SegmentSynchronizerImpl'); }); } this.captureTask = this.captureTaskImpl(); diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 16722152e..4c5c49582 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -484,8 +484,14 @@ export class AgentServer { const delay = Math.min(retries * 2, 10); this.#logger.warn( - { error: e, retry_count: retries, max_retry: this.#opts.maxRetry }, - `failed to connect to LiveKit server (${this.#opts.wsURL}), retrying in ${delay} seconds: (${retries}/${this.#opts.maxRetry})`, + { + 'lk.pii.url': this.#opts.wsURL, + 'lk.pii.error': e, + retry_count: retries, + max_retry: this.#opts.maxRetry, + retryDelaySeconds: delay, + }, + 'failed to connect to LiveKit server, retrying', ); await new ThrowsPromise((resolve) => setTimeout(resolve, delay * 1000)); @@ -570,7 +576,8 @@ export class AgentServer { )) as unknown as ParticipantInfo; } catch (e) { this.#logger.fatal( - `participant with identity ${participantIdentity} not found in room ${roomName}`, + { participantIdentity, roomName, 'lk.pii.error': e }, + 'participant not found in room', ); throw e; } @@ -627,7 +634,7 @@ export class AgentServer { }); ws.addEventListener('error', (event) => { - this.#logger.error('worker error:', event.message); + this.#logger.error({ 'lk.pii.error': event.message }, 'worker WebSocket error'); }); ws.addEventListener('message', (event) => { @@ -644,7 +651,10 @@ export class AgentServer { wsData = `${wsData.slice(0, 128)}...(+${wsData.length - 128} more)`; } const type = typeof event.data; - this.#logger.warn({ type, ws_data: wsData }, `unexpected message type: ${type}`); + this.#logger.warn( + { type, 'lk.pii.ws_data': wsData }, + 'received unexpected worker message type', + ); return; } @@ -698,7 +708,7 @@ export class AgentServer { delete this.#pending[job.id]; task?.resolve(msg.message.value); } else { - this.#logger.child({ job }).warn('received assignment for unknown job ' + job.id); + this.#logger.child({ job }).warn('received assignment for unknown job'); } break; } @@ -794,7 +804,7 @@ export class AgentServer { ); }) .catch((e) => { - this.#logger.warn({ error: e }, 'failed to measure CPU load'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to measure CPU load'); }); }, UPDATE_LOAD_INTERVAL); @@ -853,7 +863,7 @@ export class AgentServer { this.#pending[req.id] = new PendingAssignment(); const timer = setTimeout(() => { - this.#logger.child({ req }).warn(`assignment for job ${req.id} timed out`); + this.#logger.child({ req }).warn('job assignment timed out'); return; }, ASSIGNMENT_TIMEOUT); const asgn = await this.#pending[req.id]?.promise.then(async (asgn) => { @@ -873,7 +883,9 @@ export class AgentServer { apiSecret: this.#opts.apiSecret, }); } catch (e) { - this.#logger.child({ requestId: req.id }).error(e, 'error launching job'); + this.#logger + .child({ requestId: req.id }) + .error({ 'lk.pii.error': e }, 'error launching job'); } } else { this.#logger.child({ requestId: req.id }).warn('pending assignment not found'); diff --git a/agents/src/workflows/warm_transfer.ts b/agents/src/workflows/warm_transfer.ts index fbf870dfa..b13fb5b3f 100644 --- a/agents/src/workflows/warm_transfer.ts +++ b/agents/src/workflows/warm_transfer.ts @@ -232,7 +232,7 @@ export function createWarmTransferTask({ holdAudioHandle = null; } void backgroundAudio.close().catch((error) => { - logger.warn({ error }, 'failed to close background audio'); + logger.warn({ 'lk.pii.error': error }, 'failed to close background audio'); }); setIoEnabled(true); @@ -270,7 +270,7 @@ export function createWarmTransferTask({ // Cap the wait so teardown can't hang on a stuck playout. await waitUntilAborted(handle.waitForPlayout(), AbortSignal.timeout(10_000)); } catch (error) { - logger.warn({ error }, 'failed to notify human agent of caller hangup'); + logger.warn({ 'lk.pii.error': error }, 'failed to notify human agent of caller hangup'); } finally { session.shutdown(); } @@ -338,7 +338,7 @@ export function createWarmTransferTask({ } const callerRoomName = callerRoom.name; void jobCtx.deleteRoom(callerRoomName).catch((error) => { - logger.warn({ error }, 'failed to delete caller room'); + logger.warn({ 'lk.pii.error': error }, 'failed to delete caller room'); }); }; @@ -347,10 +347,10 @@ export function createWarmTransferTask({ room?: Room | null, ): Promise => { await room?.disconnect().catch((error) => { - logger.warn({ error }, 'failed to disconnect human agent room'); + logger.warn({ 'lk.pii.error': error }, 'failed to disconnect human agent room'); }); await session?.close().catch((error) => { - logger.warn({ error }, 'failed to close transfer agent session'); + logger.warn({ 'lk.pii.error': error }, 'failed to close transfer agent session'); }); }; @@ -415,7 +415,10 @@ export function createWarmTransferTask({ canSubscribe: true, } as VideoGrant); - logger.debug({ wsUrl: ctx.info.url, humanAgentRoomName }, 'connecting to human agent room'); + logger.debug( + { 'lk.pii.ws_url': ctx.info.url, humanAgentRoomName }, + 'connecting to human agent room', + ); const jwt = await token.toJwt(); room.on(RoomEvent.Disconnected, onHumanAgentRoomClose); @@ -592,7 +595,7 @@ export function createWarmTransferTask({ } transferAgentSession = result.session; } catch (error) { - logger.error({ error }, 'could not dial human agent'); + logger.error({ 'lk.pii.error': error }, 'could not dial human agent'); setResult(new ToolError('could not dial human agent')); } finally { abortController.abort(); diff --git a/examples/src/frontdesk/calendar_api.test.ts b/examples/src/frontdesk/calendar_api.test.ts index 4e7a08fd2..a1585c0a2 100644 --- a/examples/src/frontdesk/calendar_api.test.ts +++ b/examples/src/frontdesk/calendar_api.test.ts @@ -10,6 +10,11 @@ import { getUniqueHash, } from './calendar_api.js'; +const infoMock = vi.hoisted(() => vi.fn()); +vi.mock('@livekit/agents', () => ({ + log: () => ({ child: () => ({ info: infoMock }) }), +})); + describe('Calendar API', () => { describe('createAvailableSlot', () => { it('should create a valid AvailableSlot', () => { @@ -167,6 +172,7 @@ describe('Calendar API', () => { let calendar: CalComCalendar; beforeEach(() => { + infoMock.mockClear(); calendar = new CalComCalendar({ apiKey: 'test-api-key', timezone: 'America/New_York', @@ -197,15 +203,14 @@ describe('Calendar API', () => { }), } as Response); - const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - await calendar.initialize(); expect(mockFetch).toHaveBeenCalledTimes(2); - expect(consoleSpy).toHaveBeenCalledWith('[cal.com] using cal.com username: testuser'); - expect(consoleSpy).toHaveBeenCalledWith('[cal.com] event type id: 123'); - - consoleSpy.mockRestore(); + expect(infoMock).toHaveBeenCalledWith( + { 'lk.pii.username': 'testuser' }, + 'using cal.com username', + ); + expect(infoMock).toHaveBeenCalledWith({ eventTypeId: 123 }, 'cal.com event type found'); }); it('should create new event type when not exists', async () => { @@ -232,18 +237,18 @@ describe('Calendar API', () => { }), } as Response); - const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); - await calendar.initialize(); expect(mockFetch).toHaveBeenCalledTimes(3); - expect(consoleSpy).toHaveBeenCalledWith('[cal.com] using cal.com username: testuser'); - expect(consoleSpy).toHaveBeenCalledWith( - '[cal.com] successfully added livekit-front-desk event type', + expect(infoMock).toHaveBeenCalledWith( + { 'lk.pii.username': 'testuser' }, + 'using cal.com username', ); - expect(consoleSpy).toHaveBeenCalledWith('[cal.com] event type id: 456'); - - consoleSpy.mockRestore(); + expect(infoMock).toHaveBeenCalledWith( + { eventType: 'livekit-front-desk' }, + 'successfully added cal.com event type', + ); + expect(infoMock).toHaveBeenCalledWith({ eventTypeId: 456 }, 'cal.com event type found'); }); it('should handle API errors', async () => { diff --git a/examples/src/frontdesk/calendar_api.ts b/examples/src/frontdesk/calendar_api.ts index cd6a8a477..b2ce54bd5 100644 --- a/examples/src/frontdesk/calendar_api.ts +++ b/examples/src/frontdesk/calendar_api.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 +import { log } from '@livekit/agents'; import { createHash } from 'crypto'; export class SlotUnavailableError extends Error { @@ -134,14 +135,11 @@ export class CalComCalendar implements Calendar { private tz: string; private _apiKey: string; private _lkEventId?: number; - private _logger: { info: (message: string) => void }; + private _logger = log().child({ provider: 'cal.com' }); constructor(options: { apiKey: string; timezone: string }) { this.tz = options.timezone; this._apiKey = options.apiKey; - this._logger = { - info: (message: string) => console.info(`[cal.com] ${message}`), - }; } async initialize(): Promise { @@ -155,7 +153,7 @@ export class CalComCalendar implements Calendar { const meData = await meResponse.json(); const username = meData.data.username; - this._logger.info(`using cal.com username: ${username}`); + this._logger.info({ 'lk.pii.username': username }, 'using cal.com username'); const params = new URLSearchParams({ username }); const eventTypesResponse = await fetch(`${_BASE_URL}event-types/?${params}`, { @@ -195,12 +193,15 @@ export class CalComCalendar implements Calendar { ); } - this._logger.info(`successfully added ${_CAL_COM_EVENT_TYPE} event type`); + this._logger.info( + { eventType: _CAL_COM_EVENT_TYPE }, + 'successfully added cal.com event type', + ); const createData = await createResponse.json(); this._lkEventId = createData.data.id; } - this._logger.info(`event type id: ${this._lkEventId}`); + this._logger.info({ eventTypeId: this._lkEventId }, 'cal.com event type found'); } async scheduleAppointment(options: { startTime: Date; attendeeEmail: string }): Promise { diff --git a/examples/src/idle_user_timeout_example.ts b/examples/src/idle_user_timeout_example.ts index d326c881b..fd438eaf6 100644 --- a/examples/src/idle_user_timeout_example.ts +++ b/examples/src/idle_user_timeout_example.ts @@ -64,7 +64,7 @@ export default defineAgent({ }; session.on(voice.AgentSessionEventTypes.UserStateChanged, (event) => { - logger.info({ event }, 'User state changed'); + logger.info({ oldState: event.oldState, newState: event.newState }, 'User state changed'); if (task) { task.cancel(); diff --git a/examples/src/instructions_per_modality.ts b/examples/src/instructions_per_modality.ts index f2ab6b910..b0744b591 100644 --- a/examples/src/instructions_per_modality.ts +++ b/examples/src/instructions_per_modality.ts @@ -64,7 +64,10 @@ class SchedulingAgent extends voice.Agent { time: z.string().describe('The time of the appointment in the format HH:MM'), }), execute: async ({ date, time }) => { - log().info(`booking appointment for ${date} at ${time}`); + log().info( + { 'lk.pii.appointment_date': date, 'lk.pii.appointment_time': time }, + 'booking appointment', + ); return `Appointment booked for ${date} at ${time}`; }, }), diff --git a/examples/src/liveavatar_avatar.ts b/examples/src/liveavatar_avatar.ts index bc428142d..d4f3063b2 100644 --- a/examples/src/liveavatar_avatar.ts +++ b/examples/src/liveavatar_avatar.ts @@ -67,7 +67,7 @@ export default defineAgent({ session.on(voice.AgentSessionEventTypes.UserInputTranscribed, (ev) => { logger.info( - { final: ev.isFinal, transcript: ev.transcript, language: ev.language }, + { final: ev.isFinal, 'lk.pii.transcript': ev.transcript, language: ev.language }, 'User transcript received', ); }); @@ -77,7 +77,7 @@ export default defineAgent({ }); session.on(voice.AgentSessionEventTypes.Error, (ev) => { - logger.error({ error: ev.error, source: ev.source }, 'Session emitted error'); + logger.error({ 'lk.pii.error': ev.error, source: ev.source }, 'Session emitted error'); }); ctx.addShutdownCallback(async () => { diff --git a/examples/src/play_local_audio_file.ts b/examples/src/play_local_audio_file.ts index cf80ffca8..f58c4d354 100644 --- a/examples/src/play_local_audio_file.ts +++ b/examples/src/play_local_audio_file.ts @@ -34,13 +34,13 @@ export default defineAgent({ await publication.waitForSubscription(); - logger.info(`Audio track published: ${publication?.sid}`); + logger.info({ trackSid: publication?.sid }, 'Audio track published'); const currentDir = dirname(fileURLToPath(import.meta.url)); const resourcesPath = join(currentDir, '../../agents/resources'); const audioFile = join(resourcesPath, 'office-ambience.ogg'); - logger.info(`Playing: ${audioFile}`); + logger.info({ 'lk.pii.audio_file': audioFile }, 'Playing audio file'); const abortController = new AbortController(); @@ -58,7 +58,10 @@ export default defineAgent({ frameCount++; if (frameCount % 100 === 0) { - logger.info(`Played ${frameCount} frames (${(frameCount * 0.1).toFixed(1)}s)`); + logger.info( + { frameCount, durationSeconds: Number((frameCount * 0.1).toFixed(1)) }, + 'Played audio frames', + ); } } }, diff --git a/examples/src/realtime_streaming_transcript.ts b/examples/src/realtime_streaming_transcript.ts index 00f525a31..0685a5f47 100644 --- a/examples/src/realtime_streaming_transcript.ts +++ b/examples/src/realtime_streaming_transcript.ts @@ -24,12 +24,12 @@ export default defineAgent({ let lastPartialLength = 0; session.on(voice.AgentSessionEventTypes.UserInputTranscribed, (ev) => { if (ev.isFinal) { - logger.info({ transcript: ev.transcript }, '[user transcript FINAL]'); + logger.info({ 'lk.pii.transcript': ev.transcript }, 'user transcript final'); lastPartialLength = 0; return; } if (ev.transcript.length - lastPartialLength >= 6) { - logger.info({ transcript: ev.transcript }, '[user transcript partial]'); + logger.info({ 'lk.pii.transcript': ev.transcript }, 'user transcript partial'); lastPartialLength = ev.transcript.length; } }); diff --git a/examples/src/realtime_with_tts.ts b/examples/src/realtime_with_tts.ts index d0e898630..d6f69b417 100644 --- a/examples/src/realtime_with_tts.ts +++ b/examples/src/realtime_with_tts.ts @@ -19,7 +19,7 @@ export default defineAgent({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => { - logger.info(`getting weather for ${location}`); + logger.info({ 'lk.pii.location': location }, 'getting weather'); return `The weather in ${location} is sunny, and the temperature is 20 degrees Celsius.`; }, }); diff --git a/examples/src/telephony_amd.ts b/examples/src/telephony_amd.ts index d22e8e3d1..c1fc80599 100644 --- a/examples/src/telephony_amd.ts +++ b/examples/src/telephony_amd.ts @@ -175,7 +175,7 @@ export default defineAgent({ try { await rooms.deleteRoom(roomName); } catch (err) { - logger.warn({ err }, 'failed to delete room during hangup'); + logger.warn({ 'lk.pii.error': err }, 'failed to delete room during hangup'); } }); }, diff --git a/examples/src/warm_transfer.ts b/examples/src/warm_transfer.ts index ce392a66a..a754cef81 100644 --- a/examples/src/warm_transfer.ts +++ b/examples/src/warm_transfer.ts @@ -75,11 +75,14 @@ Examples on when the tool should be called: ctx.session.shutdown(); } catch (error) { if (error instanceof llm.ToolError) { - logger.error({ error }, 'failed to transfer to human agent with tool error'); + logger.error( + { 'lk.pii.error': error }, + 'failed to transfer to human agent with tool error', + ); throw error; } - logger.error({ error }, 'failed to transfer to human agent'); + logger.error({ 'lk.pii.error': error }, 'failed to transfer to human agent'); throw new llm.ToolError(`failed to transfer to human agent with error: ${error}`); } }, diff --git a/plugins/anam/src/api.ts b/plugins/anam/src/api.ts index 9f9601e8c..c0c5cd447 100644 --- a/plugins/anam/src/api.ts +++ b/plugins/anam/src/api.ts @@ -64,10 +64,10 @@ export class AnamAPI { logger.debug( { - url, + 'lk.pii.url': url, method: 'POST', headers: redactedHeaders, - body: redactedBody, + 'lk.pii.body': redactedBody, attempt: attempt + 1, }, 'calling Anam API', @@ -83,32 +83,32 @@ export class AnamAPI { const text = await res.text(); logger.error( { - url, + 'lk.pii.url': url, method: 'POST', headers: redactedHeaders, - body: redactedBody, + 'lk.pii.body': redactedBody, status: res.status, - response: text, + 'lk.pii.response': text, }, 'Anam API request failed', ); throw new AnamException(`Anam ${path} failed: ${res.status} ${text}`); } const json = (await res.json()) as T; - logger.debug({ url }, 'Anam API request succeeded'); + logger.debug({ 'lk.pii.url': url }, 'Anam API request succeeded'); return json; } catch (e) { lastErr = e; if (attempt === maxRetry - 1) break; logger.warn( { - url, + 'lk.pii.url': url, method: 'POST', - body: + 'lk.pii.body': body && typeof body === 'object' ? { ...(body as Record), livekitToken: '****' } : body, - error: (e as Error)?.message, + 'lk.pii.error': (e as Error)?.message, nextRetrySec: retryInterval, }, 'Anam API error, retrying', diff --git a/plugins/anam/src/avatar.ts b/plugins/anam/src/avatar.ts index 90e038d33..cd63c461d 100644 --- a/plugins/anam/src/avatar.ts +++ b/plugins/anam/src/avatar.ts @@ -97,11 +97,11 @@ export class AvatarSession extends voice.AvatarSession { logger.debug( { - personaName: this.opts.personaConfig?.name, - avatarId: this.opts.personaConfig?.avatarId, - personaId: this.opts.personaConfig?.personaId, - apiUrl: apiUrl ?? '(default https://api.anam.ai)', - livekitUrl, + 'lk.pii.persona_name': this.opts.personaConfig?.name, + 'lk.pii.avatar_id': this.opts.personaConfig?.avatarId, + 'lk.pii.persona_id': this.opts.personaConfig?.personaId, + 'lk.pii.api_url': apiUrl ?? '(default https://api.anam.ai)', + 'lk.pii.livekit_url': livekitUrl, avatarParticipantIdentity: this.opts.avatarParticipantIdentity ?? 'anam-avatar-agent', publishOnBehalf: localIdentity, }, @@ -117,7 +117,7 @@ export class AvatarSession extends voice.AvatarSession { }); const anam = new AnamAPI(apiKey, apiUrl, this.opts.connOptions); - logger.debug({ livekitUrl }, 'requesting Anam session token'); + logger.debug({ 'lk.pii.livekit_url': livekitUrl }, 'requesting Anam session token'); const { sessionToken } = await anam.createSessionToken({ personaConfig: this.opts.personaConfig, diff --git a/plugins/assemblyai/src/stt.ts b/plugins/assemblyai/src/stt.ts index 421116ef3..59f49783d 100644 --- a/plugins/assemblyai/src/stt.ts +++ b/plugins/assemblyai/src/stt.ts @@ -161,7 +161,8 @@ export class STT extends stt.STT { const supportsCarryover = isU3ProModel(rawModel) || rawModel === 'u3-pro'; if (opts.agentContextCarryover && !supportsCarryover) { log().warn( - `agentContextCarryover is enabled but model '${rawModel}' does not support it; ignoring`, + { model: rawModel }, + 'agentContextCarryover is enabled but the model does not support it; ignoring', ); } super({ @@ -357,12 +358,23 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - `failed to connect to AssemblyAI, retrying in ${retryDelaySeconds} seconds: ${e} (${retries}/${maxRetry})`, + { + retryDelaySeconds, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'failed to connect to AssemblyAI, retrying', ); await delay(retryDelaySeconds * 1000); } else { this.#logger.warn( - `AssemblyAI disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'AssemblyAI disconnected', ); } } @@ -450,7 +462,13 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'AssemblyAI WebSocket closed unexpectedly', + ); reject(new Error('WebSocket closed')); } }); @@ -517,7 +535,13 @@ export class SpeechStream extends stt.SpeechStream { resolve(); } } catch (err) { - this.#logger.error(`AssemblyAI: error processing message: ${msg}`); + this.#logger.error( + { + 'lk.pii.error': err, + 'lk.pii.message': msg.toString(), + }, + 'AssemblyAI failed to process message', + ); reject(err); } }; @@ -574,7 +598,8 @@ export class SpeechStream extends stt.SpeechStream { this.#sessionId = data.id ?? null; this.#expiresAt = data.expires_at ?? null; this.#logger.info( - `AssemblyAI session started id=${this.#sessionId} expires_at=${this.#expiresAt}`, + { sessionId: this.#sessionId, expiresAt: this.#expiresAt }, + 'AssemblyAI session started', ); return; } @@ -586,7 +611,11 @@ export class SpeechStream extends stt.SpeechStream { if (messageType === 'Termination') { this.#logger.debug( - `AssemblyAI session terminated audio_duration=${data.audio_duration_seconds}s session_duration=${data.session_duration_seconds}s`, + { + audioDurationSeconds: data.audio_duration_seconds, + sessionDurationSeconds: data.session_duration_seconds, + }, + 'AssemblyAI session terminated', ); return; } diff --git a/plugins/azure/src/stt.ts b/plugins/azure/src/stt.ts index 0e1e76ccb..33ad3ea7f 100644 --- a/plugins/azure/src/stt.ts +++ b/plugins/azure/src/stt.ts @@ -401,9 +401,9 @@ export class SpeechStream extends stt.SpeechStream { { code: evt.errorCode, reason: evt.reason, - errorDetails: evt.errorDetails, + 'lk.pii.error_details': evt.errorDetails, }, - `Speech recognition canceled: ${evt.errorDetails || evt.reason}`, + 'Speech recognition canceled', ); this._cancellationError = evt; this._sessionStoppedEvent.set(); diff --git a/plugins/baseten/src/stt.ts b/plugins/baseten/src/stt.ts index 84dc7952c..1fe442460 100644 --- a/plugins/baseten/src/stt.ts +++ b/plugins/baseten/src/stt.ts @@ -138,12 +138,23 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - `failed to connect to Baseten, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, + { + delay, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'failed to connect to Baseten, retrying', ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - `Baseten disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'Baseten disconnected', ); } } @@ -242,7 +253,7 @@ export class SpeechStream extends stt.SpeechStream { // Skip if no transcript text if (!transcript) { - this.#logger.debug('Received non-transcript message:', msg); + this.#logger.debug({ 'lk.pii.message': msg }, 'Received non-transcript message'); return; } @@ -307,7 +318,13 @@ export class SpeechStream extends stt.SpeechStream { resolve(); } } catch (err) { - this.#logger.error(`STT: Error processing message: ${data}`); + this.#logger.error( + { + 'lk.pii.error': err, + 'lk.pii.message': String(data), + }, + 'Baseten STT failed to process message', + ); reject(err); } }); diff --git a/plugins/bey/src/avatar.ts b/plugins/bey/src/avatar.ts index ce23bb31e..eb702fc82 100644 --- a/plugins/bey/src/avatar.ts +++ b/plugins/bey/src/avatar.ts @@ -238,9 +238,9 @@ export class AvatarSession extends voice.AvatarSession { return; } catch (e) { if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call bey presence api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call bey presence api'); } else { - this.#logger.error({ error: e }, 'failed to call bey presence api'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call bey presence api'); } if (i < this.connOptions.maxRetry - 1) { diff --git a/plugins/cartesia/src/stt.ts b/plugins/cartesia/src/stt.ts index 9cb8ddd35..f5604cc0e 100644 --- a/plugins/cartesia/src/stt.ts +++ b/plugins/cartesia/src/stt.ts @@ -293,7 +293,7 @@ export class SpeechStream extends stt.SpeechStream { try { const url = this.#getCartesiaUrl(); - this.#logger.debug(`Connecting to Cartesia STT: ${url}`); + this.#logger.debug({ 'lk.pii.url': url }, 'connecting to Cartesia STT'); const ws = new WebSocket(url, { headers: { @@ -355,7 +355,7 @@ export class SpeechStream extends stt.SpeechStream { await Promise.all([sendPromise, recvPromise]); if (firstError !== undefined) throw firstError; } catch (error) { - this.#logger.error('Cartesia STT stream error', { error }); + this.#logger.error({ 'lk.pii.error': error }, 'Cartesia STT stream error'); throw error; } finally { abortController.abort(); @@ -419,7 +419,8 @@ export class SpeechStream extends stt.SpeechStream { // the stream wait forever. const drainTimer = setTimeout(() => { this.#logger.warn( - `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`, + { timeoutMs: DRAIN_TIMEOUT_MS }, + 'Cartesia STT did not close after done; forcing close', ); if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) { ws.terminate(); @@ -447,12 +448,12 @@ export class SpeechStream extends stt.SpeechStream { try { msg = JSON.parse(data.toString()); } catch (error) { - this.#logger.error('Failed to parse Cartesia STT message', { error }); + this.#logger.error({ 'lk.pii.error': error }, 'Failed to parse Cartesia STT message'); return; } try { if (msg.type === 'error') { - this.#logger.error('Cartesia sent an error', msg); + this.#logger.error({ 'lk.pii.message': msg }, 'Cartesia sent an error'); // do not close the websocket on bad requests since that may be caused by invalid messages if (msg.status_code === undefined || msg.status_code >= 500) { @@ -472,7 +473,7 @@ export class SpeechStream extends stt.SpeechStream { this.#processStreamEvent(msg); } } catch (error) { - this.#logger.error('Failed to process Cartesia STT message', { error }); + this.#logger.error({ 'lk.pii.error': error }, 'Failed to process Cartesia STT message'); } }); @@ -513,7 +514,13 @@ export class SpeechStream extends stt.SpeechStream { settle(); return; } - this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`); + this.#logger.warn( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Cartesia STT WebSocket closed', + ); settle( new APIConnectionError({ message: `Cartesia STT connection closed unexpectedly (code=${code})`, @@ -610,7 +617,7 @@ export class SpeechStream extends stt.SpeechStream { } default: - this.#logger.warn('received unexpected message from Cartesia STT', { data }); + this.#logger.warn({ 'lk.pii.data': data }, 'received unexpected message from Cartesia STT'); } } diff --git a/plugins/cartesia/src/tts.ts b/plugins/cartesia/src/tts.ts index 218e8f925..e6e197df3 100644 --- a/plugins/cartesia/src/tts.ts +++ b/plugins/cartesia/src/tts.ts @@ -120,8 +120,7 @@ const checkGenerationConfig = (opts: TTSOptions) => { if (opts.speed || opts.emotion) { logger.warn( { model: opts.model, speed: opts.speed, emotion: opts.emotion }, - `speed and emotion controls are only supported for model '${MODEL_WITH_EXPERIMENTAL_CONTROLS}' ` + - `or sonic-3 models, see https://docs.cartesia.ai/developer-tools/changelog for details`, + 'speed and emotion controls are only supported for sonic-2-2025-04-16 or sonic-3 models', ); } } @@ -353,7 +352,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Cartesia TTS response error'); + this.#logger.error({ 'lk.pii.error': err }, 'Cartesia TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -361,7 +360,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ err }, 'Cartesia TTS request error'); + this.#logger.error({ 'lk.pii.error': err }, 'Cartesia TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.on('close', () => { @@ -498,7 +497,10 @@ export class SynthesizeStream extends tts.SynthesizeStream { // Set up WebSocket listeners ONCE (not in a loop) const onMessage = (data: RawData) => { void eventChannel.write(data).catch((error: unknown) => { - this.#logger.debug({ error }, 'Failed writing Cartesia event to channel (likely closed)'); + this.#logger.debug( + { 'lk.pii.error': error }, + 'Failed writing Cartesia event to channel (likely closed)', + ); }); }; @@ -507,7 +509,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { // socket lifecycle and does not close it between turns. If it happens // before `done`, surface it so the turn retries rather than ending mid // speech, and so withConnection discards the dead socket. - this.#logger.debug(`WebSocket closed with code ${code}: ${reason.toString()}`); + this.#logger.debug( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Cartesia TTS WebSocket closed', + ); clearTTSChunkTimeout(); if (!completed && !timedOut && !streamError) { streamError = new APIConnectionError({ @@ -518,7 +526,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { }; const onError = (err: Error) => { - this.#logger.error({ err }, 'Cartesia WebSocket error'); + this.#logger.error({ 'lk.pii.error': err }, 'Cartesia WebSocket error'); if (!completed && !timedOut && !streamError) { streamError = err instanceof APIError ? err : toRetryableConnectionError(err); } @@ -546,7 +554,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { const json = JSON.parse(rawMsg.toString()); serverMsg = cartesiaMessageSchema.parse(json); } catch (parseErr) { - this.#logger.warn({ parseErr }, 'Failed to parse Cartesia message'); + this.#logger.warn({ 'lk.pii.error': parseErr }, 'Failed to parse Cartesia message'); continue; } @@ -558,9 +566,12 @@ export class SynthesizeStream extends tts.SynthesizeStream { // below. 5xx bubbles up so the base SynthesizeStream can retry. if (isErrorMessage(serverMsg)) { if (serverMsg.status_code >= 400 && serverMsg.status_code < 500) { - this.#logger.debug({ error: serverMsg.error }, 'Cartesia sent a non-fatal error'); + this.#logger.debug( + { 'lk.pii.error': serverMsg.error }, + 'Cartesia sent a non-fatal error', + ); } else { - this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error'); + this.#logger.error({ 'lk.pii.error': serverMsg.error }, 'Cartesia returned error'); throw new APIStatusError({ message: `Cartesia returned error: ${serverMsg.error}`, options: { statusCode: serverMsg.status_code, retryable: true }, @@ -587,7 +598,8 @@ export class SynthesizeStream extends tts.SynthesizeStream { timeout = setTimeout(() => { // cartesia chunk timeout quite often, so we make it a debug log this.#logger.debug( - `Cartesia WebSocket TTS chunk stream timeout after ${this.#opts.chunkTimeout}ms`, + { timeoutMs: this.#opts.chunkTimeout }, + 'Cartesia WebSocket TTS chunk stream timed out', ); // The socket is stuck mid-generation, so it must not return to the // pool. Poison it and unblock the reader; the post-loop check turns @@ -635,7 +647,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } else if (!isFlushDoneMessage(serverMsg) && !isErrorMessage(serverMsg)) { // flush_done is an ack with nothing to do; error frames without // done:true were already logged above. - this.#logger.warn({ message: serverMsg }, 'Unknown Cartesia message'); + this.#logger.warn({ 'lk.pii.message': serverMsg }, 'Unknown Cartesia message'); } } @@ -658,11 +670,14 @@ export class SynthesizeStream extends tts.SynthesizeStream { err.message.includes('Channel is closed') ) { this.#logger.warn( - { err }, + { 'lk.pii.error': err }, 'Channel closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ err }, 'Error in recvTask from Cartesia WebSocket'); + this.#logger.error( + { 'lk.pii.error': err }, + 'Error in recvTask from Cartesia WebSocket', + ); } } } finally { diff --git a/plugins/deepgram/src/stt.ts b/plugins/deepgram/src/stt.ts index df15f16be..edf78c727 100644 --- a/plugins/deepgram/src/stt.ts +++ b/plugins/deepgram/src/stt.ts @@ -234,7 +234,8 @@ export class STT extends stt.STT { ].includes(model) ) { this.#logger.warn( - `${model} does not support language ${language}, falling back to nova-2-general`, + { model, language, fallbackModel: 'nova-2-general' }, + 'model does not support language, falling back', ); return 'nova-2-general'; } @@ -339,12 +340,23 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - `failed to connect to Deepgram, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, + { + delay, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'failed to connect to Deepgram, retrying', ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - `Deepgram disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'Deepgram disconnected', ); } } @@ -391,7 +403,13 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise(async (_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Deepgram STT WebSocket closed unexpectedly', + ); reject(new Error('WebSocket closed')); } }); @@ -540,7 +558,9 @@ export class SpeechStream extends stt.SpeechStream { break; } default: { - this.#logger.child({ msg: json }).warn('received unexpected message from Deepgram'); + this.#logger + .child({ 'lk.pii.message': json }) + .warn('received unexpected message from Deepgram'); break; } } @@ -549,7 +569,7 @@ export class SpeechStream extends stt.SpeechStream { resolve(); } } catch (err) { - this.#logger.error(`STT: Error processing message: ${msg}`); + this.#logger.error({ 'lk.pii.message': msg }, 'Deepgram STT failed to process message'); reject(err); } }); diff --git a/plugins/deepgram/src/stt_v2.ts b/plugins/deepgram/src/stt_v2.ts index 28605ec26..37a77ba38 100644 --- a/plugins/deepgram/src/stt_v2.ts +++ b/plugins/deepgram/src/stt_v2.ts @@ -314,7 +314,7 @@ class SpeechStreamv2 extends stt.SpeechStream { this.#reconnectEvent.clear(); const url = this.#getDeepgramUrl(); - this.#logger.debug(`Connecting to Deepgram: ${url}`); + this.#logger.debug({ 'lk.pii.url': url }, 'connecting to Deepgram'); this.#ws = new WebSocket(url, { headers: { Authorization: `Token ${this.#opts.apiKey}` }, @@ -357,7 +357,7 @@ class SpeechStreamv2 extends stt.SpeechStream { break; } } catch (error) { - this.#logger.error('Deepgram stream error', { error }); + this.#logger.error({ 'lk.pii.error': error }, 'Deepgram stream error'); throw error; // Let Base Class handle retry logic } finally { if (this.#ws?.readyState === WebSocket.OPEN) { @@ -451,12 +451,18 @@ class SpeechStreamv2 extends stt.SpeechStream { const msg = JSON.parse(data.toString()); this.#processStreamEvent(msg); } catch (error) { - this.#logger.error('Failed to parse Deepgram message', { error }); + this.#logger.error({ 'lk.pii.error': error }, 'Failed to parse Deepgram message'); } }); this.#ws.on('close', (code, reason) => { - this.#logger.debug(`Deepgram WebSocket closed: ${code} ${reason}`); + this.#logger.debug( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Deepgram WebSocket closed', + ); resolve(); }); @@ -504,7 +510,7 @@ class SpeechStreamv2 extends stt.SpeechStream { this.#onEndOfSpeech(); } } else if (data.type === 'Error') { - this.#logger.warn('deepgram sent an error', { data }); + this.#logger.warn({ 'lk.pii.data': data }, 'deepgram sent an error'); const desc = (data.description as string) || 'unknown error from deepgram'; throw new Error(`Deepgram API Error: ${desc}`); } diff --git a/plugins/deepgram/src/tts.ts b/plugins/deepgram/src/tts.ts index 56f7b651f..746dd47da 100644 --- a/plugins/deepgram/src/tts.ts +++ b/plugins/deepgram/src/tts.ts @@ -195,7 +195,7 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Deepgram TTS response error'); + this.#logger.error({ 'lk.pii.error': err }, 'Deepgram TTS response error'); settle(() => reject(err)); }); @@ -220,7 +220,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ err }, 'Deepgram TTS request error'); + this.#logger.error({ 'lk.pii.error': err }, 'Deepgram TTS request error'); settle(() => reject(err)); }); @@ -282,7 +282,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - console.warn(`Error during WebSocket close sequence: ${e}`); + this.#logger.warn({ 'lk.pii.error': e }, 'error during WebSocket close sequence'); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); @@ -417,12 +417,15 @@ export class SynthesizeStream extends tts.SynthesizeStream { resolve(); return; } else if (message.type === 'Warning') { - this.#logger.warn(`Deepgram warning: ${message.warn_msg}`); + this.#logger.warn( + { 'lk.pii.warning': message.warn_msg }, + 'Deepgram returned a warning', + ); } else if (message.type === 'Error' || message.type === 'error') { reject(new APIError('Deepgram TTS returned error', { body: message })); return; } else if (message.type !== 'Metadata') { - this.#logger.warn({ message }, 'Unknown Deepgram message type'); + this.#logger.warn({ 'lk.pii.message': message }, 'Unknown Deepgram message type'); } resetMessageTimeout(reject); diff --git a/plugins/did/src/api.ts b/plugins/did/src/api.ts index 184b84f4f..9607d73c7 100644 --- a/plugins/did/src/api.ts +++ b/plugins/did/src/api.ts @@ -128,9 +128,9 @@ export class DIDAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call d-id api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call d-id api'); } else { - this.#logger.error({ error: e }, 'failed to call d-id api'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call d-id api'); } if (i < this.connOptions.maxRetry) { diff --git a/plugins/elevenlabs/src/stt.ts b/plugins/elevenlabs/src/stt.ts index 950a42118..261828d14 100644 --- a/plugins/elevenlabs/src/stt.ts +++ b/plugins/elevenlabs/src/stt.ts @@ -306,8 +306,13 @@ export class STT extends stt.STT { }); } else { this.#logger.warn( - { stt: this.label, attempt: i + 1, error }, - `failed to recognize speech, retrying in ${retryInterval}ms`, + { + stt: this.label, + attempt: i + 1, + retryIntervalMs: retryInterval, + 'lk.pii.error': error, + }, + 'failed to recognize speech, retrying', ); } @@ -625,7 +630,10 @@ export class SpeechStream extends stt.SpeechStream { try { this.#processStreamEvent(parseStreamEvent(JSON.parse(msg.toString()))); } catch (error) { - this.#logger.error({ error }, 'failed to process ElevenLabs STT message'); + this.#logger.error( + { 'lk.pii.error': error }, + 'failed to process ElevenLabs STT message', + ); } }; const onClose = (code: number, reason: Buffer) => { @@ -804,7 +812,7 @@ export class SpeechStream extends stt.SpeechStream { } if (messageType === 'partial_transcript') { - this.#logger.debug({ data }, 'Received message type partial_transcript'); + this.#logger.debug({ 'lk.pii.data': data }, 'Received message type partial_transcript'); if (text) { if (!this.#speaking) { this.queue.put({ type: stt.SpeechEventType.START_OF_SPEECH }); @@ -839,7 +847,7 @@ export class SpeechStream extends stt.SpeechStream { } else if (messageType === 'committed_transcript') { return; } else if (messageType === 'session_started') { - this.#logger.debug(`Session started with ID: ${data.session_id ?? 'unknown'}`); + this.#logger.debug({ sessionId: data.session_id }, 'session started'); } else if ( messageType === 'auth_error' || messageType === 'quota_exceeded' || @@ -849,7 +857,13 @@ export class SpeechStream extends stt.SpeechStream { ) { const errorMsg = data.message ?? 'Unknown error'; const detailsSuffix = data.details ? ` - ${data.details}` : ''; - this.#logger.error(`ElevenLabs STT error [${messageType}]: ${errorMsg}${detailsSuffix}`); + this.#logger.error( + { + messageType, + 'lk.pii.error': `${errorMsg}${detailsSuffix}`, + }, + 'ElevenLabs STT returned an error', + ); throw new APIConnectionError({ message: `${messageType}: ${errorMsg}${detailsSuffix}` }); } else if ( messageType === 'committed_transcript_with_timestamps' && @@ -857,7 +871,13 @@ export class SpeechStream extends stt.SpeechStream { ) { return; } else { - this.#logger.warn(`ElevenLabs STT unknown message type: ${messageType}, data: ${data}`); + this.#logger.warn( + { + messageType, + 'lk.pii.data': data, + }, + 'ElevenLabs STT returned an unknown message type', + ); } } } diff --git a/plugins/elevenlabs/src/tts.ts b/plugins/elevenlabs/src/tts.ts index e0bc31c97..14faf22b9 100644 --- a/plugins/elevenlabs/src/tts.ts +++ b/plugins/elevenlabs/src/tts.ts @@ -302,7 +302,7 @@ class Connection { }); this.#ws.on('error', (error) => { - this.#logger.error({ error }, 'WebSocket connection error'); + this.#logger.error({ 'lk.pii.error': error }, 'WebSocket connection error'); reject(new APIConnectionError({ message: `WebSocket error: ${error.message}` })); }); }); @@ -423,7 +423,7 @@ class Connection { } } } catch (e) { - this.#logger.warn({ error: e }, 'send loop error'); + this.#logger.warn({ 'lk.pii.error': e }, 'send loop error'); } finally { if (!this.#closed) { await this.close(); @@ -441,7 +441,7 @@ class Connection { const parsed = JSON.parse(rawData.toString()); messageChannel.write(parsed); } catch (e) { - this.#logger.warn({ error: e }, 'failed to parse WebSocket message'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to parse WebSocket message'); } }; @@ -479,7 +479,11 @@ class Connection { if (data.error) { this.#logger.error( - { context_id: contextId, error: data.error, data }, + { + context_id: contextId, + 'lk.pii.error': data.error, + 'lk.pii.data': data, + }, 'elevenlabs tts returned error', ); if (contextId) { @@ -494,13 +498,16 @@ class Connection { if (!ctx) { if (data.type === 'flush_done') { this.#logger.debug( - { context_id: contextId, data }, + { context_id: contextId, 'lk.pii.data': data }, 'ignoring elevenlabs flush_done message for inactive context', ); continue; } - this.#logger.warn({ data }, 'unexpected message received from elevenlabs tts'); + this.#logger.warn( + { 'lk.pii.data': data }, + 'unexpected message received from elevenlabs tts', + ); continue; } @@ -601,7 +608,7 @@ class Connection { } } } catch (e) { - this.#logger.warn({ error: e }, 'recv loop error'); + this.#logger.warn({ 'lk.pii.error': e }, 'recv loop error'); for (const ctx of this.#contextData.values()) { ctx.waiter.reject(asError(e)); } diff --git a/plugins/fishaudio/src/tts.ts b/plugins/fishaudio/src/tts.ts index 4a54b039d..5817d1bb5 100644 --- a/plugins/fishaudio/src/tts.ts +++ b/plugins/fishaudio/src/tts.ts @@ -461,7 +461,10 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Fish Audio TTS error response stream error'); + this.#logger.error( + { 'lk.pii.error': err }, + 'Fish Audio TTS error response stream error', + ); if (!doneFut.done) { doneFut.reject( new APIStatusError({ @@ -498,7 +501,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Fish Audio TTS response error'); + this.#logger.error({ 'lk.pii.error': err }, 'Fish Audio TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -506,7 +509,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ err }, 'Fish Audio TTS request error'); + this.#logger.error({ 'lk.pii.error': err }, 'Fish Audio TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.write(payload); @@ -606,7 +609,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { parsed = decode(frame) as Record; } catch (err) { - this.#logger.warn({ err }, 'Fish Audio failed to decode message'); + this.#logger.warn({ 'lk.pii.error': err }, 'Fish Audio failed to decode message'); return; } @@ -643,7 +646,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } if (!finished.done) finished.resolve(); } else { - this.#logger.debug({ event }, 'unknown Fish Audio event'); + this.#logger.debug({ 'lk.pii.event': event }, 'unknown Fish Audio event'); } }; diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 51874b26f..3c0e31fef 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -331,8 +331,8 @@ export class LLM extends llm.LLM { // Gemini 3: only supports thinkingLevel if (thinkingBudget !== undefined && thinkingLevel === undefined) { log().warn( - `Model ${this.#opts.model} is Gemini 3 which does not support thinkingBudget. ` + - `Please use thinkingLevel ('low' or 'high') instead. Ignoring thinkingBudget.`, + { model: this.#opts.model }, + 'Gemini 3 model does not support thinkingBudget; use thinkingLevel instead', ); } extras.thinkingConfig = { @@ -345,9 +345,8 @@ export class LLM extends llm.LLM { // Gemini 2.5 and earlier: only supports thinkingBudget if (thinkingLevel !== undefined && thinkingBudget === undefined) { log().warn( - `Model ${this.#opts.model} does not support thinkingLevel. ` + - `Please use thinkingBudget (number) instead for Gemini 2.5 and earlier models. ` + - `Ignoring thinkingLevel.`, + { model: this.#opts.model }, + 'model does not support thinkingLevel; use thinkingBudget instead', ); extras.thinkingConfig = { includeThoughts }; } else if (thinkingBudget !== undefined) { @@ -466,7 +465,7 @@ export class LLMStream extends llm.LLMStream { } if (dropped.length > 0) { this.logger.warn( - { dropped, cachedContent }, + { dropped, 'lk.pii.cached_content': cachedContent }, 'dropping systemInstruction from Gemini request because cachedContent is set; this field must be baked into the CachedContent resource', ); } @@ -513,7 +512,10 @@ export class LLMStream extends llm.LLMStream { } if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) { - this.logger.warn(`No content in the response: ${JSON.stringify(chunk)}`); + this.logger.warn( + { 'lk.pii.response': chunk }, + 'Google LLM response contained no content', + ); if (retryable) { throw new APIStatusError({ message: 'Google LLM: no content in the response', diff --git a/plugins/google/src/realtime/realtime_api.ts b/plugins/google/src/realtime/realtime_api.ts index 214cd0039..538e6170a 100644 --- a/plugins/google/src/realtime/realtime_api.ts +++ b/plugins/google/src/realtime/realtime_api.ts @@ -358,7 +358,8 @@ export class RealtimeModel extends llm.RealtimeModel { if (!mutableSession) { this.#logger.warn( - `'${model}' has limited mid-session update support. instructions, chat context, and tool updates will not be applied until the next session.`, + { model }, + 'model has limited mid-session update support; updates apply to the next session', ); } @@ -525,7 +526,7 @@ export class RealtimeSession extends llm.RealtimeSession { try { await this.activeSession.close(); } catch (error) { - this.#logger.warn({ error }, 'Error closing Gemini session'); + this.#logger.warn({ 'lk.pii.error': error }, 'Error closing Gemini session'); } finally { this.activeSession = undefined; } @@ -814,7 +815,8 @@ export class RealtimeSession extends llm.RealtimeSession { ): Promise { if (!this.realtimeModel.capabilities.midSessionChatCtxUpdate) { this.#logger.warn( - `generateReply is not compatible with '${this.options.model}' and will be ignored.`, + { model: this.options.model }, + 'generateReply is not compatible with this model and will be ignored', ); throw new Error(`generateReply is not compatible with '${this.options.model}'`); } @@ -1010,7 +1012,14 @@ export class RealtimeSession extends llm.RealtimeSession { ? ' (message may be truncated - check model name and API permissions)' : ''; const errorMsg = event.reason || `WebSocket closed with code ${event.code}`; - this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`); + this.#logger.error( + { + code: event.code, + truncated: isTruncated, + 'lk.pii.error': `${errorMsg}${truncationNote}`, + }, + 'Gemini Live session error', + ); this.emitError( new APIStatusError({ @@ -1026,7 +1035,13 @@ export class RealtimeSession extends llm.RealtimeSession { false, ); } else { - this.#logger.debug('Gemini Live session closed:', event.code, event.reason); + this.#logger.debug( + { + code: event.code, + 'lk.pii.reason': event.reason, + }, + 'Gemini Live session closed', + ); } this.markCurrentGenerationDone(); }, @@ -1074,7 +1089,7 @@ export class RealtimeSession extends llm.RealtimeSession { await cancelAndWait([sendTask, restartWaitTask], 2000); } catch (error) { - this.#logger.error(`Gemini Realtime API error: ${error}`); + this.#logger.error({ 'lk.pii.error': error }, 'Gemini Realtime API error'); if (this.#closed) break; @@ -1099,8 +1114,9 @@ export class RealtimeSession extends llm.RealtimeSession { { attempt: this.numRetries, maxRetries, + retryIntervalMs: retryInterval, }, - `Gemini Realtime API connection failed, retrying in ${retryInterval}ms`, + 'Gemini Realtime API connection failed, retrying', ); await delay(retryInterval); @@ -1130,7 +1146,13 @@ export class RealtimeSession extends llm.RealtimeSession { case 'content': const { turns, turnComplete } = msg.value; if (LK_GOOGLE_DEBUG) { - this.#logger.debug(`(client) -> ${JSON.stringify(this.loggableClientEvent(msg))}`); + this.#logger.debug( + { + eventType: msg.type, + 'lk.pii.event': this.loggableClientEvent(msg), + }, + 'sent Gemini Live client event', + ); } await session.sendClientContent({ turns, @@ -1141,7 +1163,13 @@ export class RealtimeSession extends llm.RealtimeSession { const { functionResponses } = msg.value; if (functionResponses) { if (LK_GOOGLE_DEBUG) { - this.#logger.debug(`(client) -> ${JSON.stringify(this.loggableClientEvent(msg))}`); + this.#logger.debug( + { + eventType: msg.type, + 'lk.pii.event': this.loggableClientEvent(msg), + }, + 'sent Gemini Live client event', + ); } try { await session.sendToolResponse({ @@ -1172,13 +1200,13 @@ export class RealtimeSession extends llm.RealtimeSession { if (activityEnd) await session.sendRealtimeInput({ activityEnd }); break; default: - this.#logger.warn(`Warning: Received unhandled message type: ${msg.type}`); + this.#logger.warn({ messageType: msg.type }, 'received unhandled message type'); break; } } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error(`Error in send task: ${e}`); + this.#logger.error({ 'lk.pii.error': e }, 'Gemini Live send task failed'); this.markRestartNeeded(); } } finally { @@ -1202,9 +1230,15 @@ export class RealtimeSession extends llm.RealtimeSession { (part) => part.inlineData?.data, ); if (LK_GOOGLE_DEBUG) { - this.#logger.debug(`(server) <- ${JSON.stringify(this.loggableServerMessage(response))}`); + this.#logger.debug( + { 'lk.pii.response': this.loggableServerMessage(response) }, + 'received Gemini Live server event', + ); } else if (!hasAudioData) { - this.#logger.debug(`(server) <- ${JSON.stringify(this.loggableServerMessage(response))}`); + this.#logger.debug( + { 'lk.pii.response': this.loggableServerMessage(response) }, + 'received Gemini Live server event', + ); } const unlock = await this.sessionLock.lock(); @@ -1252,7 +1286,10 @@ export class RealtimeSession extends llm.RealtimeSession { if (this.isNewGeneration(response)) { this.startNewGeneration(); if (LK_GOOGLE_DEBUG) { - this.#logger.debug(`new generation started: ${this.currentGeneration?.responseId}`); + this.#logger.debug( + { responseId: this.currentGeneration?.responseId }, + 'new generation started', + ); } } } @@ -1291,7 +1328,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error(`Error in onReceiveMessage: ${e}`); + this.#logger.error({ 'lk.pii.error': e }, 'failed to process Gemini Live server event'); this.markRestartNeeded(); } } @@ -1620,7 +1657,10 @@ export class RealtimeSession extends llm.RealtimeSession { gen.audioChannel.write(audioFrame); } catch (error) { - this.#logger.error('Error processing audio data:', error); + this.#logger.error( + { 'lk.pii.error': error }, + 'Error processing Gemini Live audio data', + ); } } } diff --git a/plugins/inworld/src/stt.ts b/plugins/inworld/src/stt.ts index 8e4166165..a4b802851 100644 --- a/plugins/inworld/src/stt.ts +++ b/plugins/inworld/src/stt.ts @@ -276,12 +276,23 @@ export class SpeechStream extends stt.SpeechStream { const delay = Math.min(retries * 5, 10); retries++; this.#logger.warn( - `Failed to connect to Inworld STT, retrying in ${delay}s: ${e} (${retries}/${maxRetry})`, + { + delay, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'Failed to connect to Inworld STT, retrying', ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - `Inworld STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'Inworld STT disconnected', ); } } @@ -321,7 +332,13 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error(`Inworld STT WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Inworld STT WebSocket closed unexpectedly', + ); reject(new Error('WebSocket closed')); } }); diff --git a/plugins/inworld/src/tts.ts b/plugins/inworld/src/tts.ts index bbf23e0f5..878a15fda 100644 --- a/plugins/inworld/src/tts.ts +++ b/plugins/inworld/src/tts.ts @@ -262,7 +262,7 @@ class WSConnectionPool { }), ); } else { - this.#logger.error({ err, requestId }, 'Inworld WebSocket error'); + this.#logger.error({ 'lk.pii.error': err, requestId }, 'Inworld WebSocket error'); } }); @@ -281,10 +281,10 @@ class WSConnectionPool { this.#listeners.get(contextId)!(json); } } else if (json.error) { - this.#logger.warn({ error: json.error }, 'Inworld received error message'); + this.#logger.warn({ 'lk.pii.error': json.error }, 'Inworld received error message'); } } catch (e) { - this.#logger.warn({ error: e }, 'Failed to parse Inworld WebSocket message'); + this.#logger.warn({ 'lk.pii.error': e }, 'Failed to parse Inworld WebSocket message'); } }); }); @@ -512,7 +512,13 @@ class ChunkedStream extends tts.ChunkedStream { throw new Error(data.error.message); } } catch (e) { - log().warn({ error: e, line }, 'Failed to parse Inworld chunk'); + log().warn( + { + 'lk.pii.error': e, + 'lk.pii.line': line, + }, + 'Failed to parse Inworld chunk', + ); } } } @@ -775,7 +781,7 @@ class SynthesizeStream extends tts.SynthesizeStream { } sendLastFrame(true); } catch (e) { - log().error({ error: e }, 'Error in SynthesizeStream run'); + log().error({ 'lk.pii.error': e }, 'Error in SynthesizeStream run'); throw e; } finally { ws.off('close', onClose); diff --git a/plugins/krisp/src/_krisp.ts b/plugins/krisp/src/_krisp.ts index 683188dd7..194715e2b 100644 --- a/plugins/krisp/src/_krisp.ts +++ b/plugins/krisp/src/_krisp.ts @@ -91,7 +91,7 @@ class KrispLicenseSdkManager { log().debug('Krisp Audio SDK (license) initialized'); } this.referenceCount += 1; - log().debug(`Krisp SDK (license) reference count: ${this.referenceCount}`); + log().debug({ referenceCount: this.referenceCount }, 'Krisp SDK reference acquired'); // Non-null once refCount > 0: release() only nulls the module at refCount 0. return this.module!; } @@ -102,13 +102,13 @@ class KrispLicenseSdkManager { return; } this.referenceCount -= 1; - log().debug(`Krisp SDK (license) reference count: ${this.referenceCount}`); + log().debug({ referenceCount: this.referenceCount }, 'Krisp SDK reference released'); if (this.referenceCount === 0 && this.module !== null) { try { this.module.globalDestroy(); log().debug('Krisp Audio SDK (license) destroyed'); } catch (e) { - log().error(`Error during Krisp SDK cleanup: ${e}`); + log().error({ 'lk.pii.error': e }, 'error during Krisp SDK cleanup'); } finally { this.module = null; } @@ -212,14 +212,14 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { ); } - log().info(`Creating Krisp session for sample rate: ${sampleRate}Hz`); + log().info({ sampleRate }, 'Creating Krisp session'); // Free the previous session (if any) before replacing it on a rate change. if (this.session !== null) { try { this.session.destroy(); } catch (e) { - log().error(`Error destroying Krisp session: ${e}`); + log().error({ 'lk.pii.error': e }, 'error destroying Krisp session'); } this.session = null; } @@ -277,8 +277,8 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { if (frame.channels !== 1) { if (!this.warnedChannels) { log().warn( - `Krisp filter not applied: expected mono audio but got ${frame.channels} ` + - 'channels; frames are passed through unprocessed.', + { channels: frame.channels }, + 'Krisp filter requires mono audio; frames are passed through unprocessed', ); this.warnedChannels = true; } @@ -313,12 +313,13 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { const outputBuf = this.session!.process(inputBuf, this.level); chunkOut = bufferToInt16(outputBuf); } catch (e) { - log().error(`Error processing frame: ${e}`); + log().error({ 'lk.pii.error': e }, 'error processing frame'); chunkOut = chunkIn; } if (chunkOut.length !== chunk) { log().warn( - `Krisp returned ${chunkOut.length} samples, expected ${chunk}; using original audio`, + { returnedSamples: chunkOut.length, expectedSamples: chunk }, + 'Krisp returned an unexpected sample count; using original audio', ); chunkOut = chunkIn; } @@ -347,7 +348,7 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { try { this.session.destroy(); } catch (e) { - log().error(`Error destroying Krisp session: ${e}`); + log().error({ 'lk.pii.error': e }, 'error destroying Krisp session'); } this.session = null; } diff --git a/plugins/lemonslice/src/avatar.ts b/plugins/lemonslice/src/avatar.ts index 19d5157d4..9136591d4 100644 --- a/plugins/lemonslice/src/avatar.ts +++ b/plugins/lemonslice/src/avatar.ts @@ -440,7 +440,7 @@ export class AvatarSession extends voice.AvatarSession { try { await this.callLeaveMeeting(sessionId, meetingBotId); } catch (error) { - this.#logger.warn({ error }, 'failed to leave meeting via LemonSlice API'); + this.#logger.warn({ 'lk.pii.error': error }, 'failed to leave meeting via LemonSlice API'); } } @@ -552,9 +552,9 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call lemonslice api'); } else { - this.#logger.error({ error: e }, 'failed to call lemonslice api'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call lemonslice api'); } if (i <= this.connOptions.maxRetry - 1) { @@ -642,9 +642,9 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call lemonslice api'); } else { - this.#logger.error({ error: e }, 'failed to call lemonslice api'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call lemonslice api'); } if (i <= this.connOptions.maxRetry - 1) { diff --git a/plugins/lemonslice/src/meeting/audio.ts b/plugins/lemonslice/src/meeting/audio.ts index 2bf90f896..58bf7cc0a 100644 --- a/plugins/lemonslice/src/meeting/audio.ts +++ b/plugins/lemonslice/src/meeting/audio.ts @@ -90,7 +90,7 @@ export class MeetingAudioInput extends voice.AudioInput { for (const out of this.resample(downmixToMono(frame))) { void this.channel.write(out).catch((error) => { - this.#logger.warn({ error }, 'failed to write meeting audio frame'); + this.#logger.warn({ 'lk.pii.error': error }, 'failed to write meeting audio frame'); }); } } @@ -191,7 +191,7 @@ export async function streamMeetingRelay( if (stop.aborted) { return; } - logger.warn({ error, backoffMs }, 'meeting relay disconnected; retrying'); + logger.warn({ 'lk.pii.error': error, backoffMs }, 'meeting relay disconnected; retrying'); } if (stop.aborted) { diff --git a/plugins/lemonslice/src/meeting/chat.ts b/plugins/lemonslice/src/meeting/chat.ts index 57fe28311..19408a062 100644 --- a/plugins/lemonslice/src/meeting/chat.ts +++ b/plugins/lemonslice/src/meeting/chat.ts @@ -100,12 +100,15 @@ export class MeetingChatRelay { return; } - this.#logger.info({ userInput: userInput.slice(0, 120) }, 'meeting chat relay: user_input'); + this.#logger.info( + { 'lk.pii.user_input': userInput.slice(0, 120) }, + 'meeting chat relay received user input', + ); try { this.session.interrupt(); this.session.generateReply({ userInput }); } catch (error) { - this.#logger.warn({ error }, 'meeting chat relay: generateReply failed'); + this.#logger.warn({ 'lk.pii.error': error }, 'meeting chat relay: generateReply failed'); } } } diff --git a/plugins/liveavatar/src/api.ts b/plugins/liveavatar/src/api.ts index a010662b7..a9c4dea89 100644 --- a/plugins/liveavatar/src/api.ts +++ b/plugins/liveavatar/src/api.ts @@ -187,8 +187,8 @@ export class LiveAvatarAPI { throw e; } this.#logger.warn( - { error: String(e), url, attempt: i }, - `API request to ${url} failed on attempt ${i}`, + { 'lk.pii.error': e, 'lk.pii.url': url, attempt: i }, + 'LiveAvatar API request failed', ); } diff --git a/plugins/liveavatar/src/avatar.ts b/plugins/liveavatar/src/avatar.ts index ce0575d3b..a154dad40 100644 --- a/plugins/liveavatar/src/avatar.ts +++ b/plugins/liveavatar/src/avatar.ts @@ -244,7 +244,7 @@ export class AvatarSession extends voice.AvatarSession { // protocol failure does not surface as an unhandled rejection. The main task // itself handles its own cleanup in finally. this.mainTaskPromise = this.mainTask().catch((e) => { - this.#logger.warn({ error: String(e) }, 'LiveAvatar main task failed'); + this.#logger.warn({ 'lk.pii.error': e }, 'LiveAvatar main task failed'); }); } @@ -462,7 +462,7 @@ export class AvatarSession extends voice.AvatarSession { wsRef.send(JSON.stringify(value)); resetKeepAlive(); } catch (e) { - this.#logger.warn({ error: String(e) }, 'failed to send LiveAvatar event'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to send LiveAvatar event'); break; } } @@ -573,7 +573,7 @@ export class AvatarSession extends voice.AvatarSession { await Promise.race([forwardAudio(), sendTask(), recvTask(), keepAliveTask()]); } catch (e) { - this.#logger.warn({ error: String(e) }, 'LiveAvatar main task error'); + this.#logger.warn({ 'lk.pii.error': e }, 'LiveAvatar main task error'); } finally { this.closing = true; closingResolver.resolve(); @@ -587,7 +587,7 @@ export class AvatarSession extends voice.AvatarSession { } } } catch (e) { - this.#logger.warn({ error: String(e) }, 'Failed to stop LiveAvatar session'); + this.#logger.warn({ 'lk.pii.error': e }, 'Failed to stop LiveAvatar session'); } if (this.audioBuffer) { diff --git a/plugins/livekit/src/hf_utils.ts b/plugins/livekit/src/hf_utils.ts index 14b815623..3177475ac 100644 --- a/plugins/livekit/src/hf_utils.ts +++ b/plugins/livekit/src/hf_utils.ts @@ -76,7 +76,7 @@ async function getBranchHeadCommit( return null; } catch (error) { logger.error( - { error: (error as Error).message, repo: toRepoId(repo), revision }, + { 'lk.pii.error': error, repo: toRepoId(repo), revision }, 'Error getting HEAD commit', ); throw error; @@ -122,7 +122,7 @@ async function createSymlink(sourcePath: string, targetPath: string): Promise { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Neuphonic TTS response error'); + this.#logger.error({ 'lk.pii.error': err }, 'Neuphonic TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ err }, 'Neuphonic TTS request error'); + this.#logger.error({ 'lk.pii.error': err }, 'Neuphonic TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -263,13 +263,16 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error(`Error parsing WebSocket message: ${error}`); + this.#logger.error( + { 'lk.pii.error': error }, + 'Error parsing Neuphonic WebSocket message', + ); reject(error); } }); ws.on('error', (error) => { - this.#logger.error(`WebSocket error: ${error}`); + this.#logger.error({ 'lk.pii.error': error }, 'Neuphonic WebSocket error'); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -280,7 +283,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { ws.on('close', (code, reason) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Neuphonic WebSocket closed unexpectedly', + ); this.queue.put(SynthesizeStream.END_OF_STREAM); } // Only reject if we haven't processed all expected frames @@ -295,11 +304,14 @@ export class SynthesizeStream extends tts.SynthesizeStream { if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { if (err.message.includes('Queue is closed')) { this.#logger.warn( - { err }, + { 'lk.pii.error': err }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ err }, 'Error in recvTask from Neuphonic WebSocket'); + this.#logger.error( + { 'lk.pii.error': err }, + 'Error in recvTask from Neuphonic WebSocket', + ); } } break; diff --git a/plugins/openai/src/realtime/realtime_model.ts b/plugins/openai/src/realtime/realtime_model.ts index e63f83de0..a4c160a9a 100644 --- a/plugins/openai/src/realtime/realtime_model.ts +++ b/plugins/openai/src/realtime/realtime_model.ts @@ -589,7 +589,10 @@ export class RealtimeSession extends llm.RealtimeSession { ); if (blockingErrors.length > 0) { this.#logger.error( - { issues: validation.issues, blockingErrors }, + { + 'lk.pii.issues': validation.issues, + 'lk.pii.blocking_errors': blockingErrors, + }, 'Invalid chat context supplied to updateChatCtx', ); throw new Error( @@ -606,7 +609,7 @@ export class RealtimeSession extends llm.RealtimeSession { this.#logger.debug( { warnings: validation.warnings, - issues: validation.issues, + 'lk.pii.issues': validation.issues, }, 'Chat context warnings detected before realtime update', ); @@ -673,7 +676,10 @@ export class RealtimeSession extends llm.RealtimeSession { } } } catch (e) { - this.#logger.error((e as Error).message); + this.#logger.error( + { 'lk.pii.error': (e as Error).message }, + 'failed to update OpenAI Realtime chat context', + ); throw e; } finally { unlock(); @@ -1027,7 +1033,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (!this.currentGeneration._doneFut.done) { this.currentGeneration._doneFut.resolve(); } - this.#logger.warn(`In-progress generation discarded due to ${reason}`); + this.#logger.warn({ 'lk.pii.reason': reason }, 'in-progress generation discarded'); this.currentGeneration = undefined; } @@ -1075,7 +1081,7 @@ export class RealtimeSession extends llm.RealtimeSession { }); if (lkOaiDebug) { - this.#logger.debug(`Connecting to OpenAI Realtime API at ${url}`); + this.#logger.debug({ 'lk.pii.url': url }, 'connecting to OpenAI Realtime API'); } return new Promise((resolve, reject) => { @@ -1224,9 +1230,10 @@ export class RealtimeSession extends llm.RealtimeSession { { attempt: numRetries, maxRetries, - error, + retryIntervalMs: retryInterval, + 'lk.pii.error': error, }, - `OpenAI Realtime API connection failed, retrying in ${retryInterval / 1000}s`, + 'OpenAI Realtime API connection failed, retrying', ); await delay(retryInterval); @@ -1254,7 +1261,13 @@ export class RealtimeSession extends llm.RealtimeSession { } if (lkOaiDebug) { - this.#logger.debug(this.loggableEvent(event), `(client) -> ${event.type}`); + this.#logger.debug( + { + eventType: event.type, + 'lk.pii.event': this.loggableEvent(event), + }, + 'sent OpenAI Realtime client event', + ); } this.emit('openai_client_event_queued', event); @@ -1285,7 +1298,13 @@ export class RealtimeSession extends llm.RealtimeSession { this.emit('openai_server_event_received', event); if (lkOaiDebug) { - this.#logger.debug(this.loggableEvent(event), `(server) <- ${event.type}`); + this.#logger.debug( + { + eventType: event.type, + 'lk.pii.event': this.loggableEvent(event), + }, + 'received OpenAI Realtime server event', + ); } try { @@ -1359,7 +1378,7 @@ export class RealtimeSession extends llm.RealtimeSession { break; default: if (lkOaiDebug) { - this.#logger.debug(`unhandled event: ${event.type}`); + this.#logger.debug({ eventType: event.type }, 'unhandled event'); } break; } @@ -1369,7 +1388,13 @@ export class RealtimeSession extends llm.RealtimeSession { wsConn.close(); return; } - this.#logger.error({ error, event }, 'Failed to handle OpenAI Realtime API event'); + this.#logger.error( + { + 'lk.pii.error': error, + 'lk.pii.event': event, + }, + 'Failed to handle OpenAI Realtime API event', + ); } }; @@ -1577,7 +1602,10 @@ export class RealtimeSession extends llm.RealtimeSession { try { this.remoteChatCtx.insert(event.previous_item_id, incomingItem); } catch (error) { - this.#logger.error({ error, itemId: event.item.id }, 'failed to insert conversation item'); + this.#logger.error( + { 'lk.pii.error': error, itemId: event.item.id }, + 'failed to insert conversation item', + ); } const fut = pendingCreateFuture; @@ -1599,7 +1627,10 @@ export class RealtimeSession extends llm.RealtimeSession { try { this.remoteChatCtx.delete(event.item_id); } catch (error) { - this.#logger.error({ error, itemId: event.item_id }, 'failed to delete conversation item'); + this.#logger.error( + { 'lk.pii.error': error, itemId: event.item_id }, + 'failed to delete conversation item', + ); } const fut = this.itemDeleteFutures[event.item_id]; @@ -1665,7 +1696,7 @@ export class RealtimeSession extends llm.RealtimeSession { event: api_proto.ConversationItemInputAudioTranscriptionFailedEvent, ): void { this.#logger.error( - { error: event.error }, + { 'lk.pii.error': event.error }, 'OpenAI Realtime API failed to transcribe input audio', ); this.finalizePartialOnTranscriptionFailure(event.item_id, event.content_index ?? 0); @@ -1694,7 +1725,7 @@ export class RealtimeSession extends llm.RealtimeSession { const itemGeneration = this.currentGeneration.messages.get(itemId); if (!itemGeneration) { - this.#logger.warn(`itemGeneration not found for itemId=${itemId}`); + this.#logger.warn({ itemId }, 'itemGeneration not found'); return; } @@ -2010,9 +2041,9 @@ export class RealtimeSession extends llm.RealtimeSession { eventId: event.response.id, eventResponseStatus: event.response.status, eventResponseStatusType: statusType, - eventResponseStatusReason: statusReason, + 'lk.pii.event_response_status_reason': statusReason, }, - `OpenAI Realtime API response done but not complete with status: ${event.response.status} (type=${statusType}, reason=${statusReason})`, + 'OpenAI Realtime API response finished without completing', ); } else { this.#logger.debug({ eventResponseStatus: event.response.status }, 'Unknown response status'); @@ -2023,7 +2054,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (event.error.message.startsWith('Cancellation failed')) { return; } - this.#logger.error({ error: event.error }, 'OpenAI Realtime API returned an error'); + this.#logger.error({ 'lk.pii.error': event.error }, 'OpenAI Realtime API returned an error'); const eventId = event.error.event_id; if (eventId) { diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 9e2f22f4f..f7374e7e5 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -362,7 +362,14 @@ export class RealtimeSession extends llm.RealtimeSession { const item = chatCtx.getById(itemId); if (item?.type === 'function_call_output' && this.pendingToolCallIds.has(item.callId)) { this.pendingToolCallIds.delete(item.callId); - this.#logger.info(`Sending tool call output for ${item.name} (call_id: ${item.callId})`); + this.#logger.info( + { + function: item.name, + callId: item.callId, + 'lk.pii.output': item.output, + }, + 'sending tool call output', + ); this.socket?.sendToolCallOutput({ type: 'tool_call_output', tool_call_id: item.callId, @@ -375,7 +382,10 @@ export class RealtimeSession extends llm.RealtimeSession { } if (item?.type === 'message') { if ((item.role === 'system' || item.role === 'developer') && item.rawTextContent) { - this.#logger.debug(`Sending add system message: ${item.rawTextContent}`); + this.#logger.debug( + { 'lk.pii.system_message': item.rawTextContent }, + 'sending add system message', + ); this.socket?.sendAddSystemMessage({ type: 'add_system_message', system_message: item.rawTextContent, @@ -385,7 +395,7 @@ export class RealtimeSession extends llm.RealtimeSession { // Only treat a user message as text input when it's appended at the tail of the context. if (item.role === 'user' && itemId === lastItemId && item.rawTextContent) { - this.#logger.debug(`Received user text input: ${item.rawTextContent}`); + this.#logger.debug({ 'lk.pii.text': item.rawTextContent }, 'received user text input'); this.pendingUserText = item.rawTextContent; bufferedUserText = true; } @@ -720,7 +730,7 @@ export class RealtimeSession extends llm.RealtimeSession { break; case 'conversation_created': this.conversationId = message.conversation_id; - this.#logger.info(`Phonic Conversation began with ID: ${this.conversationId}`); + this.#logger.info({ conversationId: this.conversationId }, 'Phonic conversation began'); this.options.onConversationCreated?.(this.conversationId); break; case 'tool_call_interrupted': @@ -828,7 +838,11 @@ export class RealtimeSession extends llm.RealtimeSession { private handleToolCallInterrupted(message: Phonic.ToolCallInterruptedPayload): void { this.pendingToolCallIds.delete(message.tool_call_id); this.#logger.warn( - `Tool call for ${message.tool_name} (call_id: ${message.tool_call_id}) was cancelled due to user interruption.`, + { + toolName: message.tool_name, + callId: message.tool_call_id, + }, + 'tool call was cancelled due to user interruption', ); } diff --git a/plugins/protoface/src/avatar.ts b/plugins/protoface/src/avatar.ts index fefa1e488..a1dab17c7 100644 --- a/plugins/protoface/src/avatar.ts +++ b/plugins/protoface/src/avatar.ts @@ -152,7 +152,10 @@ export class AvatarSession extends voice.AvatarSession { try { await this.api.endSession(sessionId); } catch (error) { - this.#logger.warn({ error: String(error), sessionId }, 'failed to end protoface session'); + this.#logger.warn( + { 'lk.pii.error': error, sessionId }, + 'failed to end protoface session', + ); } } } finally { diff --git a/plugins/resemble/src/tts.ts b/plugins/resemble/src/tts.ts index b393d7cd2..830efcd18 100644 --- a/plugins/resemble/src/tts.ts +++ b/plugins/resemble/src/tts.ts @@ -161,7 +161,7 @@ export class ChunkedStream extends tts.ChunkedStream { this.queue.close(); doneFut.resolve(); } catch (error) { - this.#logger.error('Error processing Resemble API response:', error); + this.#logger.error({ 'lk.pii.error': error }, 'Error processing Resemble API response'); this.queue.close(); doneFut.resolve(); } @@ -174,14 +174,14 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ err }, 'Resemble TTS response error'); + this.#logger.error({ 'lk.pii.error': err }, 'Resemble TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ err }, 'Resemble TTS request error'); + this.#logger.error({ 'lk.pii.error': err }, 'Resemble TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -268,7 +268,10 @@ export class SynthesizeStream extends tts.SynthesizeStream { lastFrame = frame; } } catch (audioError) { - this.#logger.error(`Error processing audio content: ${audioError}`); + this.#logger.error( + { 'lk.pii.error': audioError }, + 'Error processing Resemble audio content', + ); } } else if ('type' in json && json.type === 'audio_end') { for (const frame of bstream.flush()) { @@ -291,8 +294,11 @@ export class SynthesizeStream extends tts.SynthesizeStream { const errorName = json.error_name || 'Unknown'; const explanation = json.error_params?.explanation || 'No details provided'; this.#logger - .child({ error: errorName }) - .error(`Resemble API error: ${explanation}`); + .child({ + error: errorName, + 'lk.pii.explanation': explanation, + }) + .error('Resemble API returned an error'); closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -302,13 +308,16 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error(`Error parsing WebSocket message: ${error}`); + this.#logger.error( + { 'lk.pii.error': error }, + 'Error parsing Resemble WebSocket message', + ); reject(error); } }); ws.on('error', (error) => { - this.#logger.error(`WebSocket error: ${error}`); + this.#logger.error({ 'lk.pii.error': error }, 'Resemble WebSocket error'); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -319,7 +328,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { ws.on('close', (code, reason) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Resemble WebSocket closed unexpectedly', + ); this.queue.put(SynthesizeStream.END_OF_STREAM); } // Only reject if we haven't received all expected frames @@ -333,7 +348,10 @@ export class SynthesizeStream extends tts.SynthesizeStream { } catch (err) { // Skip log error for normal websocket close if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { - this.#logger.error({ err }, 'Error in recvTask from Resemble WebSocket'); + this.#logger.error( + { 'lk.pii.error': err }, + 'Error in recvTask from Resemble WebSocket', + ); } break; } diff --git a/plugins/rime/src/tts.ts b/plugins/rime/src/tts.ts index 8dfd7de4d..928125192 100644 --- a/plugins/rime/src/tts.ts +++ b/plugins/rime/src/tts.ts @@ -497,7 +497,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { void messageChannel.write(JSON.parse(rawData.toString())); } catch (error) { - this.#logger.warn({ error }, 'failed to parse Rime WebSocket message'); + this.#logger.warn({ 'lk.pii.error': error }, 'failed to parse Rime WebSocket message'); } }; const onClose = (code: number, reason: Buffer) => { diff --git a/plugins/runway/src/avatar.ts b/plugins/runway/src/avatar.ts index c1a23c228..81c74f049 100644 --- a/plugins/runway/src/avatar.ts +++ b/plugins/runway/src/avatar.ts @@ -211,7 +211,7 @@ export class AvatarSession extends voice.AvatarSession { this.realtimeSessionId = payload.id; } else { this.#logger.warn( - { payload: JSON.stringify(payload) }, + { 'lk.pii.payload': payload }, 'Runway API response missing session id; API cancellation fallback will be unavailable', ); } @@ -220,9 +220,9 @@ export class AvatarSession extends voice.AvatarSession { if (e instanceof APIStatusError && !e.retryable) throw e; if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call Runway API'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call Runway API'); } else { - this.#logger.error({ error: e }, 'failed to call Runway API'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call Runway API'); } if (i < maxAttempts - 1) { @@ -269,7 +269,7 @@ export class AvatarSession extends voice.AvatarSession { return; } catch (error) { this.#logger.warn( - { error: String(error) }, + { 'lk.pii.error': error }, 'error ending Runway realtime session via data channel', ); } @@ -303,13 +303,17 @@ export class AvatarSession extends voice.AvatarSession { this.#logger.debug({ sessionId }, 'cancelled Runway realtime session'); } else { this.#logger.warn( - { sessionId, status: response.status, body: await response.text() }, + { + sessionId, + status: response.status, + 'lk.pii.body': await response.text(), + }, 'could not cancel Runway realtime session', ); } } catch (error) { this.#logger.warn( - { sessionId, error: String(error) }, + { sessionId, 'lk.pii.error': error }, 'error cancelling Runway realtime session', ); } diff --git a/plugins/sarvam/src/stt.ts b/plugins/sarvam/src/stt.ts index 48a6d7abc..41c1a75fc 100644 --- a/plugins/sarvam/src/stt.ts +++ b/plugins/sarvam/src/stt.ts @@ -377,7 +377,11 @@ function extractConfidence( } if (value != null) { logger.debug( - `Unexpected language_probability type: ${typeof value} (value=${JSON.stringify(value)}); falling back to confidence=1.0`, + { + valueType: typeof value, + 'lk.pii.value': value, + }, + 'unexpected language_probability type; falling back to confidence=1.0', ); } return 1; @@ -589,7 +593,7 @@ export class SpeechStream extends stt.SpeechStream { while (!this.input.closed && !this.closed) { const wsUrl = buildWsUrl(this.#opts); - this.#logger.info(`Sarvam STT connecting to: ${wsUrl}`); + this.#logger.info({ 'lk.pii.url': wsUrl }, 'Sarvam STT connecting'); const ws = new WebSocket(wsUrl, { headers: { 'api-subscription-key': this.#opts.apiKey }, }); @@ -625,12 +629,23 @@ export class SpeechStream extends stt.SpeechStream { const delay = Math.min(retries * 5, 10); retries++; this.#logger.warn( - `Failed to connect to Sarvam STT, retrying in ${delay}s: ${e} (${retries}/${maxRetry})`, + { + delay, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'Failed to connect to Sarvam STT, retrying', ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - `Sarvam STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'Sarvam STT disconnected', ); } } @@ -661,7 +676,13 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code: number, reason: Buffer) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'Sarvam STT WebSocket closed unexpectedly', + ); reject(new Error('WebSocket closed')); } }); @@ -759,7 +780,10 @@ export class SpeechStream extends stt.SpeechStream { ws.on('message', (msg: RawData) => { try { const raw = msg.toString(); - this.#logger.debug(`Sarvam STT raw WS message: ${raw.substring(0, 500)}`); + this.#logger.debug( + { 'lk.pii.raw_message': raw.substring(0, 500) }, + 'received Sarvam STT WebSocket message', + ); const json = JSON.parse(raw); const msgType: string = json['type'] ?? ''; @@ -791,7 +815,11 @@ export class SpeechStream extends stt.SpeechStream { // Log metrics when available if (td.metrics) { this.#logger.debug( - `Sarvam STT metrics: audio_duration=${td.metrics.audio_duration}s, latency=${td.metrics.processing_latency}s`, + { + audioDurationSeconds: td.metrics.audio_duration, + latencySeconds: td.metrics.processing_latency, + }, + 'Sarvam STT metrics', ); } @@ -826,7 +854,13 @@ export class SpeechStream extends stt.SpeechStream { json['message'] ?? 'Unknown error'; const errorCode = nested?.code ?? json['code'] ?? ''; - this.#logger.error(`Sarvam STT WebSocket error [${errorCode}]: ${errorInfo}`); + this.#logger.error( + { + errorCode, + 'lk.pii.error': errorInfo, + }, + 'Sarvam STT WebSocket returned an error', + ); reject(new Error(`Sarvam STT API error [${errorCode}]: ${errorInfo}`)); return; } @@ -835,7 +869,13 @@ export class SpeechStream extends stt.SpeechStream { resolve(); } } catch (err) { - this.#logger.error(`Error processing Sarvam STT message: ${msg}`); + this.#logger.error( + { + 'lk.pii.error': err, + 'lk.pii.message': msg.toString(), + }, + 'Error processing Sarvam STT message', + ); reject(err); } }); diff --git a/plugins/sarvam/src/tts.ts b/plugins/sarvam/src/tts.ts index 13ff43b59..302d5791d 100644 --- a/plugins/sarvam/src/tts.ts +++ b/plugins/sarvam/src/tts.ts @@ -495,7 +495,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - this.#logger.warn(`Error during WebSocket close sequence: ${e}`); + this.#logger.warn({ 'lk.pii.error': e }, 'Error during Sarvam WebSocket close sequence'); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); diff --git a/plugins/soniox/src/stt.ts b/plugins/soniox/src/stt.ts index 186ad8617..e115ca3fc 100644 --- a/plugins/soniox/src/stt.ts +++ b/plugins/soniox/src/stt.ts @@ -249,7 +249,13 @@ export class SpeechStream extends stt.SpeechStream { if (content.error_code || content.error_message) { const statusCode = parseStatusCode(content.error_code); const errorMessage = content.error_message ?? 'Unknown Soniox STT error'; - this.#logger.error(`WebSocket error: ${content.error_code ?? ''} - ${errorMessage}`); + this.#logger.error( + { + errorCode: content.error_code ?? '', + 'lk.pii.error': errorMessage, + }, + 'Soniox STT WebSocket returned an error', + ); reject( new APIStatusError({ message: `Soniox STT error: ${content.error_code ?? ''} - ${errorMessage}`, diff --git a/plugins/soniox/src/tts.ts b/plugins/soniox/src/tts.ts index fb820bcb9..b3bf5ce3e 100644 --- a/plugins/soniox/src/tts.ts +++ b/plugins/soniox/src/tts.ts @@ -134,7 +134,7 @@ export class TTS extends tts.TTS { prewarm(): void { void currentConnection(this, this.#opts.websocketUrl, 20000).catch((error: unknown) => { - log().debug({ error }, 'Soniox TTS prewarm failed'); + log().debug({ 'lk.pii.error': error }, 'Soniox TTS prewarm failed'); }); } @@ -480,7 +480,7 @@ class Connection { this.#ws.on('message', (data) => this.#handleMessage(data.toString())); this.#ws.on('error', (error) => { - this.#logger.warn({ error }, 'Soniox TTS WebSocket error'); + this.#logger.warn({ 'lk.pii.error': error }, 'Soniox TTS WebSocket error'); this.#failAll(new APIConnectionError({ message: `Soniox TTS WebSocket error: ${error}` })); }); this.#ws.on('close', (code) => { @@ -630,7 +630,7 @@ class Connection { } } } catch (error) { - this.#logger.warn({ error }, 'Soniox TTS send loop error'); + this.#logger.warn({ 'lk.pii.error': error }, 'Soniox TTS send loop error'); this.#failAll(new APIConnectionError({ message: `Soniox TTS send loop error: ${error}` })); } } @@ -640,7 +640,13 @@ class Connection { try { response = JSON.parse(raw) as Record; } catch (error) { - this.#logger.warn({ error, raw }, 'Failed to parse Soniox TTS response'); + this.#logger.warn( + { + 'lk.pii.error': error, + 'lk.pii.raw_response': raw, + }, + 'Failed to parse Soniox TTS response', + ); return; } @@ -648,8 +654,12 @@ class Connection { if (typeof streamId !== 'string') { if (response.error_code !== undefined) { this.#logger.error( - { response }, - `Soniox TTS connection-level error: ${response.error_code} - ${response.error_message}`, + { + errorCode: response.error_code, + 'lk.pii.error_message': response.error_message, + 'lk.pii.response': response, + }, + 'Soniox TTS connection-level error', ); } return; @@ -657,7 +667,7 @@ class Connection { const stream = this.#streams.get(streamId); if (stream === undefined) { - this.#logger.debug(`Ignoring message for unknown Soniox TTS stream ${streamId}`); + this.#logger.debug({ streamId }, 'Ignoring message for unknown Soniox TTS stream'); return; } diff --git a/plugins/tavus/src/api.test.ts b/plugins/tavus/src/api.test.ts index 16a0b4b94..c00240530 100644 --- a/plugins/tavus/src/api.test.ts +++ b/plugins/tavus/src/api.test.ts @@ -49,9 +49,14 @@ describe('Tavus TavusAPI.createConversation', () => { const body = sentBody(f); expect(body.face_id).toBe('r1'); expect(body.pal_id).toBe('x1'); - const msgs = warnMock.mock.calls.map((c) => String(c[0])); - expect(msgs.some((m) => m.includes('replicaId') && m.includes('faceId'))).toBe(true); - expect(msgs.some((m) => m.includes('personaId') && m.includes('palId'))).toBe(true); + expect(warnMock).toHaveBeenCalledWith( + { deprecatedName: 'replicaId', replacementName: 'faceId' }, + 'deprecated option used', + ); + expect(warnMock).toHaveBeenCalledWith( + { deprecatedName: 'personaId', replacementName: 'palId' }, + 'deprecated option used', + ); }); it('does not warn when both the new and deprecated options are supplied', async () => { diff --git a/plugins/tavus/src/api.ts b/plugins/tavus/src/api.ts index 9a2b2a358..663be6625 100644 --- a/plugins/tavus/src/api.ts +++ b/plugins/tavus/src/api.ts @@ -53,7 +53,7 @@ function coalesceWithDeprecated( ): string | undefined { // Prefer the new option; fall back to the deprecated alias and warn only when it's used. if (!newValue && deprecatedValue) { - log().warn(`\`${deprecatedName}\` is deprecated, use \`${newName}\` instead`); + log().warn({ deprecatedName, replacementName: newName }, 'deprecated option used'); } return newValue || deprecatedValue; } @@ -62,7 +62,10 @@ function deprecatedEnv(deprecatedName: string, newName: string): string | undefi // Read a deprecated env var, warning if it's set so callers migrate to `newName`. const value = process.env[deprecatedName]; if (value) { - log().warn(`\`${deprecatedName}\` is deprecated, use \`${newName}\` instead`); + log().warn( + { deprecatedName, replacementName: newName }, + 'deprecated environment variable used', + ); } return value; } @@ -221,9 +224,9 @@ export class TavusAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: String(e) }, 'failed to call tavus api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call tavus api'); } else { - this.#logger.error({ error: e }, 'failed to call tavus api'); + this.#logger.error({ 'lk.pii.error': e }, 'failed to call tavus api'); } if (i < this.connOptions.maxRetry) { diff --git a/plugins/trugen/src/avatar.ts b/plugins/trugen/src/avatar.ts index 71276e012..e72ca0a82 100644 --- a/plugins/trugen/src/avatar.ts +++ b/plugins/trugen/src/avatar.ts @@ -202,14 +202,14 @@ export class AvatarSession extends voice.AvatarSession { } catch (e) { if (e instanceof APIStatusError) { this.#logger.warn( - { statusCode: e.statusCode, body: e.body }, + { statusCode: e.statusCode, 'lk.pii.body': e.body }, 'failed to call trugen api', ); if (!e.retryable) { throw e; } } else { - this.#logger.warn({ error: String(e) }, 'failed to call trugen api'); + this.#logger.warn({ 'lk.pii.error': e }, 'failed to call trugen api'); } if (i < this.connOptions.maxRetry) { await new Promise((resolve) => diff --git a/plugins/xai/src/stt.ts b/plugins/xai/src/stt.ts index 8c47477c8..55673e679 100644 --- a/plugins/xai/src/stt.ts +++ b/plugins/xai/src/stt.ts @@ -218,12 +218,23 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - `failed to connect to xAI STT, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, + { + delay, + retries, + maxRetry, + 'lk.pii.error': e, + }, + 'failed to connect to xAI STT, retrying', ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - `xAI STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, + { + inputClosed: this.input.closed, + isClosed: this.closed, + 'lk.pii.error': e, + }, + 'xAI STT disconnected', ); } } @@ -241,7 +252,13 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise(async (_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); + this.#logger.error( + { + code, + 'lk.pii.reason': reason.toString(), + }, + 'xAI STT WebSocket closed unexpectedly', + ); reject(new Error('WebSocket closed')); } }); @@ -299,7 +316,13 @@ export class SpeechStream extends stt.SpeechStream { resolve(); } } catch (err) { - this.#logger.error(`xAI STT: error processing message: ${msg}`); + this.#logger.error( + { + 'lk.pii.error': err, + 'lk.pii.message': msg.toString(), + }, + 'xAI STT failed to process message', + ); reject(err); } }); @@ -412,9 +435,12 @@ export class SpeechStream extends stt.SpeechStream { this.#putMessage({ type: stt.SpeechEventType.END_OF_SPEECH }); } } else if (msgType === 'error') { - this.#logger.error(`xAI STT error: ${(data['message'] as string) ?? 'unknown error'}`); + this.#logger.error( + { 'lk.pii.error': (data['message'] as string) ?? 'unknown error' }, + 'xAI STT returned an error', + ); } else { - this.#logger.warn(`received unexpected message from xAI: ${msgType}`); + this.#logger.warn({ messageType: msgType }, 'received unexpected message from xAI'); } } } diff --git a/plugins/xai/src/tts.ts b/plugins/xai/src/tts.ts index aacade64d..1653f5ea5 100644 --- a/plugins/xai/src/tts.ts +++ b/plugins/xai/src/tts.ts @@ -417,7 +417,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { data = JSON.parse(event.data.toString()) as Record; } catch (e) { - this.#logger.warn({ err: e }, 'Unexpected xAI message'); + this.#logger.warn( + { + 'lk.pii.error': e, + 'lk.pii.data': event.data.toString(), + }, + 'Unexpected xAI message', + ); continue; } @@ -446,7 +452,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { options: { body: data }, }); default: - this.#logger.warn({ data }, 'Unexpected xAI message'); + this.#logger.warn({ 'lk.pii.data': data }, 'Unexpected xAI message'); } } }; From b186f9e75848a459fd80b04557f399194dca4f9d Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Mon, 27 Jul 2026 12:55:57 +0100 Subject: [PATCH 2/6] fix(telemetry): mark session keyterms as PII --- agents/src/telemetry/traces.test.ts | 32 +++++++++++++++++++++++++++++ agents/src/telemetry/traces.ts | 24 +++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/agents/src/telemetry/traces.test.ts b/agents/src/telemetry/traces.test.ts index 9ac98c306..3300d6c61 100644 --- a/agents/src/telemetry/traces.test.ts +++ b/agents/src/telemetry/traces.test.ts @@ -432,6 +432,38 @@ describe('uploadSessionReport metadata', () => { expect(records[0]?.attributes).not.toHaveProperty('session.simulation'); }); + it('marks session keyterms as PII in exported session-report logs', async () => { + const exportSpy = vi + .spyOn(SimpleOTLPHttpLogExporter.prototype, 'export') + .mockResolvedValue(undefined); + const keytermsOptions = { + keyterms: ['Acme Corp'], + keytermDetection: { enabled: false }, + }; + const report = makeReport({ + audio: false, + traces: true, + logs: false, + transcript: false, + redaction: false, + }); + report.options = { keytermsOptions }; + + await uploadSessionReport({ + agentName: 'agent', + cloudHostname: 'example.livekit.cloud', + report, + }); + + const records = exportSpy.mock.calls[0]?.[0] ?? []; + const serializedOptions = records[0]?.attributes['session.options'] as Record; + const serializedKeytermsOptions = serializedOptions.keytermsOptions as Record; + expect(serializedKeytermsOptions['lk.pii.keyterms']).toEqual(['Acme Corp']); + expect(serializedKeytermsOptions).not.toHaveProperty('keyterms'); + expect(serializedKeytermsOptions.keytermDetection).toEqual({ enabled: false }); + expect(keytermsOptions.keyterms).toEqual(['Acme Corp']); + }); + it('sets job, simulation, and redaction fields on the multipart recording header', async () => { vi.spyOn(SimpleOTLPHttpLogExporter.prototype, 'export').mockResolvedValue(undefined); const submitSpy = mockSuccessfulFormSubmit(); diff --git a/agents/src/telemetry/traces.ts b/agents/src/telemetry/traces.ts index 08b070c3b..df7c19a31 100644 --- a/agents/src/telemetry/traces.ts +++ b/agents/src/telemetry/traces.ts @@ -719,6 +719,28 @@ function toRFC3339(valueMs: number | Date): string { return truncated.toISOString(); } +const SESSION_OPTION_KEY_ALIASES: Record = { + keyterms: 'lk.pii.keyterms', +}; + +function serializeSessionOptions(options: SessionReport['options']): Record { + const serialize = (value: Record): Record => + Object.fromEntries( + Object.entries(value).map(([key, nestedValue]) => [ + SESSION_OPTION_KEY_ALIASES[key] ?? key, + nestedValue !== null && + typeof nestedValue === 'object' && + !Array.isArray(nestedValue) && + (Object.getPrototypeOf(nestedValue) === Object.prototype || + Object.getPrototypeOf(nestedValue) === null) + ? serialize(nestedValue as Record) + : nestedValue, + ]), + ); + + return serialize(options as unknown as Record); +} + /** * Upload session report to LiveKit Cloud observability. * @param options - Configuration with agentName, cloudHostname, and report @@ -770,7 +792,7 @@ export async function uploadSessionReport(options: { timestampMs: report.startedAt || report.timestamp || 0, attributes: { ...commonAttrs, - 'session.options': report.options || {}, + 'session.options': serializeSessionOptions(report.options), 'session.report_timestamp': report.timestamp, agent_name: agentName, usage, From d885c60c032d11a7d54b3168073a082460ca606e Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Mon, 27 Jul 2026 13:14:40 +0100 Subject: [PATCH 3/6] fix(telemetry): keep operational errors non-PII --- agents/src/beta/tools/end_call.ts | 6 +-- agents/src/cli.ts | 9 ++-- agents/src/cli_client.ts | 4 +- agents/src/console.ts | 4 +- agents/src/download.ts | 12 ++--- agents/src/inference/_warmup.ts | 2 +- agents/src/inference/eot/detector.ts | 4 +- agents/src/inference/eot/runner.ts | 2 +- agents/src/inference/eot/transports.ts | 4 +- .../inference/interruption/ws_transport.ts | 4 +- agents/src/inference/stt.ts | 13 ++--- agents/src/inference/tts.ts | 6 +-- agents/src/inference/vad.ts | 2 +- agents/src/ipc/job_proc_lazy_main.ts | 14 +++--- agents/src/job.ts | 11 ++--- agents/src/llm/fallback_adapter.ts | 14 ++---- agents/src/llm/llm.ts | 2 +- agents/src/llm/realtime.ts | 6 +-- agents/src/log.test.ts | 23 +++++++-- agents/src/log.ts | 1 - agents/src/stream/multi_input_stream.ts | 2 +- agents/src/stt/fallback_adapter.ts | 27 ++++------- agents/src/stt/stt.ts | 4 +- agents/src/tts/fallback_adapter.ts | 14 ++---- agents/src/tts/tts.ts | 4 +- agents/src/utils.ts | 2 +- agents/src/vad.ts | 2 +- agents/src/voice/agent_activity.ts | 48 +++++++------------ agents/src/voice/agent_session.ts | 6 +-- agents/src/voice/amd.ts | 19 ++++---- agents/src/voice/audio_recognition.ts | 18 +++---- .../src/voice/avatar/avatar_session.test.ts | 2 +- agents/src/voice/avatar/avatar_session.ts | 4 +- agents/src/voice/generation.ts | 8 ++-- agents/src/voice/recorder_io/recorder_io.ts | 15 +++--- agents/src/voice/remote_session.ts | 20 ++++---- agents/src/voice/room_io/_output.ts | 6 +-- agents/src/voice/room_io/room_io.ts | 8 ++-- agents/src/voice/tool_executor.ts | 4 +- .../src/voice/transcription/synchronizer.ts | 2 +- agents/src/worker.ts | 12 ++--- agents/src/workflows/warm_transfer.ts | 12 ++--- examples/src/liveavatar_avatar.ts | 2 +- examples/src/telephony_amd.ts | 2 +- examples/src/warm_transfer.ts | 7 +-- plugins/anam/src/api.ts | 2 +- plugins/assemblyai/src/stt.ts | 6 +-- plugins/baseten/src/stt.ts | 6 +-- plugins/bey/src/avatar.ts | 4 +- plugins/cartesia/src/stt.ts | 6 +-- plugins/cartesia/src/tts.ts | 27 ++++------- plugins/deepgram/src/stt.ts | 4 +- plugins/deepgram/src/stt_v2.ts | 4 +- plugins/deepgram/src/tts.ts | 6 +-- plugins/did/src/api.ts | 4 +- plugins/elevenlabs/src/stt.ts | 9 ++-- plugins/elevenlabs/src/tts.ts | 10 ++-- plugins/fishaudio/src/tts.ts | 11 ++--- plugins/google/src/realtime/realtime_api.ts | 15 +++--- plugins/inworld/src/stt.ts | 4 +- plugins/inworld/src/tts.ts | 10 ++-- plugins/krisp/src/_krisp.ts | 8 ++-- plugins/lemonslice/src/avatar.ts | 10 ++-- plugins/lemonslice/src/meeting/audio.ts | 4 +- plugins/lemonslice/src/meeting/chat.ts | 2 +- plugins/liveavatar/src/api.ts | 2 +- plugins/liveavatar/src/avatar.ts | 8 ++-- plugins/livekit/src/hf_utils.ts | 10 +--- plugins/minimax/src/tts.ts | 2 +- plugins/neuphonic/src/tts.ts | 18 +++---- plugins/openai/src/realtime/realtime_model.ts | 20 +++----- plugins/protoface/src/avatar.ts | 5 +- plugins/resemble/src/tts.ts | 20 +++----- plugins/rime/src/tts.ts | 2 +- plugins/runway/src/avatar.ts | 14 ++---- plugins/sarvam/src/stt.ts | 8 ++-- plugins/sarvam/src/tts.ts | 2 +- plugins/soniox/src/stt.ts | 2 +- plugins/soniox/src/tts.ts | 8 ++-- plugins/tavus/src/api.ts | 4 +- plugins/trugen/src/avatar.ts | 2 +- plugins/xai/src/stt.ts | 8 ++-- plugins/xai/src/tts.ts | 2 +- 83 files changed, 292 insertions(+), 390 deletions(-) diff --git a/agents/src/beta/tools/end_call.ts b/agents/src/beta/tools/end_call.ts index 523d82479..d444dd17e 100644 --- a/agents/src/beta/tools/end_call.ts +++ b/agents/src/beta/tools/end_call.ts @@ -150,9 +150,7 @@ export function createEndCallTool({ jobCtx.shutdown(String(event.reason)); }) - .catch((error) => - log().error({ 'lk.pii.error': error }, 'error during end call shutdown'), - ); + .catch((error) => log().error({ error }, 'error during end call shutdown')); ctx.speechHandle.addDoneCallback(() => { if (!(llm instanceof RealtimeModel) || !llm.capabilities.autoToolReplyGeneration) { @@ -161,7 +159,7 @@ export function createEndCallTool({ } void delayedSessionShutdown(session, signal).catch((error) => - log().error({ 'lk.pii.error': error }, 'error during delayed session shutdown'), + log().error({ error }, 'error during delayed session shutdown'), ); }); diff --git a/agents/src/cli.ts b/agents/src/cli.ts index 9e1010620..0f6f506d3 100644 --- a/agents/src/cli.ts +++ b/agents/src/cli.ts @@ -237,7 +237,7 @@ export const runApp = (opts: ServerOptions) => { connectAddr: commandOptions.connectAddr, record: commandOptions.record === true, }).catch((error) => { - log().fatal({ 'lk.pii.error': error }, 'console mode failed'); + log().fatal({ error }, 'console mode failed'); process.exit(1); }); }); @@ -267,10 +267,7 @@ export const runApp = (opts: ServerOptions) => { logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); } catch (error) { failures.push({ plugin, error }); - logger.error( - { 'lk.pii.error': error, plugin: plugin.title }, - 'failed to download plugin files', - ); + logger.error({ error, plugin: plugin.title }, 'failed to download plugin files'); } } @@ -284,7 +281,7 @@ export const runApp = (opts: ServerOptions) => { process.exit(0); }) .catch((error) => { - logger.fatal({ 'lk.pii.error': error }, 'error during file downloads'); + logger.fatal({ error }, 'error during file downloads'); process.exit(1); }); }); diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index 0b360e786..b76ee4467 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -61,12 +61,12 @@ export class CLIClient { // Best-effort: log and move on if the CLI isn't listening. socket.on('error', (err) => { - logger.debug({ 'lk.pii.error': err }, 'dev channel unavailable'); + logger.debug({ error: err }, 'dev channel unavailable'); }); this.#socket = socket; } catch (err) { - logger.debug({ 'lk.pii.error': err }, 'dev channel unavailable'); + logger.debug({ error: err }, 'dev channel unavailable'); } } diff --git a/agents/src/console.ts b/agents/src/console.ts index e98563129..01f1964c3 100644 --- a/agents/src/console.ts +++ b/agents/src/console.ts @@ -158,7 +158,7 @@ export async function runConsole({ try { await fn(); } catch (error) { - logger.error({ 'lk.pii.error': error, step }, 'error during teardown'); + logger.error({ error, step }, 'error during teardown'); } }; @@ -179,7 +179,7 @@ export async function runConsole({ const results = await Promise.allSettled(jobCtx.shutdownCallbacks.map((cb) => cb())); for (const result of results) { if (result.status === 'rejected') { - logger.error({ 'lk.pii.error': result.reason }, 'error while running shutdown callback'); + logger.error({ error: result.reason }, 'error while running shutdown callback'); } } } diff --git a/agents/src/download.ts b/agents/src/download.ts index eeea457af..6f60511ea 100644 --- a/agents/src/download.ts +++ b/agents/src/download.ts @@ -149,7 +149,7 @@ export const main = async (cwd: string = process.cwd()): Promise => { await import(pathToFileURL(entryAbs).href); } catch (error) { importFailures += 1; - logger.error({ 'lk.pii.error': error, package: pkg.name }, 'failed to import plugin package'); + logger.error({ error, package: pkg.name }, 'failed to import plugin package'); } } @@ -161,18 +161,12 @@ export const main = async (cwd: string = process.cwd()): Promise => { logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); } catch (error) { failures.push({ plugin, error }); - logger.error( - { 'lk.pii.error': error, plugin: plugin.title }, - 'failed to download plugin files', - ); + logger.error({ error, plugin: plugin.title }, 'failed to download plugin files'); } } if (failures.length > 0) { - logger.fatal( - { 'lk.pii.error': formatDownloadFailureMessage(failures) }, - 'plugin file downloads failed', - ); + logger.fatal({ error: formatDownloadFailureMessage(failures) }, 'plugin file downloads failed'); } return failures.length > 0 || importFailures > 0 ? 1 : 0; }; diff --git a/agents/src/inference/_warmup.ts b/agents/src/inference/_warmup.ts index f3ed6927e..737b52617 100644 --- a/agents/src/inference/_warmup.ts +++ b/agents/src/inference/_warmup.ts @@ -32,7 +32,7 @@ function getNative(): typeof import('@livekit/local-inference') | undefined { return nativeMod; } catch (err) { log().warn( - { 'lk.pii.error': err }, + { error: err }, '@livekit/local-inference native binding not loadable; local VAD/EOT paths disabled', ); return undefined; diff --git a/agents/src/inference/eot/detector.ts b/agents/src/inference/eot/detector.ts index 8c91e473d..7858166ab 100644 --- a/agents/src/inference/eot/detector.ts +++ b/agents/src/inference/eot/detector.ts @@ -290,7 +290,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _fallBackToLocal(reason: Error): void { if (!this._warnedCloudFailure) { this._detLogger.warn( - { 'lk.pii.error': reason }, + { error: reason }, 'cloud turn detector failed; falling back to local mini model', ); this._warnedCloudFailure = true; @@ -320,7 +320,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _onLocalFailure(reason: Error): void { if (!this._warnedLocalFailure) { this._detLogger.warn( - { 'lk.pii.error': reason }, + { error: reason }, 'local audio turn detector failed; defaulting to 1.0 and retrying on next turn', ); this._warnedLocalFailure = true; diff --git a/agents/src/inference/eot/runner.ts b/agents/src/inference/eot/runner.ts index d0ef890aa..4a009d458 100644 --- a/agents/src/inference/eot/runner.ts +++ b/agents/src/inference/eot/runner.ts @@ -57,7 +57,7 @@ export default class EotRunner extends InferenceRunner extends BaseSpeechStream { try { this.activeWs.send(JSON.stringify({ type: 'session.update', settings })); } catch (e) { - this.#logger.debug( - { 'lk.pii.error': e }, - 'failed to send session.update, ws may be closing', - ); + this.#logger.debug({ error: e }, 'failed to send session.update, ws may be closing'); } } } @@ -777,7 +774,7 @@ export class SpeechStream extends BaseSpeechStream { }); ws.on('error', (e) => { - this.#logger.error({ 'lk.pii.error': e }, 'WebSocket error'); + this.#logger.error({ error: e }, 'WebSocket error'); resourceCleanup(); reject(e); }); @@ -891,7 +888,7 @@ export class SpeechStream extends BaseSpeechStream { if (!parseResult.success) { this.#logger.warn( { - 'lk.pii.error': parseResult.error, + error: parseResult.error, 'lk.pii.raw_data': result.value, }, 'Failed to parse STT server event', @@ -1077,11 +1074,11 @@ export class SpeechStream extends BaseSpeechStream { if (e instanceof Error && e.message.includes('Queue is closed')) { // Expected behavior on disconnect, log as warning this.#logger.warn( - { 'lk.pii.error': e }, + { error: e }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ 'lk.pii.error': e }, 'Error putting transcript to queue'); + this.#logger.error({ error: e }, 'Error putting transcript to queue'); } } } diff --git a/agents/src/inference/tts.ts b/agents/src/inference/tts.ts index ba48a7a04..c9f21ceee 100644 --- a/agents/src/inference/tts.ts +++ b/agents/src/inference/tts.ts @@ -689,17 +689,17 @@ export class SynthesizeStream extends BaseSynthesizeSt // writer.write returns a promise; avoid unhandled rejections if stream is closed void eventChannel.write(validatedEvent).catch((error) => { this.#logger.debug( - { 'lk.pii.error': error }, + { error }, 'Failed writing TTS event to stream channel (likely closed)', ); }); } catch (e) { - this.#logger.error({ 'lk.pii.error': e }, 'Error parsing WebSocket message'); + this.#logger.error({ error: e }, 'Error parsing WebSocket message'); } }; const onError = (e: Error) => { - this.#logger.error({ 'lk.pii.error': e }, 'WebSocket error'); + this.#logger.error({ error: e }, 'WebSocket error'); void resourceCleanup(); try { // If the ws is misbehaving, hard-stop it immediately to avoid buffering. diff --git a/agents/src/inference/vad.ts b/agents/src/inference/vad.ts index ffe558648..a505dd9e9 100644 --- a/agents/src/inference/vad.ts +++ b/agents/src/inference/vad.ts @@ -136,7 +136,7 @@ class InferenceVADStream extends BaseVADStream { this._windowSamples = mod.VAD_WINDOW_SAMPLES; } this._pumpTask = this._pump().catch((err) => { - this._logger.error({ 'lk.pii.error': err }, 'VAD pump failed'); + this._logger.error({ error: err }, 'VAD pump failed'); }); } diff --git a/agents/src/ipc/job_proc_lazy_main.ts b/agents/src/ipc/job_proc_lazy_main.ts index acd93c348..24805d837 100644 --- a/agents/src/ipc/job_proc_lazy_main.ts +++ b/agents/src/ipc/job_proc_lazy_main.ts @@ -31,7 +31,7 @@ const safeSend = (msg: IPCMessage): boolean => { if (error instanceof Error && error.message.includes('Channel closed')) { log().debug({ msgCase: msg.case }, 'IPC channel closed, message not sent'); } else { - log().error({ 'lk.pii.error': error, msgCase: msg.case }, 'IPC send failed unexpectedly'); + log().error({ error, msgCase: msg.case }, 'IPC send failed unexpectedly'); } return false; } @@ -163,7 +163,7 @@ const startJob = ( clearTimeout(unconnectedTimeout); }); } catch (error) { - logger.error({ 'lk.pii.error': error }, 'error in entry function'); + logger.error({ error }, 'error in entry function'); shutdown = true; safeSend({ case: 'exiting', @@ -183,7 +183,7 @@ const startJob = ( void sessionClosePromise.catch((sessionCloseError) => logger.debug( - { 'lk.pii.error': sessionCloseError }, + { error: sessionCloseError }, 'AgentSession.close() rejected after shutdown timeout', ), ); @@ -198,7 +198,7 @@ const startJob = ( try { await ctx._onSessionEnd(); } catch (error) { - logger.error({ 'lk.pii.error': error }, 'error in ctx._onSessionEnd'); + logger.error({ error }, 'error in ctx._onSessionEnd'); } await room.disconnect(); @@ -209,7 +209,7 @@ const startJob = ( shutdownTasks.push(callback()); } await ThrowsPromise.all(shutdownTasks).catch((error) => - logger.error({ 'lk.pii.error': error }, 'error while shutting down the job'), + logger.error({ error }, 'error while shutting down the job'), ); safeSend({ case: 'done', value: undefined }); @@ -266,7 +266,7 @@ const startJob = ( let logger = log().child({ pid: proc.pid }); process.on('unhandledRejection', (reason) => { - logger.debug({ 'lk.pii.error': reason }, 'Unhandled promise rejection'); + logger.debug({ error: reason }, 'Unhandled promise rejection'); }); logger.debug('initializing job runner'); @@ -335,7 +335,7 @@ const startJob = ( await dispose(); logger.debug('native resources disposed'); } catch (error) { - logger.warn({ 'lk.pii.error': error }, 'failed to dispose native resources'); + logger.warn({ error }, 'failed to dispose native resources'); } logger.debug('Job process shutdown'); diff --git a/agents/src/job.ts b/agents/src/job.ts index 6516a4ac9..58825dbe7 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -387,10 +387,7 @@ export class JobContext> { await client.deleteRoom(targetRoomName); this.#logger.info({ roomName: targetRoomName }, 'room deleted'); } catch (error) { - this.#logger.warn( - { 'lk.pii.error': error, roomName: targetRoomName }, - 'error while deleting room', - ); + this.#logger.warn({ error, roomName: targetRoomName }, 'error while deleting room'); } } @@ -447,7 +444,7 @@ export class JobContext> { JSON.stringify(sessionReportToJSON(report), null, 2), ); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'failed to save the session report'); + this.#logger.error({ error }, 'failed to save the session report'); } } @@ -472,7 +469,7 @@ export class JobContext> { 'Session report uploaded to LiveKit Cloud', ); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Failed to upload session report'); + this.#logger.error({ error }, 'Failed to upload session report'); } } } @@ -492,7 +489,7 @@ export class JobContext> { try { await flushOtelLogs(); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Failed to flush OTEL logs'); + this.#logger.error({ error }, 'Failed to flush OTEL logs'); } } diff --git a/agents/src/llm/fallback_adapter.ts b/agents/src/llm/fallback_adapter.ts index 821ef8ba5..19ca1fa8d 100644 --- a/agents/src/llm/fallback_adapter.ts +++ b/agents/src/llm/fallback_adapter.ts @@ -233,12 +233,9 @@ class FallbackLLMStream extends LLMStream { } catch (error) { if (error instanceof APIError) { if (checkRecovery) { - this._log.warn({ llm: llm.label(), 'lk.pii.error': error }, 'recovery failed'); + this._log.warn({ llm: llm.label(), error }, 'recovery failed'); } else { - this._log.warn( - { llm: llm.label(), 'lk.pii.error': error }, - 'failed, switching to next LLM', - ); + this._log.warn({ llm: llm.label(), error }, 'failed, switching to next LLM'); } throw error; } @@ -255,12 +252,9 @@ class FallbackLLMStream extends LLMStream { // Unexpected error if (checkRecovery) { - this._log.error({ llm: llm.label(), 'lk.pii.error': error }, 'recovery unexpected error'); + this._log.error({ llm: llm.label(), error }, 'recovery unexpected error'); } else { - this._log.error( - { llm: llm.label(), 'lk.pii.error': error }, - 'unexpected error, switching to next LLM', - ); + this._log.error({ llm: llm.label(), error }, 'unexpected error, switching to next LLM'); } throw error; } finally { diff --git a/agents/src/llm/llm.ts b/agents/src/llm/llm.ts index 2efd91c43..a484b9b58 100644 --- a/agents/src/llm/llm.ts +++ b/agents/src/llm/llm.ts @@ -225,7 +225,7 @@ export abstract class LLMStream implements AsyncIterableIterator { llm: this.#llm.label(), attempt: i + 1, retryIntervalMs: retryInterval, - 'lk.pii.error': error, + error, }, 'failed to generate LLM completion, retrying', ); diff --git a/agents/src/llm/realtime.ts b/agents/src/llm/realtime.ts index a4393cf3d..88cd27a19 100644 --- a/agents/src/llm/realtime.ts +++ b/agents/src/llm/realtime.ts @@ -194,21 +194,21 @@ export abstract class RealtimeSession extends EventEmitter { try { await this.updateInstructions(instructions); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to update the instructions'); + this.logger.error({ error }, 'failed to update the instructions'); } } if (chatCtx !== undefined) { try { await this.updateChatCtx(chatCtx); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to update the chat context'); + this.logger.error({ error }, 'failed to update the chat context'); } } if (tools !== undefined) { try { await this.updateTools(tools); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to update the tools'); + this.logger.error({ error }, 'failed to update the tools'); } } } diff --git a/agents/src/log.test.ts b/agents/src/log.test.ts index f42608b46..78941ff5a 100644 --- a/agents/src/log.test.ts +++ b/agents/src/log.test.ts @@ -52,7 +52,6 @@ describe('OTEL logging', () => { enableOtelLogging(); log().info({ 'lk.pii.user_input': 'secret transcript' }, 'received user input'); - log().error({ 'lk.pii.error': new Error('secret provider error') }, 'provider failed'); await vi.waitFor(() => { const record = emitSpy.mock.calls @@ -65,7 +64,23 @@ describe('OTEL logging', () => { 'lk.pii.user_input': 'secret transcript', }); expect(record?.msg).not.toContain('secret transcript'); + }); + }); + + it('exports operational errors under the standard error field', async () => { + initializeLogger({ pretty: false, level: 'info' }); + const emitSpy = vi.spyOn(PinoCloudExporter.prototype, 'emit').mockImplementation(() => {}); + + initPinoCloudExporter({ + cloudHostname: 'example.livekit.cloud', + roomId: 'RM_test', + jobId: 'AJ_test', + }); + enableOtelLogging(); + + log().error({ error: new Error('connection failed') }, 'provider failed'); + await vi.waitFor(() => { const errorRecord = emitSpy.mock.calls .map(([logObj]) => logObj) .find((logObj) => { @@ -73,12 +88,12 @@ describe('OTEL logging', () => { }); expect(errorRecord).toMatchObject({ msg: 'provider failed', - 'lk.pii.error': { + error: { type: 'Error', - message: 'secret provider error', + message: 'connection failed', }, }); - expect(errorRecord?.msg).not.toContain('secret provider error'); + expect(errorRecord).not.toHaveProperty('lk.pii.error'); }); }); }); diff --git a/agents/src/log.ts b/agents/src/log.ts index 8de08d6cf..36a65022d 100644 --- a/agents/src/log.ts +++ b/agents/src/log.ts @@ -52,7 +52,6 @@ const createLogger = ({ pretty, level }: LoggerOptions): Logger => { level: logLevel, serializers: { error: pino.stdSerializers.err, - 'lk.pii.error': pino.stdSerializers.err, }, }, multistream(streams), diff --git a/agents/src/stream/multi_input_stream.ts b/agents/src/stream/multi_input_stream.ts index 9e259be09..c5a92c980 100644 --- a/agents/src/stream/multi_input_stream.ts +++ b/agents/src/stream/multi_input_stream.ts @@ -158,7 +158,7 @@ export class MultiInputStream { return; } - this.logger.error({ 'lk.pii.error': e }, 'Error pumping input stream from MultiInputStream'); + this.logger.error({ error: e }, 'Error pumping input stream from MultiInputStream'); } finally { try { reader.releaseLock(); diff --git a/agents/src/stt/fallback_adapter.ts b/agents/src/stt/fallback_adapter.ts index b3e1000b7..932c412cb 100644 --- a/agents/src/stt/fallback_adapter.ts +++ b/agents/src/stt/fallback_adapter.ts @@ -237,12 +237,9 @@ export class FallbackAdapter extends STT { this.emitAvailabilityChanged(stt, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: stt.label, 'lk.pii.error': e }, 'STT recovery failed'); + this._logger.warn({ stt: stt.label, error: e }, 'STT recovery failed'); } else { - this._logger.debug( - { stt: stt.label, 'lk.pii.error': e }, - 'STT recovery unexpected error', - ); + this._logger.debug({ stt: stt.label, error: e }, 'STT recovery unexpected error'); } } }); @@ -280,13 +277,10 @@ export class FallbackAdapter extends STT { return result; } catch (e) { if (e instanceof APIError) { - this._logger.warn( - { stt: stt.label, 'lk.pii.error': e }, - 'STT failed, switching to next STT', - ); + this._logger.warn({ stt: stt.label, error: e }, 'STT failed, switching to next STT'); } else { this._logger.warn( - { stt: stt.label, 'lk.pii.error': e }, + { stt: stt.label, error: e }, 'STT unexpected error, switching to next STT', ); } @@ -399,12 +393,9 @@ class FallbackSpeechStream extends SpeechStream { this.fallbackAdapter.emitAvailabilityChanged(sttInstance, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: sttInstance.label, 'lk.pii.error': e }, 'STT recovery failed'); + this._logger.warn({ stt: sttInstance.label, error: e }, 'STT recovery failed'); } else { - this._logger.debug( - { stt: sttInstance.label, 'lk.pii.error': e }, - 'STT recovery unexpected error', - ); + this._logger.debug({ stt: sttInstance.label, error: e }, 'STT recovery unexpected error'); } } finally { sttInstance.off('error', errorSink); @@ -452,7 +443,7 @@ class FallbackSpeechStream extends SpeechStream { if (typeof item === 'symbol') current.flush(); else current.pushFrame(item); } catch (e) { - this._logger.debug({ 'lk.pii.error': e }, 'error forwarding input to main stream'); + this._logger.debug({ error: e }, 'error forwarding input to main stream'); } } } @@ -529,12 +520,12 @@ class FallbackSpeechStream extends SpeechStream { } catch (e) { if (e instanceof APIError) { this._logger.warn( - { stt: sttInstance.label, 'lk.pii.error': e }, + { stt: sttInstance.label, error: e }, 'STT failed, switching to next STT', ); } else { this._logger.warn( - { stt: sttInstance.label, 'lk.pii.error': e }, + { stt: sttInstance.label, error: e }, 'STT unexpected error, switching to next STT', ); } diff --git a/agents/src/stt/stt.ts b/agents/src/stt/stt.ts index d4b1323f9..47657d0d9 100644 --- a/agents/src/stt/stt.ts +++ b/agents/src/stt/stt.ts @@ -384,7 +384,7 @@ export abstract class SpeechStream implements AsyncIterableIterator stt: this.#stt.label, attempt: i + 1, retryIntervalMs: retryInterval, - 'lk.pii.error': error, + error, }, 'failed to recognize speech, retrying', ); @@ -437,7 +437,7 @@ export abstract class SpeechStream implements AsyncIterableIterator } catch (e) { if (e instanceof Error && e.message.includes('Queue is closed')) { this.logger.warn( - { 'lk.pii.error': e }, + { error: e }, 'Queue closed during transcript processing (expected during disconnect)', ); } diff --git a/agents/src/tts/fallback_adapter.ts b/agents/src/tts/fallback_adapter.ts index 375785c8b..4309be35d 100644 --- a/agents/src/tts/fallback_adapter.ts +++ b/agents/src/tts/fallback_adapter.ts @@ -208,10 +208,7 @@ export class FallbackAdapter extends TTS { if (controller.signal.aborted) { return; } - this._logger.debug( - { tts: tts.label, 'lk.pii.error': error }, - 'TTS recovery failed, will retry', - ); + this._logger.debug({ tts: tts.label, error }, 'TTS recovery failed, will retry'); // Retry recovery after delay (matches Python's retry behavior) const timeoutId = setTimeout(() => { this._recoveryTimeouts.delete(index); @@ -388,10 +385,7 @@ class FallbackChunkedStream extends ChunkedStream { return; } catch (error) { if (error instanceof APIError || error instanceof APIConnectionError) { - this._logger.warn( - { tts: tts.label, 'lk.pii.error': error }, - 'TTS failed, switching to next instance', - ); + this._logger.warn({ tts: tts.label, error }, 'TTS failed, switching to next instance'); this.adapter.markUnAvailable(i); } else { throw error; @@ -439,7 +433,7 @@ class FallbackSynthesizeStream extends SynthesizeStream { this.tokenBuffer.push(input); } } catch (error) { - this._logger.debug({ 'lk.pii.error': error }, 'Error reading input LLM stream'); + this._logger.debug({ error }, 'Error reading input LLM stream'); throw error; } finally { this.tokenBuffer.push(SynthesizeStream.END_OF_STREAM); @@ -606,7 +600,7 @@ class FallbackSynthesizeStream extends SynthesizeStream { if (error instanceof APIError || error instanceof APIConnectionError) { this._logger.warn( - { tts: originalTts.label, 'lk.pii.error': error }, + { tts: originalTts.label, error }, 'TTS failed, switching to next instance', ); this.adapter.markUnAvailable(i); diff --git a/agents/src/tts/tts.ts b/agents/src/tts/tts.ts index 757fb27f7..198f545fb 100644 --- a/agents/src/tts/tts.ts +++ b/agents/src/tts/tts.ts @@ -300,7 +300,7 @@ export abstract class SynthesizeStream tts: this.#tts.label, attempt: i + 1, retryIntervalMs: retryInterval, - 'lk.pii.error': error, + error, }, 'failed to synthesize speech, retrying', ); @@ -710,7 +710,7 @@ export abstract class ChunkedStream implements AsyncIterableIterator { try { callback(); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Task done callback failed'); + this.#logger.error({ error }, 'Task done callback failed'); } } this.doneCallbacks.clear(); diff --git a/agents/src/vad.ts b/agents/src/vad.ts index 68039beac..75675d368 100644 --- a/agents/src/vad.ts +++ b/agents/src/vad.ts @@ -150,7 +150,7 @@ export abstract class VADStream implements AsyncIterableIterator { await this.inputWriter.write(value); } } catch (e) { - this.logger.error({ 'lk.pii.error': e }, 'error pumping deferred stream'); + this.logger.error({ error: e }, 'error pumping deferred stream'); throw e; } finally { reader.releaseLock(); diff --git a/agents/src/voice/agent_activity.ts b/agents/src/voice/agent_activity.ts index cff810fff..679a02a75 100644 --- a/agents/src/voice/agent_activity.ts +++ b/agents/src/voice/agent_activity.ts @@ -196,7 +196,7 @@ export async function cleanupReusableResources( for (const output of outputs) { if (output.status === 'rejected') { if (logger) { - logger.error({ 'lk.pii.error': output.reason }, 'error cleaning up reusable resources'); + logger.error({ error: output.reason }, 'error cleaning up reusable resources'); } else { console.error('error cleaning up reusable resources'); } @@ -560,7 +560,7 @@ export class AgentActivity implements RecognitionHooks { !rtReused || capabilities.midSessionToolsUpdate ? this.tools : undefined, ); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to update realtime session'); + this.logger.error({ error }, 'failed to update realtime session'); } if (!capabilities.audioOutput && !this.tts && this.agentSession.output.audio) { @@ -578,7 +578,7 @@ export class AgentActivity implements RecognitionHooks { addIfMissing: true, }); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to update the instructions'); + this.logger.error({ error }, 'failed to update the instructions'); } } @@ -1784,7 +1784,7 @@ export class AgentActivity implements RecognitionHooks { try { await this.agent.onUserTurnExceeded(ev); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'error in onUserTurnExceeded callback'); + this.logger.error({ error }, 'error in onUserTurnExceeded callback'); } finally { this.userTurnExceededLocked = false; this.userTurnExceededTask = undefined; @@ -2016,10 +2016,7 @@ export class AgentActivity implements RecognitionHooks { await Promise.race([promise, waitForAbort(signal)]); } catch (error) { if (!signal.aborted) { - this.logger.error( - { 'lk.pii.error': error, operation: errorMessage }, - 'operation failed while waiting', - ); + this.logger.error({ error, operation: errorMessage }, 'operation failed while waiting'); } } } @@ -2422,7 +2419,7 @@ export class AgentActivity implements RecognitionHooks { if (e instanceof StopResponse) { return; } - this.logger.error({ 'lk.pii.error': e }, 'error occurred during onUserTurnCompleted'); + this.logger.error({ error: e }, 'error occurred during onUserTurnCompleted'); } const callbackDuration = Date.now() - startTime; @@ -2780,7 +2777,7 @@ export class AgentActivity implements RecognitionHooks { addIfMissing: true, }); } catch (e) { - this.logger.error({ 'lk.pii.error': e }, 'error occurred during updateInstructions'); + this.logger.error({ error: e }, 'error occurred during updateInstructions'); } } @@ -3664,10 +3661,7 @@ export class AgentActivity implements RecognitionHooks { if (output.played === 'partial') break; } } catch (error) { - this.logger.error( - { 'lk.pii.error': error }, - 'error reading messages from the realtime API', - ); + this.logger.error({ error }, 'error reading messages from the realtime API'); } }; @@ -3805,7 +3799,7 @@ export class AgentActivity implements RecognitionHooks { await realtimeSession.updateChatCtx(this.agent._chatCtx); } catch (error) { this.logger.warn( - { 'lk.pii.error': error }, + { error }, 'failed to sync chat context to remove never-played messages', ); } @@ -3939,7 +3933,7 @@ export class AgentActivity implements RecognitionHooks { await realtimeSession.updateChatCtx(chatCtx); } catch (error) { this.logger.warn( - { 'lk.pii.error': error }, + { error }, 'failed to update chat context before generating the function calls results', ); if (fut && !fut.done) { @@ -4198,14 +4192,14 @@ export class AgentActivity implements RecognitionHooks { try { this.realtimeSession.updateOptions({ toolChoice: originalToolChoice }); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to reset tool_choice'); + this.logger.error({ error }, 'failed to reset tool_choice'); } } if (originalTools !== undefined) { try { await this.realtimeSession.updateTools(originalTools); } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to reset tools'); + this.logger.error({ error }, 'failed to reset tools'); } } } @@ -4499,10 +4493,7 @@ export class AgentActivity implements RecognitionHooks { return detector; } catch (error: unknown) { - this.logger.warn( - { 'lk.pii.error': error }, - 'could not instantiate AdaptiveInterruptionDetector', - ); + this.logger.warn({ error }, 'could not instantiate AdaptiveInterruptionDetector'); } return undefined; } @@ -4666,13 +4657,13 @@ export class AgentActivity implements RecognitionHooks { if (this.audioRecognition) { this.audioRecognition.disableInterruptionDetection().catch((err) => { - this.logger.warn({ 'lk.pii.error': err }, 'error while disabling interruption detection'); + this.logger.warn({ error: err }, 'error while disabling interruption detection'); }); } this.logger.info( { - 'lk.pii.error': error?.message, + error: error?.message, label: error?.label, }, 'adaptive interruption disabled due to unrecoverable error, falling back to VAD-based interruption', @@ -4808,10 +4799,7 @@ export class AgentActivity implements RecognitionHooks { void activity .onToolsetToolsChanged() .catch((error) => - activity.logger.error( - { 'lk.pii.error': error }, - 'error re-advertising toolset tools', - ), + activity.logger.error({ error }, 'error re-advertising toolset tools'), ); }, }), @@ -4819,7 +4807,7 @@ export class AgentActivity implements RecognitionHooks { ); for (const output of outputs) { if (output.status === 'rejected') { - this.logger.error({ 'lk.pii.error': output.reason }, 'error setting up toolset'); + this.logger.error({ error: output.reason }, 'error setting up toolset'); } } } @@ -4828,7 +4816,7 @@ export class AgentActivity implements RecognitionHooks { const outputs = await Promise.allSettled(toolsets.map((ts) => ts.aclose())); for (const output of outputs) { if (output.status === 'rejected') { - this.logger.error({ 'lk.pii.error': output.reason }, 'error closing toolset'); + this.logger.error({ error: output.reason }, 'error closing toolset'); } } } diff --git a/agents/src/voice/agent_session.ts b/agents/src/voice/agent_session.ts index a937599d7..58ee9a4bc 100644 --- a/agents/src/voice/agent_session.ts +++ b/agents/src/voice/agent_session.ts @@ -1153,7 +1153,7 @@ export class AgentSession< throw error; } this.logger.debug( - { 'lk.pii.error': error }, + { error }, 'generateReply scheduling raced with handoff drain; retrying on next activity', ); return nextActivity.generateReply({ userMessage, ...options, inputDetails }); @@ -1714,7 +1714,7 @@ export class AgentSession< try { await this.activity.interrupt({ force: true }).await; } catch (error) { - this.logger.warn({ 'lk.pii.error': error }, 'Error interrupting activity'); + this.logger.warn({ error }, 'Error interrupting activity'); } } @@ -1780,6 +1780,6 @@ export class AgentSession< await this._roomIO?.close(); this._roomIO = undefined; - this.logger.info({ reason, 'lk.pii.error': error }, 'AgentSession closed'); + this.logger.info({ reason, error }, 'AgentSession closed'); } } diff --git a/agents/src/voice/amd.ts b/agents/src/voice/amd.ts index a17f9e3e5..f0df3ed9d 100644 --- a/agents/src/voice/amd.ts +++ b/agents/src/voice/amd.ts @@ -459,14 +459,14 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) try { await this.stt.close(); } catch (e) { - this._log.warn({ 'lk.pii.error': e }, 'AMD failed to close owned STT'); + this._log.warn({ error: e }, 'AMD failed to close owned STT'); } } if (this.llmOwned) { try { await this.llm.aclose(); } catch (e) { - this._log.warn({ 'lk.pii.error': e }, 'AMD failed to close owned LLM'); + this._log.warn({ error: e }, 'AMD failed to close owned LLM'); } } } @@ -544,7 +544,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) this.sttPumpTask = Task.from(({ signal }) => this.runSTTPump(signal)); this.sttPumpTask.result.catch((err) => { if (this.settled) return; - this._log.warn({ 'lk.pii.error': err }, 'AMD dedicated STT pump exited with error'); + this._log.warn({ error: err }, 'AMD dedicated STT pump exited with error'); }); } @@ -610,7 +610,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } } catch (err) { if (this.settled) return; - this._log.debug({ 'lk.pii.error': err }, 'AMD dedicated STT receive pump error'); + this._log.debug({ error: err }, 'AMD dedicated STT receive pump error'); } })(); @@ -697,10 +697,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) // Otherwise the SIP participant disconnected before going active: no audio // remains, so fall through and let the no-speech timer settle AMD instead // of stranding it until the detection timeout. - this._log.debug( - { 'lk.pii.error': err }, - 'AMD SIP answer wait failed; starting to listen', - ); + this._log.debug({ error: err }, 'AMD SIP answer wait failed; starting to listen'); } if (!this.settled) { @@ -715,7 +712,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) if (this.trackGateAbort?.signal.aborted) { return; } - this._log.debug({ 'lk.pii.error': err }, 'AMD listening gate failed; starting immediately'); + this._log.debug({ error: err }, 'AMD listening gate failed; starting immediately'); if (!this.settled) { this.startListening(); } @@ -842,7 +839,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } )._onAmdPrediction?.(result); } catch (err) { - this._log.debug({ 'lk.pii.error': err }, 'AMD: session host failed to handle amd_prediction'); + this._log.debug({ error: err }, 'AMD: session host failed to handle amd_prediction'); } this.emit('amd_prediction', result); } @@ -1324,7 +1321,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) abortSignal: new AbortController().signal, }); } catch (error) { - this._log.warn({ 'lk.pii.error': error, toolName: tc.name }, 'AMD tool execution failed'); + this._log.warn({ error, toolName: tc.name }, 'AMD tool execution failed'); } } diff --git a/agents/src/voice/audio_recognition.ts b/agents/src/voice/audio_recognition.ts index 211d81440..3854dd86b 100644 --- a/agents/src/voice/audio_recognition.ts +++ b/agents/src/voice/audio_recognition.ts @@ -459,7 +459,7 @@ export class AudioRecognition { if (!this.warnedTurnDetectorPushFailure) { this.warnedTurnDetectorPushFailure = true; this.logger.warn( - { 'lk.pii.error': err }, + { error: err }, 'audio EOT stream pushAudio failed; dropping frames for this turn', ); } @@ -644,14 +644,14 @@ export class AudioRecognition { this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error running VAD task'); + this.logger.error({ error: err }, 'error running VAD task'); }); this.interruptionTask = Task.from(({ signal }) => this.createInterruptionTask(this.interruptionDetection, signal), ); this.interruptionTask.result.catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error running interruption task'); + this.logger.error({ error: err }, 'error running interruption task'); }); // Open (or adopt) the audio EOT detector stream now that the activity is @@ -983,7 +983,7 @@ export class AudioRecognition { await this.interruptionStreamChannel.write(frame); return true; } catch (e: unknown) { - this.logger.warn({ 'lk.pii.error': e }, 'could not forward interruption sentinel'); + this.logger.warn({ error: e }, 'could not forward interruption sentinel'); } } return false; @@ -1781,12 +1781,12 @@ export class AudioRecognition { this.sttForwardTask = Task.from(({ signal }) => this.forwardInputAudioToStt(pipeline, signal)); this.sttForwardTask.result.catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error forwarding audio to STT pipeline'); + this.logger.error({ error: err }, 'error forwarding audio to STT pipeline'); }); this.sttConsumerTask = Task.from(({ signal }) => this.consumeSttEvents(pipeline, signal)); this.sttConsumerTask.result.catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error running STT task'); + this.logger.error({ error: err }, 'error running STT task'); }); } @@ -2079,7 +2079,7 @@ export class AudioRecognition { } finally { await cleanup(); await forwardTask?.catch((e) => { - this.logger.debug({ 'lk.pii.error': e }, 'interruption task exited with error'); + this.logger.debug({ error: e }, 'interruption task exited with error'); }); } } @@ -2173,7 +2173,7 @@ export class AudioRecognition { }; void restartStt().catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error resetting STT task'); + this.logger.error({ error: err }, 'error resetting STT task'); }); } @@ -2189,7 +2189,7 @@ export class AudioRecognition { if (this.closed) return; this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error({ 'lk.pii.error': err }, 'error running VAD task'); + this.logger.error({ error: err }, 'error running VAD task'); }); }); } diff --git a/agents/src/voice/avatar/avatar_session.test.ts b/agents/src/voice/avatar/avatar_session.test.ts index c683b0922..2fe387f25 100644 --- a/agents/src/voice/avatar/avatar_session.test.ts +++ b/agents/src/voice/avatar/avatar_session.test.ts @@ -180,7 +180,7 @@ describe('AvatarSession base', () => { expect(removeParticipantMock).toHaveBeenCalledWith('test-room', 'avatar-identity'); expect(warn).toHaveBeenCalledWith( - { 'lk.pii.error': error, identity: 'avatar-identity' }, + { error, identity: 'avatar-identity' }, 'failed to remove avatar participant', ); expect(debug).not.toHaveBeenCalledWith( diff --git a/agents/src/voice/avatar/avatar_session.ts b/agents/src/voice/avatar/avatar_session.ts index 0b29da470..eebfca04b 100644 --- a/agents/src/voice/avatar/avatar_session.ts +++ b/agents/src/voice/avatar/avatar_session.ts @@ -137,7 +137,7 @@ export class AvatarSession extends (EventEmitter as new () => TypedEmitter TypedEmitter { if (!abortController.signal.aborted) { - this.#logger.warn({ 'lk.pii.error': error }, 'failed while waiting for avatar participant'); + this.#logger.warn({ error }, 'failed while waiting for avatar participant'); } }); } diff --git a/agents/src/voice/generation.ts b/agents/src/voice/generation.ts index dbec023f1..989007d3e 100644 --- a/agents/src/voice/generation.ts +++ b/agents/src/voice/generation.ts @@ -654,7 +654,7 @@ export function performLLMInference( return; } // surface inference silent errors even when this task's rejection is never awaited - logger.error({ 'lk.pii.error': error }, 'error in llm node'); + logger.error({ error }, 'error in llm node'); throw error; } finally { span.setAttribute(traceTypes.ATTR_RESPONSE_TEXT, data.generatedText); @@ -966,7 +966,7 @@ async function forwardAudio( let resampler: AudioResampler | null = null; const cancelReader = () => { void reader.cancel().catch((error) => { - logger.debug({ 'lk.pii.error': error }, 'failed to cancel TTS stream reader after abort'); + logger.debug({ error }, 'failed to cancel TTS stream reader after abort'); }); }; signal?.addEventListener('abort', cancelReader, { once: true }); @@ -1224,7 +1224,7 @@ export function performToolExecutions({ function: toolCall.name, 'lk.pii.arguments': toolCall.args, speech_id: speechHandle.id, - 'lk.pii.error': error.message, + error: error.message, }, 'tried to call AI function with invalid arguments', ); @@ -1289,7 +1289,7 @@ export function performToolExecutions({ { function: toolCall.name, speech_id: speechHandle.id, - 'lk.pii.error': toError(rawError).message, + error: toError(rawError).message, }, 'exception occurred while executing tool', ); diff --git a/agents/src/voice/recorder_io/recorder_io.ts b/agents/src/voice/recorder_io/recorder_io.ts index 364ec53c4..5a2c3d882 100644 --- a/agents/src/voice/recorder_io/recorder_io.ts +++ b/agents/src/voice/recorder_io/recorder_io.ts @@ -156,10 +156,7 @@ export class RecorderIO { await this.outChan.write([]); } catch (err) { if (!isWritableStreamClosedError(err)) { - this.logger.error( - { 'lk.pii.error': err }, - 'Error writing final RecorderIO input buffer', - ); + this.logger.error({ error: err }, 'Error writing final RecorderIO input buffer'); } } } @@ -197,12 +194,12 @@ export class RecorderIO { const inputBuf = this.inRecord!.takeBuf(this.outRecord?._lastSpeechEndTime); this.inChan.write(inputBuf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO input buffer'); + this.logger.error({ error: err }, 'Error writing RecorderIO input buffer'); } }); this.outChan.write(buf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ error: err }, 'Error writing RecorderIO output buffer'); } }); } @@ -258,7 +255,7 @@ export class RecorderIO { break; } - this.logger.error({ 'lk.pii.error': err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ error: err }, 'Error writing RecorderIO output buffer'); } } } @@ -289,7 +286,7 @@ export class RecorderIO { if (isFfmpegTeardownError(err)) { resolve(); } else { - this.logger.error({ 'lk.pii.error': err }, 'FFmpeg encoding error'); + this.logger.error({ error: err }, 'FFmpeg encoding error'); reject(err); } }) @@ -432,7 +429,7 @@ export class RecorderIO { await this.ffmpegPromise; } } catch (err) { - this.logger.error({ 'lk.pii.error': err }, 'Error in encode task'); + this.logger.error({ error: err }, 'Error in encode task'); } finally { try { inReader.releaseLock(); diff --git a/agents/src/voice/remote_session.ts b/agents/src/voice/remote_session.ts index d7262cc9c..71d6a2d0c 100644 --- a/agents/src/voice/remote_session.ts +++ b/agents/src/voice/remote_session.ts @@ -128,7 +128,7 @@ export class RoomSessionTransport extends SessionTransport { return; } this.readStream(reader).catch((e) => { - log().warn({ 'lk.pii.error': e }, 'failed to read binary stream message'); + log().warn({ error: e }, 'failed to read binary stream message'); }); }; @@ -149,7 +149,7 @@ export class RoomSessionTransport extends SessionTransport { this.enqueue(msg); } catch (e) { if (!this.closed) { - log().warn({ 'lk.pii.error': e }, 'failed to parse binary stream message'); + log().warn({ error: e }, 'failed to parse binary stream message'); } } } @@ -171,7 +171,7 @@ export class RoomSessionTransport extends SessionTransport { await writer.write(new Uint8Array(data)); await writer.close(); } catch (e) { - log().warn({ 'lk.pii.error': e }, 'failed to send binary stream message'); + log().warn({ error: e }, 'failed to send binary stream message'); } } @@ -183,7 +183,7 @@ export class RoomSessionTransport extends SessionTransport { try { this.room.unregisterByteStreamHandler(TOPIC_SESSION_MESSAGES); } catch (e) { - log().debug({ 'lk.pii.error': e }, 'byte stream handler already unregistered'); + log().debug({ error: e }, 'byte stream handler already unregistered'); } this.handlerRegistered = false; } @@ -342,7 +342,7 @@ export class TcpSessionTransport extends SessionTransport { try { msg = pb.AgentSessionMessage.fromBinary(payload); } catch (e) { - log().warn({ 'lk.pii.error': e }, 'failed to parse TCP session message'); + log().warn({ error: e }, 'failed to parse TCP session message'); continue; } yield msg; @@ -350,7 +350,7 @@ export class TcpSessionTransport extends SessionTransport { } } catch (e) { if (!this.closed) { - log().warn({ 'lk.pii.error': e }, 'TCP session transport read error'); + log().warn({ error: e }, 'TCP session transport read error'); } } } @@ -734,7 +734,7 @@ export class SessionHost { } } catch (e) { if (this.started) { - log().warn({ 'lk.pii.error': e }, 'error processing session message'); + log().warn({ error: e }, 'error processing session message'); } } } @@ -919,7 +919,7 @@ export class SessionHost { try { await this.handleRequest(req); } catch (e) { - log().warn({ 'lk.pii.error': e, requestId: req.requestId }, 'error handling session request'); + log().warn({ error: e, requestId: req.requestId }, 'error handling session request'); try { const resp = new pb.AgentSessionMessage({ message: { @@ -932,7 +932,7 @@ export class SessionHost { }); await this.transport.sendMessage(resp); } catch (e) { - log().debug({ 'lk.pii.error': e }, 'failed to send error response'); + log().debug({ error: e }, 'failed to send error response'); } } } @@ -1256,7 +1256,7 @@ export class RemoteSession extends (EventEmitter as new () => TypedEventEmitter< } } catch (e) { if (this.started) { - this._logger.warn({ 'lk.pii.error': e }, 'error in RemoteSession recv loop'); + this._logger.warn({ error: e }, 'error in RemoteSession recv loop'); } } } diff --git a/agents/src/voice/room_io/_output.ts b/agents/src/voice/room_io/_output.ts index e7123d926..9b55d3627 100644 --- a/agents/src/voice/room_io/_output.ts +++ b/agents/src/voice/room_io/_output.ts @@ -207,7 +207,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); + this.logger.error({ error }, 'failed to publish transcription'); } } @@ -274,7 +274,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); + this.logger.error({ error }, 'failed to publish transcription'); } } } @@ -333,7 +333,7 @@ export class ParticipantLegacyTranscriptionOutput extends BaseParticipantTranscr }); } } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'failed to publish transcription'); + this.logger.error({ error }, 'failed to publish transcription'); } } diff --git a/agents/src/voice/room_io/room_io.ts b/agents/src/voice/room_io/room_io.ts index d4db364bb..9eacf9a26 100644 --- a/agents/src/voice/room_io/room_io.ts +++ b/agents/src/voice/room_io/room_io.ts @@ -286,7 +286,7 @@ export class RoomIO { private onUserInputTranscribed = (ev: UserInputTranscribedEvent) => { this.userTranscriptWriter.write(ev).catch((error) => { - this.logger.error({ 'lk.pii.error': error }, 'failed to write transcript event to stream'); + this.logger.error({ error }, 'failed to write transcript event to stream'); }); }; @@ -365,7 +365,7 @@ export class RoomIO { }; readText().catch((error) => { - this.logger.error({ 'lk.pii.error': error }, 'error reading text input'); + this.logger.error({ error }, 'error reading text input'); }); }; @@ -384,7 +384,7 @@ export class RoomIO { } } } catch (error) { - this.logger.error({ 'lk.pii.error': error }, 'error processing transcript stream'); + this.logger.error({ error }, 'error processing transcript stream'); } } @@ -616,7 +616,7 @@ export class RoomIO { try { await this.userTranscriptWriter.close(); } catch (e) { - this.logger.debug({ 'lk.pii.error': e }, 'userTranscriptWriter already closed or errored'); + this.logger.debug({ error: e }, 'userTranscriptWriter already closed or errored'); } await this.forwardUserTranscriptTask?.cancelAndWait(); diff --git a/agents/src/voice/tool_executor.ts b/agents/src/voice/tool_executor.ts index f20f30cd3..f4f81c72d 100644 --- a/agents/src/voice/tool_executor.ts +++ b/agents/src/voice/tool_executor.ts @@ -429,7 +429,7 @@ export class ToolExecutor { this._replyTaskDone = false; this._replyTask = this.deliverReply(ctx.session).catch((error) => { log().warn( - { 'lk.pii.error': error }, + { error }, 'deliverReply failed; async tool result may not trigger a follow-up reply', ); }); @@ -511,7 +511,7 @@ export class ToolExecutor { { function: runCtx.functionCall.name, callId: runCtx.functionCall.callId, - 'lk.pii.error': exception, + error: exception, }, 'exception occurred while executing tool after a progress update', ); diff --git a/agents/src/voice/transcription/synchronizer.ts b/agents/src/voice/transcription/synchronizer.ts index c2d5b8489..005b88727 100644 --- a/agents/src/voice/transcription/synchronizer.ts +++ b/agents/src/voice/transcription/synchronizer.ts @@ -205,7 +205,7 @@ class SegmentSynchronizerImpl { this.outputStreamWriter.close(); }) .catch((error) => { - this.logger.error({ 'lk.pii.error': error }, 'mainTask SegmentSynchronizerImpl'); + this.logger.error({ error }, 'mainTask SegmentSynchronizerImpl'); }); } this.captureTask = this.captureTaskImpl(); diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 4c5c49582..3dbf8ba2f 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -486,7 +486,7 @@ export class AgentServer { this.#logger.warn( { 'lk.pii.url': this.#opts.wsURL, - 'lk.pii.error': e, + error: e, retry_count: retries, max_retry: this.#opts.maxRetry, retryDelaySeconds: delay, @@ -576,7 +576,7 @@ export class AgentServer { )) as unknown as ParticipantInfo; } catch (e) { this.#logger.fatal( - { participantIdentity, roomName, 'lk.pii.error': e }, + { participantIdentity, roomName, error: e }, 'participant not found in room', ); throw e; @@ -634,7 +634,7 @@ export class AgentServer { }); ws.addEventListener('error', (event) => { - this.#logger.error({ 'lk.pii.error': event.message }, 'worker WebSocket error'); + this.#logger.error({ error: event.message }, 'worker WebSocket error'); }); ws.addEventListener('message', (event) => { @@ -804,7 +804,7 @@ export class AgentServer { ); }) .catch((e) => { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to measure CPU load'); + this.#logger.warn({ error: e }, 'failed to measure CPU load'); }); }, UPDATE_LOAD_INTERVAL); @@ -883,9 +883,7 @@ export class AgentServer { apiSecret: this.#opts.apiSecret, }); } catch (e) { - this.#logger - .child({ requestId: req.id }) - .error({ 'lk.pii.error': e }, 'error launching job'); + this.#logger.child({ requestId: req.id }).error({ error: e }, 'error launching job'); } } else { this.#logger.child({ requestId: req.id }).warn('pending assignment not found'); diff --git a/agents/src/workflows/warm_transfer.ts b/agents/src/workflows/warm_transfer.ts index b13fb5b3f..573691eb5 100644 --- a/agents/src/workflows/warm_transfer.ts +++ b/agents/src/workflows/warm_transfer.ts @@ -232,7 +232,7 @@ export function createWarmTransferTask({ holdAudioHandle = null; } void backgroundAudio.close().catch((error) => { - logger.warn({ 'lk.pii.error': error }, 'failed to close background audio'); + logger.warn({ error }, 'failed to close background audio'); }); setIoEnabled(true); @@ -270,7 +270,7 @@ export function createWarmTransferTask({ // Cap the wait so teardown can't hang on a stuck playout. await waitUntilAborted(handle.waitForPlayout(), AbortSignal.timeout(10_000)); } catch (error) { - logger.warn({ 'lk.pii.error': error }, 'failed to notify human agent of caller hangup'); + logger.warn({ error }, 'failed to notify human agent of caller hangup'); } finally { session.shutdown(); } @@ -338,7 +338,7 @@ export function createWarmTransferTask({ } const callerRoomName = callerRoom.name; void jobCtx.deleteRoom(callerRoomName).catch((error) => { - logger.warn({ 'lk.pii.error': error }, 'failed to delete caller room'); + logger.warn({ error }, 'failed to delete caller room'); }); }; @@ -347,10 +347,10 @@ export function createWarmTransferTask({ room?: Room | null, ): Promise => { await room?.disconnect().catch((error) => { - logger.warn({ 'lk.pii.error': error }, 'failed to disconnect human agent room'); + logger.warn({ error }, 'failed to disconnect human agent room'); }); await session?.close().catch((error) => { - logger.warn({ 'lk.pii.error': error }, 'failed to close transfer agent session'); + logger.warn({ error }, 'failed to close transfer agent session'); }); }; @@ -595,7 +595,7 @@ export function createWarmTransferTask({ } transferAgentSession = result.session; } catch (error) { - logger.error({ 'lk.pii.error': error }, 'could not dial human agent'); + logger.error({ error }, 'could not dial human agent'); setResult(new ToolError('could not dial human agent')); } finally { abortController.abort(); diff --git a/examples/src/liveavatar_avatar.ts b/examples/src/liveavatar_avatar.ts index d4f3063b2..f431b8c33 100644 --- a/examples/src/liveavatar_avatar.ts +++ b/examples/src/liveavatar_avatar.ts @@ -77,7 +77,7 @@ export default defineAgent({ }); session.on(voice.AgentSessionEventTypes.Error, (ev) => { - logger.error({ 'lk.pii.error': ev.error, source: ev.source }, 'Session emitted error'); + logger.error({ error: ev.error, source: ev.source }, 'Session emitted error'); }); ctx.addShutdownCallback(async () => { diff --git a/examples/src/telephony_amd.ts b/examples/src/telephony_amd.ts index c1fc80599..2c769b32e 100644 --- a/examples/src/telephony_amd.ts +++ b/examples/src/telephony_amd.ts @@ -175,7 +175,7 @@ export default defineAgent({ try { await rooms.deleteRoom(roomName); } catch (err) { - logger.warn({ 'lk.pii.error': err }, 'failed to delete room during hangup'); + logger.warn({ error: err }, 'failed to delete room during hangup'); } }); }, diff --git a/examples/src/warm_transfer.ts b/examples/src/warm_transfer.ts index a754cef81..ce392a66a 100644 --- a/examples/src/warm_transfer.ts +++ b/examples/src/warm_transfer.ts @@ -75,14 +75,11 @@ Examples on when the tool should be called: ctx.session.shutdown(); } catch (error) { if (error instanceof llm.ToolError) { - logger.error( - { 'lk.pii.error': error }, - 'failed to transfer to human agent with tool error', - ); + logger.error({ error }, 'failed to transfer to human agent with tool error'); throw error; } - logger.error({ 'lk.pii.error': error }, 'failed to transfer to human agent'); + logger.error({ error }, 'failed to transfer to human agent'); throw new llm.ToolError(`failed to transfer to human agent with error: ${error}`); } }, diff --git a/plugins/anam/src/api.ts b/plugins/anam/src/api.ts index c0c5cd447..e496aa1a6 100644 --- a/plugins/anam/src/api.ts +++ b/plugins/anam/src/api.ts @@ -108,7 +108,7 @@ export class AnamAPI { body && typeof body === 'object' ? { ...(body as Record), livekitToken: '****' } : body, - 'lk.pii.error': (e as Error)?.message, + error: (e as Error)?.message, nextRetrySec: retryInterval, }, 'Anam API error, retrying', diff --git a/plugins/assemblyai/src/stt.ts b/plugins/assemblyai/src/stt.ts index 59f49783d..b572a2495 100644 --- a/plugins/assemblyai/src/stt.ts +++ b/plugins/assemblyai/src/stt.ts @@ -362,7 +362,7 @@ export class SpeechStream extends stt.SpeechStream { retryDelaySeconds, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'failed to connect to AssemblyAI, retrying', ); @@ -372,7 +372,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'AssemblyAI disconnected', ); @@ -537,7 +537,7 @@ export class SpeechStream extends stt.SpeechStream { } catch (err) { this.#logger.error( { - 'lk.pii.error': err, + error: err, 'lk.pii.message': msg.toString(), }, 'AssemblyAI failed to process message', diff --git a/plugins/baseten/src/stt.ts b/plugins/baseten/src/stt.ts index 1fe442460..eb27d6c9f 100644 --- a/plugins/baseten/src/stt.ts +++ b/plugins/baseten/src/stt.ts @@ -142,7 +142,7 @@ export class SpeechStream extends stt.SpeechStream { delay, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'failed to connect to Baseten, retrying', ); @@ -152,7 +152,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'Baseten disconnected', ); @@ -320,7 +320,7 @@ export class SpeechStream extends stt.SpeechStream { } catch (err) { this.#logger.error( { - 'lk.pii.error': err, + error: err, 'lk.pii.message': String(data), }, 'Baseten STT failed to process message', diff --git a/plugins/bey/src/avatar.ts b/plugins/bey/src/avatar.ts index eb702fc82..9817e99c1 100644 --- a/plugins/bey/src/avatar.ts +++ b/plugins/bey/src/avatar.ts @@ -238,9 +238,9 @@ export class AvatarSession extends voice.AvatarSession { return; } catch (e) { if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call bey presence api'); + this.#logger.warn({ error: e }, 'failed to call bey presence api'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call bey presence api'); + this.#logger.error({ error: e }, 'failed to call bey presence api'); } if (i < this.connOptions.maxRetry - 1) { diff --git a/plugins/cartesia/src/stt.ts b/plugins/cartesia/src/stt.ts index f5604cc0e..96ab0e05d 100644 --- a/plugins/cartesia/src/stt.ts +++ b/plugins/cartesia/src/stt.ts @@ -355,7 +355,7 @@ export class SpeechStream extends stt.SpeechStream { await Promise.all([sendPromise, recvPromise]); if (firstError !== undefined) throw firstError; } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Cartesia STT stream error'); + this.#logger.error({ error }, 'Cartesia STT stream error'); throw error; } finally { abortController.abort(); @@ -448,7 +448,7 @@ export class SpeechStream extends stt.SpeechStream { try { msg = JSON.parse(data.toString()); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Failed to parse Cartesia STT message'); + this.#logger.error({ error }, 'Failed to parse Cartesia STT message'); return; } try { @@ -473,7 +473,7 @@ export class SpeechStream extends stt.SpeechStream { this.#processStreamEvent(msg); } } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Failed to process Cartesia STT message'); + this.#logger.error({ error }, 'Failed to process Cartesia STT message'); } }); diff --git a/plugins/cartesia/src/tts.ts b/plugins/cartesia/src/tts.ts index e6e197df3..6c515178d 100644 --- a/plugins/cartesia/src/tts.ts +++ b/plugins/cartesia/src/tts.ts @@ -352,7 +352,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ 'lk.pii.error': err }, 'Cartesia TTS response error'); + this.#logger.error({ error: err }, 'Cartesia TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -360,7 +360,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ 'lk.pii.error': err }, 'Cartesia TTS request error'); + this.#logger.error({ error: err }, 'Cartesia TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.on('close', () => { @@ -497,10 +497,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { // Set up WebSocket listeners ONCE (not in a loop) const onMessage = (data: RawData) => { void eventChannel.write(data).catch((error: unknown) => { - this.#logger.debug( - { 'lk.pii.error': error }, - 'Failed writing Cartesia event to channel (likely closed)', - ); + this.#logger.debug({ error }, 'Failed writing Cartesia event to channel (likely closed)'); }); }; @@ -526,7 +523,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { }; const onError = (err: Error) => { - this.#logger.error({ 'lk.pii.error': err }, 'Cartesia WebSocket error'); + this.#logger.error({ error: err }, 'Cartesia WebSocket error'); if (!completed && !timedOut && !streamError) { streamError = err instanceof APIError ? err : toRetryableConnectionError(err); } @@ -554,7 +551,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { const json = JSON.parse(rawMsg.toString()); serverMsg = cartesiaMessageSchema.parse(json); } catch (parseErr) { - this.#logger.warn({ 'lk.pii.error': parseErr }, 'Failed to parse Cartesia message'); + this.#logger.warn({ error: parseErr }, 'Failed to parse Cartesia message'); continue; } @@ -566,12 +563,9 @@ export class SynthesizeStream extends tts.SynthesizeStream { // below. 5xx bubbles up so the base SynthesizeStream can retry. if (isErrorMessage(serverMsg)) { if (serverMsg.status_code >= 400 && serverMsg.status_code < 500) { - this.#logger.debug( - { 'lk.pii.error': serverMsg.error }, - 'Cartesia sent a non-fatal error', - ); + this.#logger.debug({ error: serverMsg.error }, 'Cartesia sent a non-fatal error'); } else { - this.#logger.error({ 'lk.pii.error': serverMsg.error }, 'Cartesia returned error'); + this.#logger.error({ error: serverMsg.error }, 'Cartesia returned error'); throw new APIStatusError({ message: `Cartesia returned error: ${serverMsg.error}`, options: { statusCode: serverMsg.status_code, retryable: true }, @@ -670,14 +664,11 @@ export class SynthesizeStream extends tts.SynthesizeStream { err.message.includes('Channel is closed') ) { this.#logger.warn( - { 'lk.pii.error': err }, + { error: err }, 'Channel closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error( - { 'lk.pii.error': err }, - 'Error in recvTask from Cartesia WebSocket', - ); + this.#logger.error({ error: err }, 'Error in recvTask from Cartesia WebSocket'); } } } finally { diff --git a/plugins/deepgram/src/stt.ts b/plugins/deepgram/src/stt.ts index edf78c727..a260fd4c9 100644 --- a/plugins/deepgram/src/stt.ts +++ b/plugins/deepgram/src/stt.ts @@ -344,7 +344,7 @@ export class SpeechStream extends stt.SpeechStream { delay, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'failed to connect to Deepgram, retrying', ); @@ -354,7 +354,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'Deepgram disconnected', ); diff --git a/plugins/deepgram/src/stt_v2.ts b/plugins/deepgram/src/stt_v2.ts index 37a77ba38..7162be41f 100644 --- a/plugins/deepgram/src/stt_v2.ts +++ b/plugins/deepgram/src/stt_v2.ts @@ -357,7 +357,7 @@ class SpeechStreamv2 extends stt.SpeechStream { break; } } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Deepgram stream error'); + this.#logger.error({ error }, 'Deepgram stream error'); throw error; // Let Base Class handle retry logic } finally { if (this.#ws?.readyState === WebSocket.OPEN) { @@ -451,7 +451,7 @@ class SpeechStreamv2 extends stt.SpeechStream { const msg = JSON.parse(data.toString()); this.#processStreamEvent(msg); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Failed to parse Deepgram message'); + this.#logger.error({ error }, 'Failed to parse Deepgram message'); } }); diff --git a/plugins/deepgram/src/tts.ts b/plugins/deepgram/src/tts.ts index 746dd47da..10007a07d 100644 --- a/plugins/deepgram/src/tts.ts +++ b/plugins/deepgram/src/tts.ts @@ -195,7 +195,7 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ 'lk.pii.error': err }, 'Deepgram TTS response error'); + this.#logger.error({ error: err }, 'Deepgram TTS response error'); settle(() => reject(err)); }); @@ -220,7 +220,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ 'lk.pii.error': err }, 'Deepgram TTS request error'); + this.#logger.error({ error: err }, 'Deepgram TTS request error'); settle(() => reject(err)); }); @@ -282,7 +282,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'error during WebSocket close sequence'); + this.#logger.warn({ error: e }, 'error during WebSocket close sequence'); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); diff --git a/plugins/did/src/api.ts b/plugins/did/src/api.ts index 9607d73c7..c23ea23ed 100644 --- a/plugins/did/src/api.ts +++ b/plugins/did/src/api.ts @@ -128,9 +128,9 @@ export class DIDAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call d-id api'); + this.#logger.warn({ error: e }, 'failed to call d-id api'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call d-id api'); + this.#logger.error({ error: e }, 'failed to call d-id api'); } if (i < this.connOptions.maxRetry) { diff --git a/plugins/elevenlabs/src/stt.ts b/plugins/elevenlabs/src/stt.ts index 261828d14..e76b317ba 100644 --- a/plugins/elevenlabs/src/stt.ts +++ b/plugins/elevenlabs/src/stt.ts @@ -310,7 +310,7 @@ export class STT extends stt.STT { stt: this.label, attempt: i + 1, retryIntervalMs: retryInterval, - 'lk.pii.error': error, + error, }, 'failed to recognize speech, retrying', ); @@ -630,10 +630,7 @@ export class SpeechStream extends stt.SpeechStream { try { this.#processStreamEvent(parseStreamEvent(JSON.parse(msg.toString()))); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error }, - 'failed to process ElevenLabs STT message', - ); + this.#logger.error({ error }, 'failed to process ElevenLabs STT message'); } }; const onClose = (code: number, reason: Buffer) => { @@ -860,7 +857,7 @@ export class SpeechStream extends stt.SpeechStream { this.#logger.error( { messageType, - 'lk.pii.error': `${errorMsg}${detailsSuffix}`, + error: `${errorMsg}${detailsSuffix}`, }, 'ElevenLabs STT returned an error', ); diff --git a/plugins/elevenlabs/src/tts.ts b/plugins/elevenlabs/src/tts.ts index 14faf22b9..38902b614 100644 --- a/plugins/elevenlabs/src/tts.ts +++ b/plugins/elevenlabs/src/tts.ts @@ -302,7 +302,7 @@ class Connection { }); this.#ws.on('error', (error) => { - this.#logger.error({ 'lk.pii.error': error }, 'WebSocket connection error'); + this.#logger.error({ error }, 'WebSocket connection error'); reject(new APIConnectionError({ message: `WebSocket error: ${error.message}` })); }); }); @@ -423,7 +423,7 @@ class Connection { } } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'send loop error'); + this.#logger.warn({ error: e }, 'send loop error'); } finally { if (!this.#closed) { await this.close(); @@ -441,7 +441,7 @@ class Connection { const parsed = JSON.parse(rawData.toString()); messageChannel.write(parsed); } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to parse WebSocket message'); + this.#logger.warn({ error: e }, 'failed to parse WebSocket message'); } }; @@ -481,7 +481,7 @@ class Connection { this.#logger.error( { context_id: contextId, - 'lk.pii.error': data.error, + error: data.error, 'lk.pii.data': data, }, 'elevenlabs tts returned error', @@ -608,7 +608,7 @@ class Connection { } } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'recv loop error'); + this.#logger.warn({ error: e }, 'recv loop error'); for (const ctx of this.#contextData.values()) { ctx.waiter.reject(asError(e)); } diff --git a/plugins/fishaudio/src/tts.ts b/plugins/fishaudio/src/tts.ts index 5817d1bb5..5230069de 100644 --- a/plugins/fishaudio/src/tts.ts +++ b/plugins/fishaudio/src/tts.ts @@ -461,10 +461,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error( - { 'lk.pii.error': err }, - 'Fish Audio TTS error response stream error', - ); + this.#logger.error({ error: err }, 'Fish Audio TTS error response stream error'); if (!doneFut.done) { doneFut.reject( new APIStatusError({ @@ -501,7 +498,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ 'lk.pii.error': err }, 'Fish Audio TTS response error'); + this.#logger.error({ error: err }, 'Fish Audio TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -509,7 +506,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ 'lk.pii.error': err }, 'Fish Audio TTS request error'); + this.#logger.error({ error: err }, 'Fish Audio TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.write(payload); @@ -609,7 +606,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { parsed = decode(frame) as Record; } catch (err) { - this.#logger.warn({ 'lk.pii.error': err }, 'Fish Audio failed to decode message'); + this.#logger.warn({ error: err }, 'Fish Audio failed to decode message'); return; } diff --git a/plugins/google/src/realtime/realtime_api.ts b/plugins/google/src/realtime/realtime_api.ts index 538e6170a..e5dab55de 100644 --- a/plugins/google/src/realtime/realtime_api.ts +++ b/plugins/google/src/realtime/realtime_api.ts @@ -526,7 +526,7 @@ export class RealtimeSession extends llm.RealtimeSession { try { await this.activeSession.close(); } catch (error) { - this.#logger.warn({ 'lk.pii.error': error }, 'Error closing Gemini session'); + this.#logger.warn({ error }, 'Error closing Gemini session'); } finally { this.activeSession = undefined; } @@ -1016,7 +1016,7 @@ export class RealtimeSession extends llm.RealtimeSession { { code: event.code, truncated: isTruncated, - 'lk.pii.error': `${errorMsg}${truncationNote}`, + error: `${errorMsg}${truncationNote}`, }, 'Gemini Live session error', ); @@ -1089,7 +1089,7 @@ export class RealtimeSession extends llm.RealtimeSession { await cancelAndWait([sendTask, restartWaitTask], 2000); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Gemini Realtime API error'); + this.#logger.error({ error }, 'Gemini Realtime API error'); if (this.#closed) break; @@ -1206,7 +1206,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error({ 'lk.pii.error': e }, 'Gemini Live send task failed'); + this.#logger.error({ error: e }, 'Gemini Live send task failed'); this.markRestartNeeded(); } } finally { @@ -1328,7 +1328,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error({ 'lk.pii.error': e }, 'failed to process Gemini Live server event'); + this.#logger.error({ error: e }, 'failed to process Gemini Live server event'); this.markRestartNeeded(); } } @@ -1657,10 +1657,7 @@ export class RealtimeSession extends llm.RealtimeSession { gen.audioChannel.write(audioFrame); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error }, - 'Error processing Gemini Live audio data', - ); + this.#logger.error({ error }, 'Error processing Gemini Live audio data'); } } } diff --git a/plugins/inworld/src/stt.ts b/plugins/inworld/src/stt.ts index a4b802851..13ce37020 100644 --- a/plugins/inworld/src/stt.ts +++ b/plugins/inworld/src/stt.ts @@ -280,7 +280,7 @@ export class SpeechStream extends stt.SpeechStream { delay, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'Failed to connect to Inworld STT, retrying', ); @@ -290,7 +290,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'Inworld STT disconnected', ); diff --git a/plugins/inworld/src/tts.ts b/plugins/inworld/src/tts.ts index 878a15fda..050ff2191 100644 --- a/plugins/inworld/src/tts.ts +++ b/plugins/inworld/src/tts.ts @@ -262,7 +262,7 @@ class WSConnectionPool { }), ); } else { - this.#logger.error({ 'lk.pii.error': err, requestId }, 'Inworld WebSocket error'); + this.#logger.error({ error: err, requestId }, 'Inworld WebSocket error'); } }); @@ -281,10 +281,10 @@ class WSConnectionPool { this.#listeners.get(contextId)!(json); } } else if (json.error) { - this.#logger.warn({ 'lk.pii.error': json.error }, 'Inworld received error message'); + this.#logger.warn({ error: json.error }, 'Inworld received error message'); } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'Failed to parse Inworld WebSocket message'); + this.#logger.warn({ error: e }, 'Failed to parse Inworld WebSocket message'); } }); }); @@ -514,7 +514,7 @@ class ChunkedStream extends tts.ChunkedStream { } catch (e) { log().warn( { - 'lk.pii.error': e, + error: e, 'lk.pii.line': line, }, 'Failed to parse Inworld chunk', @@ -781,7 +781,7 @@ class SynthesizeStream extends tts.SynthesizeStream { } sendLastFrame(true); } catch (e) { - log().error({ 'lk.pii.error': e }, 'Error in SynthesizeStream run'); + log().error({ error: e }, 'Error in SynthesizeStream run'); throw e; } finally { ws.off('close', onClose); diff --git a/plugins/krisp/src/_krisp.ts b/plugins/krisp/src/_krisp.ts index 194715e2b..47d202240 100644 --- a/plugins/krisp/src/_krisp.ts +++ b/plugins/krisp/src/_krisp.ts @@ -108,7 +108,7 @@ class KrispLicenseSdkManager { this.module.globalDestroy(); log().debug('Krisp Audio SDK (license) destroyed'); } catch (e) { - log().error({ 'lk.pii.error': e }, 'error during Krisp SDK cleanup'); + log().error({ error: e }, 'error during Krisp SDK cleanup'); } finally { this.module = null; } @@ -219,7 +219,7 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { try { this.session.destroy(); } catch (e) { - log().error({ 'lk.pii.error': e }, 'error destroying Krisp session'); + log().error({ error: e }, 'error destroying Krisp session'); } this.session = null; } @@ -313,7 +313,7 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { const outputBuf = this.session!.process(inputBuf, this.level); chunkOut = bufferToInt16(outputBuf); } catch (e) { - log().error({ 'lk.pii.error': e }, 'error processing frame'); + log().error({ error: e }, 'error processing frame'); chunkOut = chunkIn; } if (chunkOut.length !== chunk) { @@ -348,7 +348,7 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { try { this.session.destroy(); } catch (e) { - log().error({ 'lk.pii.error': e }, 'error destroying Krisp session'); + log().error({ error: e }, 'error destroying Krisp session'); } this.session = null; } diff --git a/plugins/lemonslice/src/avatar.ts b/plugins/lemonslice/src/avatar.ts index 9136591d4..4b2ff48ca 100644 --- a/plugins/lemonslice/src/avatar.ts +++ b/plugins/lemonslice/src/avatar.ts @@ -440,7 +440,7 @@ export class AvatarSession extends voice.AvatarSession { try { await this.callLeaveMeeting(sessionId, meetingBotId); } catch (error) { - this.#logger.warn({ 'lk.pii.error': error }, 'failed to leave meeting via LemonSlice API'); + this.#logger.warn({ error }, 'failed to leave meeting via LemonSlice API'); } } @@ -552,9 +552,9 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call lemonslice api'); + this.#logger.warn({ error: e }, 'failed to call lemonslice api'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call lemonslice api'); + this.#logger.error({ error: e }, 'failed to call lemonslice api'); } if (i <= this.connOptions.maxRetry - 1) { @@ -642,9 +642,9 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call lemonslice api'); + this.#logger.warn({ error: e }, 'failed to call lemonslice api'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call lemonslice api'); + this.#logger.error({ error: e }, 'failed to call lemonslice api'); } if (i <= this.connOptions.maxRetry - 1) { diff --git a/plugins/lemonslice/src/meeting/audio.ts b/plugins/lemonslice/src/meeting/audio.ts index 58bf7cc0a..2bf90f896 100644 --- a/plugins/lemonslice/src/meeting/audio.ts +++ b/plugins/lemonslice/src/meeting/audio.ts @@ -90,7 +90,7 @@ export class MeetingAudioInput extends voice.AudioInput { for (const out of this.resample(downmixToMono(frame))) { void this.channel.write(out).catch((error) => { - this.#logger.warn({ 'lk.pii.error': error }, 'failed to write meeting audio frame'); + this.#logger.warn({ error }, 'failed to write meeting audio frame'); }); } } @@ -191,7 +191,7 @@ export async function streamMeetingRelay( if (stop.aborted) { return; } - logger.warn({ 'lk.pii.error': error, backoffMs }, 'meeting relay disconnected; retrying'); + logger.warn({ error, backoffMs }, 'meeting relay disconnected; retrying'); } if (stop.aborted) { diff --git a/plugins/lemonslice/src/meeting/chat.ts b/plugins/lemonslice/src/meeting/chat.ts index 19408a062..c5254b10e 100644 --- a/plugins/lemonslice/src/meeting/chat.ts +++ b/plugins/lemonslice/src/meeting/chat.ts @@ -108,7 +108,7 @@ export class MeetingChatRelay { this.session.interrupt(); this.session.generateReply({ userInput }); } catch (error) { - this.#logger.warn({ 'lk.pii.error': error }, 'meeting chat relay: generateReply failed'); + this.#logger.warn({ error }, 'meeting chat relay: generateReply failed'); } } } diff --git a/plugins/liveavatar/src/api.ts b/plugins/liveavatar/src/api.ts index a9c4dea89..8a0903f4f 100644 --- a/plugins/liveavatar/src/api.ts +++ b/plugins/liveavatar/src/api.ts @@ -187,7 +187,7 @@ export class LiveAvatarAPI { throw e; } this.#logger.warn( - { 'lk.pii.error': e, 'lk.pii.url': url, attempt: i }, + { error: e, 'lk.pii.url': url, attempt: i }, 'LiveAvatar API request failed', ); } diff --git a/plugins/liveavatar/src/avatar.ts b/plugins/liveavatar/src/avatar.ts index a154dad40..24453973a 100644 --- a/plugins/liveavatar/src/avatar.ts +++ b/plugins/liveavatar/src/avatar.ts @@ -244,7 +244,7 @@ export class AvatarSession extends voice.AvatarSession { // protocol failure does not surface as an unhandled rejection. The main task // itself handles its own cleanup in finally. this.mainTaskPromise = this.mainTask().catch((e) => { - this.#logger.warn({ 'lk.pii.error': e }, 'LiveAvatar main task failed'); + this.#logger.warn({ error: e }, 'LiveAvatar main task failed'); }); } @@ -462,7 +462,7 @@ export class AvatarSession extends voice.AvatarSession { wsRef.send(JSON.stringify(value)); resetKeepAlive(); } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to send LiveAvatar event'); + this.#logger.warn({ error: e }, 'failed to send LiveAvatar event'); break; } } @@ -573,7 +573,7 @@ export class AvatarSession extends voice.AvatarSession { await Promise.race([forwardAudio(), sendTask(), recvTask(), keepAliveTask()]); } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'LiveAvatar main task error'); + this.#logger.warn({ error: e }, 'LiveAvatar main task error'); } finally { this.closing = true; closingResolver.resolve(); @@ -587,7 +587,7 @@ export class AvatarSession extends voice.AvatarSession { } } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'Failed to stop LiveAvatar session'); + this.#logger.warn({ error: e }, 'Failed to stop LiveAvatar session'); } if (this.audioBuffer) { diff --git a/plugins/livekit/src/hf_utils.ts b/plugins/livekit/src/hf_utils.ts index 3177475ac..c511696bd 100644 --- a/plugins/livekit/src/hf_utils.ts +++ b/plugins/livekit/src/hf_utils.ts @@ -75,10 +75,7 @@ async function getBranchHeadCommit( logger.error({ repo: toRepoId(repo), revision }, 'No commits found for revision'); return null; } catch (error) { - logger.error( - { 'lk.pii.error': error, repo: toRepoId(repo), revision }, - 'Error getting HEAD commit', - ); + logger.error({ error, repo: toRepoId(repo), revision }, 'Error getting HEAD commit'); throw error; } } @@ -121,10 +118,7 @@ async function createSymlink(sourcePath: string, targetPath: string): Promise { if (err.message === 'aborted') return; - this.#logger.error({ 'lk.pii.error': err }, 'Neuphonic TTS response error'); + this.#logger.error({ error: err }, 'Neuphonic TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ 'lk.pii.error': err }, 'Neuphonic TTS request error'); + this.#logger.error({ error: err }, 'Neuphonic TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -263,16 +263,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error }, - 'Error parsing Neuphonic WebSocket message', - ); + this.#logger.error({ error }, 'Error parsing Neuphonic WebSocket message'); reject(error); } }); ws.on('error', (error) => { - this.#logger.error({ 'lk.pii.error': error }, 'Neuphonic WebSocket error'); + this.#logger.error({ error }, 'Neuphonic WebSocket error'); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -304,14 +301,11 @@ export class SynthesizeStream extends tts.SynthesizeStream { if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { if (err.message.includes('Queue is closed')) { this.#logger.warn( - { 'lk.pii.error': err }, + { error: err }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error( - { 'lk.pii.error': err }, - 'Error in recvTask from Neuphonic WebSocket', - ); + this.#logger.error({ error: err }, 'Error in recvTask from Neuphonic WebSocket'); } } break; diff --git a/plugins/openai/src/realtime/realtime_model.ts b/plugins/openai/src/realtime/realtime_model.ts index a4c160a9a..2d4a71415 100644 --- a/plugins/openai/src/realtime/realtime_model.ts +++ b/plugins/openai/src/realtime/realtime_model.ts @@ -677,7 +677,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } catch (e) { this.#logger.error( - { 'lk.pii.error': (e as Error).message }, + { error: (e as Error).message }, 'failed to update OpenAI Realtime chat context', ); throw e; @@ -1231,7 +1231,7 @@ export class RealtimeSession extends llm.RealtimeSession { attempt: numRetries, maxRetries, retryIntervalMs: retryInterval, - 'lk.pii.error': error, + error, }, 'OpenAI Realtime API connection failed, retrying', ); @@ -1390,7 +1390,7 @@ export class RealtimeSession extends llm.RealtimeSession { } this.#logger.error( { - 'lk.pii.error': error, + error, 'lk.pii.event': event, }, 'Failed to handle OpenAI Realtime API event', @@ -1602,10 +1602,7 @@ export class RealtimeSession extends llm.RealtimeSession { try { this.remoteChatCtx.insert(event.previous_item_id, incomingItem); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error, itemId: event.item.id }, - 'failed to insert conversation item', - ); + this.#logger.error({ error, itemId: event.item.id }, 'failed to insert conversation item'); } const fut = pendingCreateFuture; @@ -1627,10 +1624,7 @@ export class RealtimeSession extends llm.RealtimeSession { try { this.remoteChatCtx.delete(event.item_id); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error, itemId: event.item_id }, - 'failed to delete conversation item', - ); + this.#logger.error({ error, itemId: event.item_id }, 'failed to delete conversation item'); } const fut = this.itemDeleteFutures[event.item_id]; @@ -1696,7 +1690,7 @@ export class RealtimeSession extends llm.RealtimeSession { event: api_proto.ConversationItemInputAudioTranscriptionFailedEvent, ): void { this.#logger.error( - { 'lk.pii.error': event.error }, + { error: event.error }, 'OpenAI Realtime API failed to transcribe input audio', ); this.finalizePartialOnTranscriptionFailure(event.item_id, event.content_index ?? 0); @@ -2054,7 +2048,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (event.error.message.startsWith('Cancellation failed')) { return; } - this.#logger.error({ 'lk.pii.error': event.error }, 'OpenAI Realtime API returned an error'); + this.#logger.error({ error: event.error }, 'OpenAI Realtime API returned an error'); const eventId = event.error.event_id; if (eventId) { diff --git a/plugins/protoface/src/avatar.ts b/plugins/protoface/src/avatar.ts index a1dab17c7..b9693ec9d 100644 --- a/plugins/protoface/src/avatar.ts +++ b/plugins/protoface/src/avatar.ts @@ -152,10 +152,7 @@ export class AvatarSession extends voice.AvatarSession { try { await this.api.endSession(sessionId); } catch (error) { - this.#logger.warn( - { 'lk.pii.error': error, sessionId }, - 'failed to end protoface session', - ); + this.#logger.warn({ error, sessionId }, 'failed to end protoface session'); } } } finally { diff --git a/plugins/resemble/src/tts.ts b/plugins/resemble/src/tts.ts index 830efcd18..b377c4507 100644 --- a/plugins/resemble/src/tts.ts +++ b/plugins/resemble/src/tts.ts @@ -161,7 +161,7 @@ export class ChunkedStream extends tts.ChunkedStream { this.queue.close(); doneFut.resolve(); } catch (error) { - this.#logger.error({ 'lk.pii.error': error }, 'Error processing Resemble API response'); + this.#logger.error({ error }, 'Error processing Resemble API response'); this.queue.close(); doneFut.resolve(); } @@ -174,14 +174,14 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ 'lk.pii.error': err }, 'Resemble TTS response error'); + this.#logger.error({ error: err }, 'Resemble TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ 'lk.pii.error': err }, 'Resemble TTS request error'); + this.#logger.error({ error: err }, 'Resemble TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -269,7 +269,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } catch (audioError) { this.#logger.error( - { 'lk.pii.error': audioError }, + { error: audioError }, 'Error processing Resemble audio content', ); } @@ -308,16 +308,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error( - { 'lk.pii.error': error }, - 'Error parsing Resemble WebSocket message', - ); + this.#logger.error({ error }, 'Error parsing Resemble WebSocket message'); reject(error); } }); ws.on('error', (error) => { - this.#logger.error({ 'lk.pii.error': error }, 'Resemble WebSocket error'); + this.#logger.error({ error }, 'Resemble WebSocket error'); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -348,10 +345,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } catch (err) { // Skip log error for normal websocket close if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { - this.#logger.error( - { 'lk.pii.error': err }, - 'Error in recvTask from Resemble WebSocket', - ); + this.#logger.error({ error: err }, 'Error in recvTask from Resemble WebSocket'); } break; } diff --git a/plugins/rime/src/tts.ts b/plugins/rime/src/tts.ts index 928125192..8dfd7de4d 100644 --- a/plugins/rime/src/tts.ts +++ b/plugins/rime/src/tts.ts @@ -497,7 +497,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { void messageChannel.write(JSON.parse(rawData.toString())); } catch (error) { - this.#logger.warn({ 'lk.pii.error': error }, 'failed to parse Rime WebSocket message'); + this.#logger.warn({ error }, 'failed to parse Rime WebSocket message'); } }; const onClose = (code: number, reason: Buffer) => { diff --git a/plugins/runway/src/avatar.ts b/plugins/runway/src/avatar.ts index 81c74f049..cd56d7a6c 100644 --- a/plugins/runway/src/avatar.ts +++ b/plugins/runway/src/avatar.ts @@ -220,9 +220,9 @@ export class AvatarSession extends voice.AvatarSession { if (e instanceof APIStatusError && !e.retryable) throw e; if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call Runway API'); + this.#logger.warn({ error: e }, 'failed to call Runway API'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call Runway API'); + this.#logger.error({ error: e }, 'failed to call Runway API'); } if (i < maxAttempts - 1) { @@ -268,10 +268,7 @@ export class AvatarSession extends voice.AvatarSession { this.#logger.debug('sent Runway realtime session end call'); return; } catch (error) { - this.#logger.warn( - { 'lk.pii.error': error }, - 'error ending Runway realtime session via data channel', - ); + this.#logger.warn({ error }, 'error ending Runway realtime session via data channel'); } } @@ -312,10 +309,7 @@ export class AvatarSession extends voice.AvatarSession { ); } } catch (error) { - this.#logger.warn( - { sessionId, 'lk.pii.error': error }, - 'error cancelling Runway realtime session', - ); + this.#logger.warn({ sessionId, error }, 'error cancelling Runway realtime session'); } } diff --git a/plugins/sarvam/src/stt.ts b/plugins/sarvam/src/stt.ts index 41c1a75fc..dc66cef2c 100644 --- a/plugins/sarvam/src/stt.ts +++ b/plugins/sarvam/src/stt.ts @@ -633,7 +633,7 @@ export class SpeechStream extends stt.SpeechStream { delay, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'Failed to connect to Sarvam STT, retrying', ); @@ -643,7 +643,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'Sarvam STT disconnected', ); @@ -857,7 +857,7 @@ export class SpeechStream extends stt.SpeechStream { this.#logger.error( { errorCode, - 'lk.pii.error': errorInfo, + error: errorInfo, }, 'Sarvam STT WebSocket returned an error', ); @@ -871,7 +871,7 @@ export class SpeechStream extends stt.SpeechStream { } catch (err) { this.#logger.error( { - 'lk.pii.error': err, + error: err, 'lk.pii.message': msg.toString(), }, 'Error processing Sarvam STT message', diff --git a/plugins/sarvam/src/tts.ts b/plugins/sarvam/src/tts.ts index 302d5791d..e086399d0 100644 --- a/plugins/sarvam/src/tts.ts +++ b/plugins/sarvam/src/tts.ts @@ -495,7 +495,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - this.#logger.warn({ 'lk.pii.error': e }, 'Error during Sarvam WebSocket close sequence'); + this.#logger.warn({ error: e }, 'Error during Sarvam WebSocket close sequence'); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); diff --git a/plugins/soniox/src/stt.ts b/plugins/soniox/src/stt.ts index e115ca3fc..010254da5 100644 --- a/plugins/soniox/src/stt.ts +++ b/plugins/soniox/src/stt.ts @@ -252,7 +252,7 @@ export class SpeechStream extends stt.SpeechStream { this.#logger.error( { errorCode: content.error_code ?? '', - 'lk.pii.error': errorMessage, + error: errorMessage, }, 'Soniox STT WebSocket returned an error', ); diff --git a/plugins/soniox/src/tts.ts b/plugins/soniox/src/tts.ts index b3bf5ce3e..3947b188d 100644 --- a/plugins/soniox/src/tts.ts +++ b/plugins/soniox/src/tts.ts @@ -134,7 +134,7 @@ export class TTS extends tts.TTS { prewarm(): void { void currentConnection(this, this.#opts.websocketUrl, 20000).catch((error: unknown) => { - log().debug({ 'lk.pii.error': error }, 'Soniox TTS prewarm failed'); + log().debug({ error }, 'Soniox TTS prewarm failed'); }); } @@ -480,7 +480,7 @@ class Connection { this.#ws.on('message', (data) => this.#handleMessage(data.toString())); this.#ws.on('error', (error) => { - this.#logger.warn({ 'lk.pii.error': error }, 'Soniox TTS WebSocket error'); + this.#logger.warn({ error }, 'Soniox TTS WebSocket error'); this.#failAll(new APIConnectionError({ message: `Soniox TTS WebSocket error: ${error}` })); }); this.#ws.on('close', (code) => { @@ -630,7 +630,7 @@ class Connection { } } } catch (error) { - this.#logger.warn({ 'lk.pii.error': error }, 'Soniox TTS send loop error'); + this.#logger.warn({ error }, 'Soniox TTS send loop error'); this.#failAll(new APIConnectionError({ message: `Soniox TTS send loop error: ${error}` })); } } @@ -642,7 +642,7 @@ class Connection { } catch (error) { this.#logger.warn( { - 'lk.pii.error': error, + error, 'lk.pii.raw_response': raw, }, 'Failed to parse Soniox TTS response', diff --git a/plugins/tavus/src/api.ts b/plugins/tavus/src/api.ts index 663be6625..ef3755930 100644 --- a/plugins/tavus/src/api.ts +++ b/plugins/tavus/src/api.ts @@ -224,9 +224,9 @@ export class TavusAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call tavus api'); + this.#logger.warn({ error: e }, 'failed to call tavus api'); } else { - this.#logger.error({ 'lk.pii.error': e }, 'failed to call tavus api'); + this.#logger.error({ error: e }, 'failed to call tavus api'); } if (i < this.connOptions.maxRetry) { diff --git a/plugins/trugen/src/avatar.ts b/plugins/trugen/src/avatar.ts index e72ca0a82..7c71a682c 100644 --- a/plugins/trugen/src/avatar.ts +++ b/plugins/trugen/src/avatar.ts @@ -209,7 +209,7 @@ export class AvatarSession extends voice.AvatarSession { throw e; } } else { - this.#logger.warn({ 'lk.pii.error': e }, 'failed to call trugen api'); + this.#logger.warn({ error: e }, 'failed to call trugen api'); } if (i < this.connOptions.maxRetry) { await new Promise((resolve) => diff --git a/plugins/xai/src/stt.ts b/plugins/xai/src/stt.ts index 55673e679..b0a15f58e 100644 --- a/plugins/xai/src/stt.ts +++ b/plugins/xai/src/stt.ts @@ -222,7 +222,7 @@ export class SpeechStream extends stt.SpeechStream { delay, retries, maxRetry, - 'lk.pii.error': e, + error: e, }, 'failed to connect to xAI STT, retrying', ); @@ -232,7 +232,7 @@ export class SpeechStream extends stt.SpeechStream { { inputClosed: this.input.closed, isClosed: this.closed, - 'lk.pii.error': e, + error: e, }, 'xAI STT disconnected', ); @@ -318,7 +318,7 @@ export class SpeechStream extends stt.SpeechStream { } catch (err) { this.#logger.error( { - 'lk.pii.error': err, + error: err, 'lk.pii.message': msg.toString(), }, 'xAI STT failed to process message', @@ -436,7 +436,7 @@ export class SpeechStream extends stt.SpeechStream { } } else if (msgType === 'error') { this.#logger.error( - { 'lk.pii.error': (data['message'] as string) ?? 'unknown error' }, + { error: (data['message'] as string) ?? 'unknown error' }, 'xAI STT returned an error', ); } else { diff --git a/plugins/xai/src/tts.ts b/plugins/xai/src/tts.ts index 1653f5ea5..a4f84e5e2 100644 --- a/plugins/xai/src/tts.ts +++ b/plugins/xai/src/tts.ts @@ -419,7 +419,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } catch (e) { this.#logger.warn( { - 'lk.pii.error': e, + error: e, 'lk.pii.data': event.data.toString(), }, 'Unexpected xAI message', From 558267248716de8961ee42f3c1999fb7eed6dd3d Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Mon, 27 Jul 2026 13:46:02 +0100 Subject: [PATCH 4/6] fix(telemetry): avoid over-redacting operational fields --- agents/src/bin/livekit-agents.ts | 2 +- agents/src/cli.ts | 17 +++++--- agents/src/cli_client.ts | 4 +- agents/src/console.ts | 2 +- agents/src/download.ts | 13 +++--- agents/src/http_server.ts | 2 +- agents/src/inference/_warmup.ts | 2 +- agents/src/inference/eot/detector.ts | 4 +- agents/src/inference/eot/runner.ts | 5 ++- agents/src/inference/eot/transports.ts | 7 +++- .../inference/interruption/ws_transport.ts | 9 ++-- agents/src/inference/stt.ts | 6 +-- agents/src/inference/vad.ts | 5 ++- agents/src/ipc/supervised_proc.ts | 16 +++---- agents/src/llm/llm.ts | 9 +--- agents/src/llm/realtime.ts | 6 +-- agents/src/log.test.ts | 17 +++++--- agents/src/log.ts | 7 +--- agents/src/stt/fallback_adapter.ts | 42 ++++++++++++------- agents/src/stt/stream_adapter.ts | 2 +- agents/src/stt/stt.ts | 11 ++--- agents/src/tts/fallback_adapter.ts | 3 +- agents/src/tts/tts.ts | 18 ++------ agents/src/utils.ts | 4 +- agents/src/vad.ts | 2 +- agents/src/voice/agent.ts | 9 ++-- agents/src/voice/agent_activity.ts | 15 ++++--- agents/src/voice/agent_session.ts | 9 +--- agents/src/voice/amd.ts | 18 ++++---- agents/src/voice/audio_recognition.ts | 33 +++++++-------- agents/src/voice/avatar/avatar_session.ts | 2 +- agents/src/voice/background_audio.ts | 2 +- agents/src/voice/generation.ts | 12 +++--- agents/src/voice/keyterm_detection.ts | 4 +- agents/src/voice/recorder_io/recorder_io.ts | 15 ++++--- agents/src/voice/room_io/_output.ts | 6 +-- agents/src/voice/room_io/room_io.ts | 8 ++-- agents/src/voice/tool_executor.ts | 4 +- agents/src/worker.ts | 11 +++-- examples/src/idle_user_timeout_example.ts | 2 +- examples/src/inworld_tts.ts | 18 +++++++- examples/src/otel_trace.ts | 2 +- examples/src/phonic_realtime_agent.ts | 4 +- examples/src/play_local_audio_file.ts | 7 +--- examples/src/telephony_amd.ts | 2 +- plugins/assemblyai/src/stt.ts | 35 +++------------- plugins/azure/src/stt.ts | 4 +- plugins/baseten/src/stt.ts | 15 +------ plugins/bey/src/avatar.ts | 2 +- plugins/cartesia/src/stt.ts | 17 +++----- plugins/cartesia/src/tts.ts | 26 +++++------- plugins/deepgram/src/stt.ts | 26 ++---------- plugins/deepgram/src/stt_v2.ts | 12 ++---- plugins/deepgram/src/tts.ts | 11 ++--- plugins/did/src/api.ts | 2 +- plugins/elevenlabs/src/stt.ts | 19 ++------- plugins/fishaudio/src/tts.ts | 8 ++-- plugins/google/src/llm.ts | 11 ++--- plugins/google/src/realtime/realtime_api.ts | 41 +++++------------- plugins/inworld/src/stt.ts | 23 ++-------- plugins/inworld/src/tts.ts | 2 +- plugins/krisp/src/_krisp.ts | 21 +++++----- plugins/lemonslice/src/avatar.ts | 4 +- plugins/liveavatar/src/api.ts | 2 +- plugins/liveavatar/src/avatar.ts | 8 ++-- plugins/livekit/src/hf_utils.ts | 10 ++++- plugins/livekit/src/turn_detector/base.ts | 2 +- plugins/minimax/src/tts.ts | 2 +- plugins/neuphonic/src/tts.ts | 20 ++++----- plugins/openai/src/realtime/realtime_model.ts | 25 ++++------- plugins/phonic/src/realtime/realtime_model.ts | 17 ++------ plugins/protoface/src/avatar.ts | 4 +- plugins/resemble/src/tts.ts | 32 +++++--------- plugins/runway/src/avatar.ts | 12 ++++-- plugins/sarvam/src/stt.ts | 37 +++------------- plugins/sarvam/src/tts.ts | 2 +- plugins/soniox/src/stt.ts | 8 +--- plugins/soniox/src/tts.ts | 4 +- plugins/tavus/src/api.test.ts | 11 ++--- plugins/tavus/src/api.ts | 9 ++-- plugins/trugen/src/avatar.ts | 2 +- plugins/xai/src/stt.ts | 30 +++---------- plugins/xai/src/tts.ts | 2 +- 83 files changed, 353 insertions(+), 563 deletions(-) diff --git a/agents/src/bin/livekit-agents.ts b/agents/src/bin/livekit-agents.ts index 4c10e3a4a..0f6649f30 100644 --- a/agents/src/bin/livekit-agents.ts +++ b/agents/src/bin/livekit-agents.ts @@ -23,6 +23,6 @@ program }); program.parseAsync(process.argv).catch((error) => { - console.error('Fatal error', error instanceof Error ? error.name : 'UnknownError'); + console.error(`Fatal error: ${error}`); process.exit(1); }); diff --git a/agents/src/cli.ts b/agents/src/cli.ts index 0f6f506d3..713550530 100644 --- a/agents/src/cli.ts +++ b/agents/src/cli.ts @@ -24,6 +24,9 @@ type CliArgs = { cliAddr?: string; }; +const formatErrorMessage = (error: unknown): string => + error instanceof Error ? error.message : String(error); + const runServer = async (args: CliArgs) => { initializeLogger({ pretty: !args.production, level: args.opts.logLevel }); const logger = log(); @@ -42,7 +45,7 @@ const runServer = async (args: CliArgs) => { if (args.room) { server.event.once('worker_registered', () => { - logger.info({ roomName: args.room }, 'connecting to room'); + logger.info(`connecting to room ${args.room}`); server.simulateJob(args.room!, args.participantIdentity); }); } @@ -237,7 +240,7 @@ export const runApp = (opts: ServerOptions) => { connectAddr: commandOptions.connectAddr, record: commandOptions.record === true, }).catch((error) => { - log().fatal({ error }, 'console mode failed'); + log().fatal(`console mode failed: ${formatErrorMessage(error)}`); process.exit(1); }); }); @@ -261,13 +264,15 @@ export const runApp = (opts: ServerOptions) => { const failures: PluginDownloadFailure[] = []; for (const plugin of Plugin.registeredPlugins) { - logger.info({ plugin: plugin.title }, 'Downloading plugin files'); + logger.info(`Downloading files for ${plugin.title}`); try { await plugin.downloadFiles(); - logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); + logger.info(`Finished downloading files for ${plugin.title}`); } catch (error) { failures.push({ plugin, error }); - logger.error({ error, plugin: plugin.title }, 'failed to download plugin files'); + logger.error( + `Failed to download files for ${plugin.title}: ${formatErrorMessage(error)}`, + ); } } @@ -281,7 +286,7 @@ export const runApp = (opts: ServerOptions) => { process.exit(0); }) .catch((error) => { - logger.fatal({ error }, 'error during file downloads'); + logger.fatal(`Error during file downloads: ${error}`); process.exit(1); }); }); diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index b76ee4467..7942e4c32 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -61,12 +61,12 @@ export class CLIClient { // Best-effort: log and move on if the CLI isn't listening. socket.on('error', (err) => { - logger.debug({ error: err }, 'dev channel unavailable'); + logger.debug(`dev channel unavailable: ${err.message}`); }); this.#socket = socket; } catch (err) { - logger.debug({ error: err }, 'dev channel unavailable'); + logger.debug(`dev channel unavailable: ${(err as Error).message}`); } } diff --git a/agents/src/console.ts b/agents/src/console.ts index 01f1964c3..f648730b8 100644 --- a/agents/src/console.ts +++ b/agents/src/console.ts @@ -158,7 +158,7 @@ export async function runConsole({ try { await fn(); } catch (error) { - logger.error({ error, step }, 'error during teardown'); + logger.error({ error }, `error in ${step}`); } }; diff --git a/agents/src/download.ts b/agents/src/download.ts index 6f60511ea..3f4123ed9 100644 --- a/agents/src/download.ts +++ b/agents/src/download.ts @@ -138,8 +138,7 @@ export const main = async (cwd: string = process.cwd()): Promise => { return 0; } logger.info( - { pluginCount: packages.length, plugins: packages.map((p) => p.name) }, - 'discovered plugin packages', + `discovered ${packages.length} plugin package(s): ${packages.map((p) => p.name).join(', ')}`, ); let importFailures = 0; @@ -149,24 +148,24 @@ export const main = async (cwd: string = process.cwd()): Promise => { await import(pathToFileURL(entryAbs).href); } catch (error) { importFailures += 1; - logger.error({ error, package: pkg.name }, 'failed to import plugin package'); + logger.error(`failed to import ${pkg.name}: ${formatErrorMessage(error)}`); } } const failures: PluginDownloadFailure[] = []; for (const plugin of Plugin.registeredPlugins) { - logger.info({ plugin: plugin.title }, 'Downloading plugin files'); + logger.info(`Downloading files for ${plugin.title}`); try { await plugin.downloadFiles(); - logger.info({ plugin: plugin.title }, 'Finished downloading plugin files'); + logger.info(`Finished downloading files for ${plugin.title}`); } catch (error) { failures.push({ plugin, error }); - logger.error({ error, plugin: plugin.title }, 'failed to download plugin files'); + logger.error(`Failed to download files for ${plugin.title}: ${formatErrorMessage(error)}`); } } if (failures.length > 0) { - logger.fatal({ error: formatDownloadFailureMessage(failures) }, 'plugin file downloads failed'); + logger.fatal(formatDownloadFailureMessage(failures)); } return failures.length > 0 || importFailures > 0 ? 1 : 0; }; diff --git a/agents/src/http_server.ts b/agents/src/http_server.ts index 441dce690..54ebaf0a5 100644 --- a/agents/src/http_server.ts +++ b/agents/src/http_server.ts @@ -59,7 +59,7 @@ export class HTTPServer { if (err) reject(err); const address = this.app.address(); if (typeof address! !== 'string') { - this.#logger.info({ port: address!.port }, 'Server is listening'); + this.#logger.info(`Server is listening on port ${address!.port}`); } resolve(); }); diff --git a/agents/src/inference/_warmup.ts b/agents/src/inference/_warmup.ts index 737b52617..0c77e6814 100644 --- a/agents/src/inference/_warmup.ts +++ b/agents/src/inference/_warmup.ts @@ -32,7 +32,7 @@ function getNative(): typeof import('@livekit/local-inference') | undefined { return nativeMod; } catch (err) { log().warn( - { error: err }, + { err: err instanceof Error ? err.message : String(err) }, '@livekit/local-inference native binding not loadable; local VAD/EOT paths disabled', ); return undefined; diff --git a/agents/src/inference/eot/detector.ts b/agents/src/inference/eot/detector.ts index 7858166ab..805b65c6c 100644 --- a/agents/src/inference/eot/detector.ts +++ b/agents/src/inference/eot/detector.ts @@ -290,7 +290,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _fallBackToLocal(reason: Error): void { if (!this._warnedCloudFailure) { this._detLogger.warn( - { error: reason }, + { reason: reason.message }, 'cloud turn detector failed; falling back to local mini model', ); this._warnedCloudFailure = true; @@ -320,7 +320,7 @@ export class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream { _onLocalFailure(reason: Error): void { if (!this._warnedLocalFailure) { this._detLogger.warn( - { error: reason }, + { reason: reason.message }, 'local audio turn detector failed; defaulting to 1.0 and retrying on next turn', ); this._warnedLocalFailure = true; diff --git a/agents/src/inference/eot/runner.ts b/agents/src/inference/eot/runner.ts index 4a009d458..d79ed7ad5 100644 --- a/agents/src/inference/eot/runner.ts +++ b/agents/src/inference/eot/runner.ts @@ -57,7 +57,10 @@ export default class EotRunner extends InferenceRunner { - logger.debug({ code, 'lk.pii.reason': reason.toString() }, 'WebSocket closed'); + logger.debug({ code, reason: reason.toString() }, 'WebSocket closed'); }); } diff --git a/agents/src/inference/stt.ts b/agents/src/inference/stt.ts index 4c571eea8..6cdd11e27 100644 --- a/agents/src/inference/stt.ts +++ b/agents/src/inference/stt.ts @@ -727,7 +727,7 @@ export class SpeechStream extends BaseSpeechStream { try { this.activeWs.send(JSON.stringify({ type: 'session.update', settings })); } catch (e) { - this.#logger.debug({ error: e }, 'failed to send session.update, ws may be closing'); + this.#logger.debug({ err: e }, 'failed to send session.update, ws may be closing'); } } } @@ -1074,11 +1074,11 @@ export class SpeechStream extends BaseSpeechStream { if (e instanceof Error && e.message.includes('Queue is closed')) { // Expected behavior on disconnect, log as warning this.#logger.warn( - { error: e }, + { err: e }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ error: e }, 'Error putting transcript to queue'); + this.#logger.error({ err: e }, 'Error putting transcript to queue'); } } } diff --git a/agents/src/inference/vad.ts b/agents/src/inference/vad.ts index a505dd9e9..d6b65ae6e 100644 --- a/agents/src/inference/vad.ts +++ b/agents/src/inference/vad.ts @@ -136,7 +136,10 @@ class InferenceVADStream extends BaseVADStream { this._windowSamples = mod.VAD_WINDOW_SAMPLES; } this._pumpTask = this._pump().catch((err) => { - this._logger.error({ error: err }, 'VAD pump failed'); + this._logger.error( + { err: err instanceof Error ? err.message : String(err) }, + 'VAD pump failed', + ); }); } diff --git a/agents/src/ipc/supervised_proc.ts b/agents/src/ipc/supervised_proc.ts index 6a6db2247..727516a70 100644 --- a/agents/src/ipc/supervised_proc.ts +++ b/agents/src/ipc/supervised_proc.ts @@ -131,19 +131,19 @@ export abstract class SupervisedProc { if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) { this.#logger - .child({ ...this.memoryLoggingFields(memoryMB), processKind: this.processKind }) - .error('process exceeded memory limit, killing it'); + .child(this.memoryLoggingFields(memoryMB)) + .error(`${this.processKind} process exceeded memory limit, killing it`); this.close(); } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) { if (this.shouldEmitMemoryWarning(memoryMB)) { const advisory = this.#opts.memoryLimitMB <= 0; this.#logger - .child({ - ...this.memoryLoggingFields(memoryMB), - processKind: this.processKind, - advisory, - }) - .warn('process memory usage is above the warning threshold'); + .child(this.memoryLoggingFields(memoryMB)) + .warn( + `${this.processKind} process memory usage is above the warning threshold${ + advisory ? ' (advisory only, the process will not be terminated)' : '' + }`, + ); } } }, MEMORY_MONITOR_INTERVAL); diff --git a/agents/src/llm/llm.ts b/agents/src/llm/llm.ts index a484b9b58..ae9bde31c 100644 --- a/agents/src/llm/llm.ts +++ b/agents/src/llm/llm.ts @@ -221,13 +221,8 @@ export abstract class LLMStream implements AsyncIterableIterator { } else { this.emitError({ error, recoverable: true }); this.logger.warn( - { - llm: this.#llm.label(), - attempt: i + 1, - retryIntervalMs: retryInterval, - error, - }, - 'failed to generate LLM completion, retrying', + { llm: this.#llm.label(), attempt: i + 1, error }, + `failed to generate LLM completion, retrying in ${retryInterval}ms`, ); } diff --git a/agents/src/llm/realtime.ts b/agents/src/llm/realtime.ts index 88cd27a19..63db3a42e 100644 --- a/agents/src/llm/realtime.ts +++ b/agents/src/llm/realtime.ts @@ -194,21 +194,21 @@ export abstract class RealtimeSession extends EventEmitter { try { await this.updateInstructions(instructions); } catch (error) { - this.logger.error({ error }, 'failed to update the instructions'); + this.logger.error(error, 'failed to update the instructions'); } } if (chatCtx !== undefined) { try { await this.updateChatCtx(chatCtx); } catch (error) { - this.logger.error({ error }, 'failed to update the chat context'); + this.logger.error(error, 'failed to update the chat context'); } } if (tools !== undefined) { try { await this.updateTools(tools); } catch (error) { - this.logger.error({ error }, 'failed to update the tools'); + this.logger.error(error, 'failed to update the tools'); } } } diff --git a/agents/src/log.test.ts b/agents/src/log.test.ts index 78941ff5a..da5f76bdc 100644 --- a/agents/src/log.test.ts +++ b/agents/src/log.test.ts @@ -67,7 +67,7 @@ describe('OTEL logging', () => { }); }); - it('exports operational errors under the standard error field', async () => { + it('keeps operational errors and reasons in non-PII fields', async () => { initializeLogger({ pretty: false, level: 'info' }); const emitSpy = vi.spyOn(PinoCloudExporter.prototype, 'emit').mockImplementation(() => {}); @@ -79,13 +79,11 @@ describe('OTEL logging', () => { enableOtelLogging(); log().error({ error: new Error('connection failed') }, 'provider failed'); + log().warn({ reason: 'remote close' }, 'provider disconnected'); await vi.waitFor(() => { - const errorRecord = emitSpy.mock.calls - .map(([logObj]) => logObj) - .find((logObj) => { - return logObj.msg === 'provider failed'; - }); + const records = emitSpy.mock.calls.map(([logObj]) => logObj); + const errorRecord = records.find((logObj) => logObj.msg === 'provider failed'); expect(errorRecord).toMatchObject({ msg: 'provider failed', error: { @@ -94,6 +92,13 @@ describe('OTEL logging', () => { }, }); expect(errorRecord).not.toHaveProperty('lk.pii.error'); + + const reasonRecord = records.find((logObj) => logObj.msg === 'provider disconnected'); + expect(reasonRecord).toMatchObject({ + msg: 'provider disconnected', + reason: 'remote close', + }); + expect(reasonRecord).not.toHaveProperty('lk.pii.reason'); }); }); }); diff --git a/agents/src/log.ts b/agents/src/log.ts index 36a65022d..5f250901f 100644 --- a/agents/src/log.ts +++ b/agents/src/log.ts @@ -48,12 +48,7 @@ const createLogger = ({ pretty, level }: LoggerOptions): Logger => { ]; return pino( - { - level: logLevel, - serializers: { - error: pino.stdSerializers.err, - }, - }, + { level: logLevel, serializers: { error: pino.stdSerializers.err } }, multistream(streams), ); }; diff --git a/agents/src/stt/fallback_adapter.ts b/agents/src/stt/fallback_adapter.ts index 932c412cb..f733545bb 100644 --- a/agents/src/stt/fallback_adapter.ts +++ b/agents/src/stt/fallback_adapter.ts @@ -233,13 +233,13 @@ export class FallbackAdapter extends STT { try { await stt.recognize(frame, controller.signal); status.available = true; - this._logger.info({ stt: stt.label }, 'STT recovered'); + this._logger.info({ stt: stt.label }, `${stt.label} recovered`); this.emitAvailabilityChanged(stt, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: stt.label, error: e }, 'STT recovery failed'); + this._logger.warn({ stt: stt.label, err: e }, `${stt.label} recovery failed`); } else { - this._logger.debug({ stt: stt.label, error: e }, 'STT recovery unexpected error'); + this._logger.debug({ stt: stt.label, err: e }, `${stt.label} recovery unexpected error`); } } }); @@ -277,11 +277,14 @@ export class FallbackAdapter extends STT { return result; } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: stt.label, error: e }, 'STT failed, switching to next STT'); + this._logger.warn( + { stt: stt.label, err: e }, + `${stt.label} failed, switching to next STT`, + ); } else { this._logger.warn( - { stt: stt.label, error: e }, - 'STT unexpected error, switching to next STT', + { stt: stt.label, err: e }, + `${stt.label} unexpected error, switching to next STT`, ); } if (status.available) { @@ -389,13 +392,19 @@ class FallbackSpeechStream extends SpeechStream { } if (!gotTranscript) return; status.available = true; - this._logger.info({ stt: sttInstance.label }, 'STT recovered'); + this._logger.info({ stt: sttInstance.label }, `${sttInstance.label} recovered`); this.fallbackAdapter.emitAvailabilityChanged(sttInstance, true); } catch (e) { if (e instanceof APIError) { - this._logger.warn({ stt: sttInstance.label, error: e }, 'STT recovery failed'); + this._logger.warn( + { stt: sttInstance.label, err: e }, + `${sttInstance.label} recovery failed`, + ); } else { - this._logger.debug({ stt: sttInstance.label, error: e }, 'STT recovery unexpected error'); + this._logger.debug( + { stt: sttInstance.label, err: e }, + `${sttInstance.label} recovery unexpected error`, + ); } } finally { sttInstance.off('error', errorSink); @@ -443,7 +452,7 @@ class FallbackSpeechStream extends SpeechStream { if (typeof item === 'symbol') current.flush(); else current.pushFrame(item); } catch (e) { - this._logger.debug({ error: e }, 'error forwarding input to main stream'); + this._logger.debug({ err: e }, 'error forwarding input to main stream'); } } } @@ -516,17 +525,20 @@ class FallbackSpeechStream extends SpeechStream { status.available = false; this.fallbackAdapter.emitAvailabilityChanged(sttInstance, false); } - this._logger.warn({ stt: sttInstance.label }, 'STT failed, switching to next STT'); + this._logger.warn( + { stt: sttInstance.label }, + `${sttInstance.label} failed, switching to next STT`, + ); } catch (e) { if (e instanceof APIError) { this._logger.warn( - { stt: sttInstance.label, error: e }, - 'STT failed, switching to next STT', + { stt: sttInstance.label, err: e }, + `${sttInstance.label} failed, switching to next STT`, ); } else { this._logger.warn( - { stt: sttInstance.label, error: e }, - 'STT unexpected error, switching to next STT', + { stt: sttInstance.label, err: e }, + `${sttInstance.label} unexpected error, switching to next STT`, ); } if (status.available) { diff --git a/agents/src/stt/stream_adapter.ts b/agents/src/stt/stream_adapter.ts index 2181d6b64..648efc76b 100644 --- a/agents/src/stt/stream_adapter.ts +++ b/agents/src/stt/stream_adapter.ts @@ -121,7 +121,7 @@ export class StreamAdapterWrapper extends SpeechStream { } else { logger = logger.child({ error }); } - logger.error({ stt: this.label }, 'provider recognize task failed'); + logger.error(`${this.label}: provider recognize task failed`); continue; } } diff --git a/agents/src/stt/stt.ts b/agents/src/stt/stt.ts index 47657d0d9..b6f157519 100644 --- a/agents/src/stt/stt.ts +++ b/agents/src/stt/stt.ts @@ -380,13 +380,8 @@ export abstract class SpeechStream implements AsyncIterableIterator // Don't emit error event for recoverable errors during retry loop // to avoid ERR_UNHANDLED_ERROR or premature session termination this.logger.warn( - { - stt: this.#stt.label, - attempt: i + 1, - retryIntervalMs: retryInterval, - error, - }, - 'failed to recognize speech, retrying', + { stt: this.#stt.label, attempt: i + 1, error }, + `failed to recognize speech, retrying in ${retryInterval}ms`, ); } @@ -437,7 +432,7 @@ export abstract class SpeechStream implements AsyncIterableIterator } catch (e) { if (e instanceof Error && e.message.includes('Queue is closed')) { this.logger.warn( - { error: e }, + { err: e }, 'Queue closed during transcript processing (expected during disconnect)', ); } diff --git a/agents/src/tts/fallback_adapter.ts b/agents/src/tts/fallback_adapter.ts index 4309be35d..903231bd7 100644 --- a/agents/src/tts/fallback_adapter.ts +++ b/agents/src/tts/fallback_adapter.ts @@ -157,8 +157,7 @@ export class FallbackAdapter extends TTS { const tts = this.ttsInstances[index]!; if (this.sampleRate !== tts.sampleRate) { this._logger.debug( - { tts: tts.label, inputSampleRate: tts.sampleRate, outputSampleRate: this.sampleRate }, - 'resampling TTS audio', + `resampling ${tts.label} from ${tts.sampleRate}Hz to ${this.sampleRate}Hz`, ); return new AudioResampler(tts.sampleRate, this.sampleRate, tts.numChannels); } diff --git a/agents/src/tts/tts.ts b/agents/src/tts/tts.ts index 198f545fb..6851bc64d 100644 --- a/agents/src/tts/tts.ts +++ b/agents/src/tts/tts.ts @@ -296,13 +296,8 @@ export abstract class SynthesizeStream // Don't emit error event for recoverable errors during retry loop // to avoid ERR_UNHANDLED_ERROR or premature session termination this.logger.warn( - { - tts: this.#tts.label, - attempt: i + 1, - retryIntervalMs: retryInterval, - error, - }, - 'failed to synthesize speech, retrying', + { tts: this.#tts.label, attempt: i + 1, error }, + `failed to synthesize speech, retrying in ${retryInterval}ms`, ); } @@ -706,13 +701,8 @@ export abstract class ChunkedStream implements AsyncIterableIterator 0) { diff --git a/agents/src/utils.ts b/agents/src/utils.ts index 688a0e492..85e351e9c 100644 --- a/agents/src/utils.ts +++ b/agents/src/utils.ts @@ -550,7 +550,7 @@ export class Task { private async runTask() { const run = async () => { if (this.name) { - this.#logger.debug({ taskName: this.name }, 'Task.runTask: task started'); + this.#logger.debug(`Task.runTask: task ${this.name} started`); } return await this.fn(this.controller); }; @@ -566,7 +566,7 @@ export class Task { }) .finally(() => { if (this.name) { - this.#logger.debug({ taskName: this.name }, 'Task.runTask: task done'); + this.#logger.debug(`Task.runTask: task ${this.name} done`); } }); } diff --git a/agents/src/vad.ts b/agents/src/vad.ts index 75675d368..c0a2e8c16 100644 --- a/agents/src/vad.ts +++ b/agents/src/vad.ts @@ -150,7 +150,7 @@ export abstract class VADStream implements AsyncIterableIterator { await this.inputWriter.write(value); } } catch (e) { - this.logger.error({ error: e }, 'error pumping deferred stream'); + this.logger.error(`Error pumping deferred stream: ${e}`); throw e; } finally { reader.releaseLock(); diff --git a/agents/src/voice/agent.ts b/agents/src/voice/agent.ts index 322161e68..05536663b 100644 --- a/agents/src/voice/agent.ts +++ b/agents/src/voice/agent.ts @@ -667,10 +667,7 @@ export class AgentTask extends Agent extends Agent { - this.logger.warn({ error: err }, 'error while disabling interruption detection'); + this.logger.warn({ err }, 'error while disabling interruption detection'); }); } diff --git a/agents/src/voice/agent_session.ts b/agents/src/voice/agent_session.ts index 58ee9a4bc..c317d019d 100644 --- a/agents/src/voice/agent_session.ts +++ b/agents/src/voice/agent_session.ts @@ -793,11 +793,7 @@ export class AgentSession< // Log used IO configuration this.logger.debug( - { - audioInput: this.input.audio?.constructor.name, - audioOutput: this.output.audio?.constructor.name, - }, - 'using audio io', + `using audio io: ${this.input.audio ? '`' + this.input.audio.constructor.name + '`' : '(none)'} -> \`AgentSession\` -> ${this.output.audio ? '`' + this.output.audio.constructor.name + '`' : '(none)'}`, ); if ( @@ -814,8 +810,7 @@ export class AgentSession< } this.logger.debug( - { transcriptionOutput: this.output.transcription?.constructor.name }, - 'using transcript io', + `using transcript io: \`AgentSession\` -> ${this.output.transcription ? '`' + this.output.transcription.constructor.name + '`' : '(none)'}`, ); this.started = true; diff --git a/agents/src/voice/amd.ts b/agents/src/voice/amd.ts index f0df3ed9d..40e7aabf7 100644 --- a/agents/src/voice/amd.ts +++ b/agents/src/voice/amd.ts @@ -230,8 +230,8 @@ function warnIfNotEvaluated( if (lower === c || c.includes(lower) || lower.includes(c)) return; } log().warn( - { modelKind, model: modelName }, - 'model has not been evaluated with the AMD benchmark and may be incompatible', + `${modelKind} model ${modelName} hasn't been evaluated with our benchmark, ` + + 'it might not be compatible with amd. Set `suppressCompatibilityWarning: true` to silence this warning.', ); } @@ -459,14 +459,14 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) try { await this.stt.close(); } catch (e) { - this._log.warn({ error: e }, 'AMD failed to close owned STT'); + this._log.warn({ err: e }, 'AMD failed to close owned STT'); } } if (this.llmOwned) { try { await this.llm.aclose(); } catch (e) { - this._log.warn({ error: e }, 'AMD failed to close owned LLM'); + this._log.warn({ err: e }, 'AMD failed to close owned LLM'); } } } @@ -544,7 +544,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) this.sttPumpTask = Task.from(({ signal }) => this.runSTTPump(signal)); this.sttPumpTask.result.catch((err) => { if (this.settled) return; - this._log.warn({ error: err }, 'AMD dedicated STT pump exited with error'); + this._log.warn({ err }, 'AMD dedicated STT pump exited with error'); }); } @@ -610,7 +610,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } } catch (err) { if (this.settled) return; - this._log.debug({ error: err }, 'AMD dedicated STT receive pump error'); + this._log.debug({ err }, 'AMD dedicated STT receive pump error'); } })(); @@ -697,7 +697,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) // Otherwise the SIP participant disconnected before going active: no audio // remains, so fall through and let the no-speech timer settle AMD instead // of stranding it until the detection timeout. - this._log.debug({ error: err }, 'AMD SIP answer wait failed; starting to listen'); + this._log.debug({ err }, 'AMD SIP answer wait failed; starting to listen'); } if (!this.settled) { @@ -712,7 +712,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) if (this.trackGateAbort?.signal.aborted) { return; } - this._log.debug({ error: err }, 'AMD listening gate failed; starting immediately'); + this._log.debug({ err }, 'AMD listening gate failed; starting immediately'); if (!this.settled) { this.startListening(); } @@ -839,7 +839,7 @@ export class AMD extends (EventEmitter as new () => TypedEmitter) } )._onAmdPrediction?.(result); } catch (err) { - this._log.debug({ error: err }, 'AMD: session host failed to handle amd_prediction'); + this._log.debug({ err }, 'AMD: session host failed to handle amd_prediction'); } this.emit('amd_prediction', result); } diff --git a/agents/src/voice/audio_recognition.ts b/agents/src/voice/audio_recognition.ts index 3854dd86b..6cc8c494a 100644 --- a/agents/src/voice/audio_recognition.ts +++ b/agents/src/voice/audio_recognition.ts @@ -459,7 +459,7 @@ export class AudioRecognition { if (!this.warnedTurnDetectorPushFailure) { this.warnedTurnDetectorPushFailure = true; this.logger.warn( - { error: err }, + { err: err instanceof Error ? err.message : String(err) }, 'audio EOT stream pushAudio failed; dropping frames for this turn', ); } @@ -644,14 +644,14 @@ export class AudioRecognition { this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error({ error: err }, 'error running VAD task'); + this.logger.error(`Error running VAD task: ${err}`); }); this.interruptionTask = Task.from(({ signal }) => this.createInterruptionTask(this.interruptionDetection, signal), ); this.interruptionTask.result.catch((err) => { - this.logger.error({ error: err }, 'error running interruption task'); + this.logger.error(`Error running interruption task: ${err}`); }); // Open (or adopt) the audio EOT detector stream now that the activity is @@ -983,7 +983,9 @@ export class AudioRecognition { await this.interruptionStreamChannel.write(frame); return true; } catch (e: unknown) { - this.logger.warn({ error: e }, 'could not forward interruption sentinel'); + this.logger.warn( + `could not forward interruption sentinel: ${e instanceof Error ? e.message : String(e)}`, + ); } } return false; @@ -1417,10 +1419,7 @@ export class AudioRecognition { if (turnDetector) { if (!(await turnDetector.supportsLanguage(this.lastLanguage))) { // Unsupported language: produce no span and emit no prediction event. - this.logger.debug( - { language: this.lastLanguage }, - 'turn detector does not support language', - ); + this.logger.debug(`Turn detector does not support language ${this.lastLanguage}`); } else { await tracer.startActiveSpan( async (span) => { @@ -1781,12 +1780,12 @@ export class AudioRecognition { this.sttForwardTask = Task.from(({ signal }) => this.forwardInputAudioToStt(pipeline, signal)); this.sttForwardTask.result.catch((err) => { - this.logger.error({ error: err }, 'error forwarding audio to STT pipeline'); + this.logger.error(`Error forwarding audio to STT pipeline: ${err}`); }); this.sttConsumerTask = Task.from(({ signal }) => this.consumeSttEvents(pipeline, signal)); this.sttConsumerTask.result.catch((err) => { - this.logger.error({ error: err }, 'error running STT task'); + this.logger.error(`Error running STT task: ${err}`); }); } @@ -2058,12 +2057,8 @@ export class AudioRecognition { ), ); this.logger.warn( - { - model: interruptionDetection.label, - attempt: numRetries, - retryIntervalMs: retryInterval, - }, - 'failed to detect interruption, retrying', + { model: interruptionDetection.label, attempt: numRetries }, + `failed to detect interruption, retrying in ${retryInterval}ms`, ); numRetries++; await delay(retryInterval, { signal }); @@ -2079,7 +2074,7 @@ export class AudioRecognition { } finally { await cleanup(); await forwardTask?.catch((e) => { - this.logger.debug({ error: e }, 'interruption task exited with error'); + this.logger.debug({ err: e }, 'interruption task exited with error'); }); } } @@ -2173,7 +2168,7 @@ export class AudioRecognition { }; void restartStt().catch((err) => { - this.logger.error({ error: err }, 'error resetting STT task'); + this.logger.error(`Error resetting STT task: ${err}`); }); } @@ -2189,7 +2184,7 @@ export class AudioRecognition { if (this.closed) return; this.vadTask = Task.from(({ signal }) => this.createVadTask(this.vad, signal)); this.vadTask.result.catch((err) => { - this.logger.error({ error: err }, 'error running VAD task'); + this.logger.error(`Error running VAD task: ${err}`); }); }); } diff --git a/agents/src/voice/avatar/avatar_session.ts b/agents/src/voice/avatar/avatar_session.ts index eebfca04b..9aadd6b73 100644 --- a/agents/src/voice/avatar/avatar_session.ts +++ b/agents/src/voice/avatar/avatar_session.ts @@ -177,7 +177,7 @@ export class AvatarSession extends (EventEmitter as new () => TypedEmitter { if (!abortController.signal.aborted) { - this.#logger.warn({ error }, 'failed while waiting for avatar participant'); + this.#logger.warn({ error: String(error) }, 'failed while waiting for avatar participant'); } }); } diff --git a/agents/src/voice/background_audio.ts b/agents/src/voice/background_audio.ts index 4aff206bc..ea723e371 100644 --- a/agents/src/voice/background_audio.ts +++ b/agents/src/voice/background_audio.ts @@ -376,7 +376,7 @@ export class BackgroundAudioPlayer { ); this.publication = publication; - this.#logger.debug({ trackSid: this.publication.sid }, 'Background audio track published'); + this.#logger.debug(`Background audio track published: ${this.publication.sid}`); } private async runMixerTask(): Promise { diff --git a/agents/src/voice/generation.ts b/agents/src/voice/generation.ts index 989007d3e..a7090b5e9 100644 --- a/agents/src/voice/generation.ts +++ b/agents/src/voice/generation.ts @@ -346,7 +346,7 @@ export class _JsOutput { callId: this.toolCall.callId, function: this.toolCall.name, }, - 'AI function returned an invalid output', + `AI function ${this.toolCall.name} returned an invalid output`, ); return _SanitizedOutput.create({ toolCall: FunctionCall.create({ ...this.toolCall }), @@ -431,10 +431,9 @@ export function createToolOutput(params: { logger.error( { callId: toolCall.callId, - function: toolCall.name, 'lk.pii.output': finalOutput, }, - 'AI function returned an invalid output', + `AI function ${toolCall.name} returned an invalid output`, ); return ToolExecutionOutput.create({ toolCall: FunctionCall.create({ ...toolCall }), @@ -1173,7 +1172,7 @@ export function performToolExecutions({ function: toolCall.name, speech_id: speechHandle.id, }, - 'unknown AI function', + `unknown AI function ${toolCall.name}`, ); toolCompleted( createToolOutput({ @@ -1189,9 +1188,8 @@ export function performToolExecutions({ { function: toolCall.name, speech_id: speechHandle.id, - toolType: typeof tool, }, - 'unknown tool type', + `unknown tool type: ${typeof tool}`, ); continue; } @@ -1226,7 +1224,7 @@ export function performToolExecutions({ speech_id: speechHandle.id, error: error.message, }, - 'tried to call AI function with invalid arguments', + `tried to call AI function ${toolCall.name} with invalid arguments`, ); // Surface argument-validation errors to the LLM via ToolError so it can correct // its arguments instead of looping on the same invalid call. The argument schema diff --git a/agents/src/voice/keyterm_detection.ts b/agents/src/voice/keyterm_detection.ts index 204411127..6c7b80b59 100644 --- a/agents/src/voice/keyterm_detection.ts +++ b/agents/src/voice/keyterm_detection.ts @@ -139,7 +139,7 @@ function resolveDetectionLLM(configured?: LLM | string): LLM | undefined { return InferenceLLM.fromModelString(model); } catch { // never let detection setup break the session - log().warn({ model }, 'keyterm detection: could not create detection LLM; skipping'); + log().warn(`keyterm detection: could not create detection LLM ${model}; skipping`); return undefined; } } @@ -532,7 +532,7 @@ export async function detectKeyterms( } if (raced === timedOut) { stream.close(); - log().warn({ timeoutMs: timeout }, 'keyterm detection: pass timed out; skipping'); + log().warn(`keyterm detection: pass timed out after ${timeout}ms; skipping`); return [[], [], []]; } const result = parseToolCall(raced.toolCalls); diff --git a/agents/src/voice/recorder_io/recorder_io.ts b/agents/src/voice/recorder_io/recorder_io.ts index 5a2c3d882..d60856341 100644 --- a/agents/src/voice/recorder_io/recorder_io.ts +++ b/agents/src/voice/recorder_io/recorder_io.ts @@ -156,7 +156,7 @@ export class RecorderIO { await this.outChan.write([]); } catch (err) { if (!isWritableStreamClosedError(err)) { - this.logger.error({ error: err }, 'Error writing final RecorderIO input buffer'); + this.logger.error({ err }, 'Error writing final RecorderIO input buffer'); } } } @@ -194,12 +194,12 @@ export class RecorderIO { const inputBuf = this.inRecord!.takeBuf(this.outRecord?._lastSpeechEndTime); this.inChan.write(inputBuf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ error: err }, 'Error writing RecorderIO input buffer'); + this.logger.error({ err }, 'Error writing RecorderIO input buffer'); } }); this.outChan.write(buf).catch((err) => { if (!isWritableStreamClosedError(err)) { - this.logger.error({ error: err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ err }, 'Error writing RecorderIO output buffer'); } }); } @@ -255,7 +255,7 @@ export class RecorderIO { break; } - this.logger.error({ error: err }, 'Error writing RecorderIO output buffer'); + this.logger.error({ err }, 'Error writing RecorderIO output buffer'); } } } @@ -286,7 +286,7 @@ export class RecorderIO { if (isFfmpegTeardownError(err)) { resolve(); } else { - this.logger.error({ error: err }, 'FFmpeg encoding error'); + this.logger.error({ err }, 'FFmpeg encoding error'); reject(err); } }) @@ -357,8 +357,7 @@ export class RecorderIO { const diff = Math.abs(leftSamples.length - rightSamples.length); if (leftSamples.length < rightSamples.length) { this.logger.warn( - { missingSamples: diff }, - 'input is shorter; silence has been prepended to align the input channel', + `Input is shorter by ${diff} samples; silence has been prepended to align the input channel.`, ); const padded = new Float32Array(rightSamples.length); padded.set(leftSamples, diff); @@ -429,7 +428,7 @@ export class RecorderIO { await this.ffmpegPromise; } } catch (err) { - this.logger.error({ error: err }, 'Error in encode task'); + this.logger.error({ err }, 'Error in encode task'); } finally { try { inReader.releaseLock(); diff --git a/agents/src/voice/room_io/_output.ts b/agents/src/voice/room_io/_output.ts index 9b55d3627..a78caa9fe 100644 --- a/agents/src/voice/room_io/_output.ts +++ b/agents/src/voice/room_io/_output.ts @@ -207,7 +207,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error({ error }, 'failed to publish transcription'); + this.logger.error(error, 'failed to publish transcription'); } } @@ -274,7 +274,7 @@ export class ParticipantTranscriptionOutput extends BaseParticipantTranscription } } } catch (error) { - this.logger.error({ error }, 'failed to publish transcription'); + this.logger.error(error, 'failed to publish transcription'); } } } @@ -333,7 +333,7 @@ export class ParticipantLegacyTranscriptionOutput extends BaseParticipantTranscr }); } } catch (error) { - this.logger.error({ error }, 'failed to publish transcription'); + this.logger.error(error, 'failed to publish transcription'); } } diff --git a/agents/src/voice/room_io/room_io.ts b/agents/src/voice/room_io/room_io.ts index 9eacf9a26..ed5315bf7 100644 --- a/agents/src/voice/room_io/room_io.ts +++ b/agents/src/voice/room_io/room_io.ts @@ -286,7 +286,7 @@ export class RoomIO { private onUserInputTranscribed = (ev: UserInputTranscribedEvent) => { this.userTranscriptWriter.write(ev).catch((error) => { - this.logger.error({ error }, 'failed to write transcript event to stream'); + this.logger.error({ error }, 'Failed to write transcript event to stream'); }); }; @@ -365,7 +365,7 @@ export class RoomIO { }; readText().catch((error) => { - this.logger.error({ error }, 'error reading text input'); + this.logger.error({ error }, 'Error reading text input'); }); }; @@ -384,7 +384,7 @@ export class RoomIO { } } } catch (error) { - this.logger.error({ error }, 'error processing transcript stream'); + this.logger.error({ error }, 'Error processing transcript stream'); } } @@ -509,7 +509,7 @@ export class RoomIO { this.textStreamHandlerRegistered = true; } catch (error) { if (this.inputOptions.textEnabled) { - this.logger.warn({ topic: TOPIC_CHAT }, 'text stream handler already set, ignoring'); + this.logger.warn(`text stream handler for topic "${TOPIC_CHAT}" already set, ignoring`); } } } diff --git a/agents/src/voice/tool_executor.ts b/agents/src/voice/tool_executor.ts index f4f81c72d..b2e69c6be 100644 --- a/agents/src/voice/tool_executor.ts +++ b/agents/src/voice/tool_executor.ts @@ -355,7 +355,9 @@ export class ToolExecutor { if (settled) return; log().error( { tool: call.name, callId: call.callId, timeoutMs: DRAIN_TOOL_TIMEOUT_MS }, - 'cancelled tool is still running after the deadline; its execute() may not honor the abort signal', + `tool ${call.name} was cancelled but its execute() is still running after the deadline; it likely ` + + 'does not honor the abort signal. Observe the provided abortSignal in execute() so ' + + 'cancellation actually stops the work.', ); }, DRAIN_TOOL_TIMEOUT_MS); timer.unref?.(); diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 3dbf8ba2f..4a9332585 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -576,8 +576,7 @@ export class AgentServer { )) as unknown as ParticipantInfo; } catch (e) { this.#logger.fatal( - { participantIdentity, roomName, error: e }, - 'participant not found in room', + `participant with identity ${participantIdentity} not found in room ${roomName}`, ); throw e; } @@ -634,7 +633,7 @@ export class AgentServer { }); ws.addEventListener('error', (event) => { - this.#logger.error({ error: event.message }, 'worker WebSocket error'); + this.#logger.error('worker error:', event.message); }); ws.addEventListener('message', (event) => { @@ -708,7 +707,7 @@ export class AgentServer { delete this.#pending[job.id]; task?.resolve(msg.message.value); } else { - this.#logger.child({ job }).warn('received assignment for unknown job'); + this.#logger.child({ job }).warn('received assignment for unknown job ' + job.id); } break; } @@ -863,7 +862,7 @@ export class AgentServer { this.#pending[req.id] = new PendingAssignment(); const timer = setTimeout(() => { - this.#logger.child({ req }).warn('job assignment timed out'); + this.#logger.child({ req }).warn(`assignment for job ${req.id} timed out`); return; }, ASSIGNMENT_TIMEOUT); const asgn = await this.#pending[req.id]?.promise.then(async (asgn) => { @@ -883,7 +882,7 @@ export class AgentServer { apiSecret: this.#opts.apiSecret, }); } catch (e) { - this.#logger.child({ requestId: req.id }).error({ error: e }, 'error launching job'); + this.#logger.child({ requestId: req.id }).error(e, 'error launching job'); } } else { this.#logger.child({ requestId: req.id }).warn('pending assignment not found'); diff --git a/examples/src/idle_user_timeout_example.ts b/examples/src/idle_user_timeout_example.ts index fd438eaf6..d326c881b 100644 --- a/examples/src/idle_user_timeout_example.ts +++ b/examples/src/idle_user_timeout_example.ts @@ -64,7 +64,7 @@ export default defineAgent({ }; session.on(voice.AgentSessionEventTypes.UserStateChanged, (event) => { - logger.info({ oldState: event.oldState, newState: event.newState }, 'User state changed'); + logger.info({ event }, 'User state changed'); if (task) { task.cancel(); diff --git a/examples/src/inworld_tts.ts b/examples/src/inworld_tts.ts index ba9f6a888..112972d5f 100644 --- a/examples/src/inworld_tts.ts +++ b/examples/src/inworld_tts.ts @@ -77,13 +77,27 @@ export default defineAgent({ if (data.wordAlignment) { const { words, starts, ends } = data.wordAlignment; for (let i = 0; i < words.length; i++) { - console.log(`[Inworld TTS] Word: "${words[i]}", Start: ${starts[i]}, End: ${ends[i]}`); + logger.info( + { + 'lk.pii.word': words[i], + startTime: starts[i], + endTime: ends[i], + }, + 'Inworld TTS word alignment', + ); } } if (data.characterAlignment) { const { chars, starts, ends } = data.characterAlignment; for (let i = 0; i < chars.length; i++) { - console.log(`[Inworld TTS] Char: "${chars[i]}", Start: ${starts[i]}, End: ${ends[i]}`); + logger.info( + { + 'lk.pii.character': chars[i], + startTime: starts[i], + endTime: ends[i], + }, + 'Inworld TTS character alignment', + ); } } }); diff --git a/examples/src/otel_trace.ts b/examples/src/otel_trace.ts index d8efa979b..6cc15bf20 100644 --- a/examples/src/otel_trace.ts +++ b/examples/src/otel_trace.ts @@ -80,7 +80,7 @@ const lookupWeather = llm.tool({ location: z.string().describe('The location they are asking for'), }), execute: async ({ location }) => { - logger().info({ location }, 'Looking up weather'); + logger().info({ 'lk.pii.location': location }, 'Looking up weather'); return 'sunny with a temperature of 70 degrees.'; }, }); diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index 174eae9ec..e4004eba4 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 -import { type JobContext, ServerOptions, cli, defineAgent, llm, voice } from '@livekit/agents'; +import { type JobContext, ServerOptions, cli, defineAgent, llm, log, voice } from '@livekit/agents'; import * as phonic from '@livekit/agents-plugin-phonic'; import { fileURLToPath } from 'node:url'; import { z } from 'zod'; @@ -14,7 +14,7 @@ const toggleLight = llm.tool({ state: z.enum(['on', 'off']).describe('Whether to turn the light on or off'), }), execute: async ({ light_id, state }) => { - console.log(`Turning ${state} light ${light_id}`); + log().info({ state, 'lk.pii.light_id': light_id }, 'toggling light'); await new Promise((resolve) => setTimeout(resolve, 1_000)); return `Light ${light_id} turned ${state}`; }, diff --git a/examples/src/play_local_audio_file.ts b/examples/src/play_local_audio_file.ts index f58c4d354..51c916309 100644 --- a/examples/src/play_local_audio_file.ts +++ b/examples/src/play_local_audio_file.ts @@ -34,7 +34,7 @@ export default defineAgent({ await publication.waitForSubscription(); - logger.info({ trackSid: publication?.sid }, 'Audio track published'); + logger.info(`Audio track published: ${publication?.sid}`); const currentDir = dirname(fileURLToPath(import.meta.url)); const resourcesPath = join(currentDir, '../../agents/resources'); @@ -58,10 +58,7 @@ export default defineAgent({ frameCount++; if (frameCount % 100 === 0) { - logger.info( - { frameCount, durationSeconds: Number((frameCount * 0.1).toFixed(1)) }, - 'Played audio frames', - ); + logger.info(`Played ${frameCount} frames (${(frameCount * 0.1).toFixed(1)}s)`); } } }, diff --git a/examples/src/telephony_amd.ts b/examples/src/telephony_amd.ts index 2c769b32e..d22e8e3d1 100644 --- a/examples/src/telephony_amd.ts +++ b/examples/src/telephony_amd.ts @@ -175,7 +175,7 @@ export default defineAgent({ try { await rooms.deleteRoom(roomName); } catch (err) { - logger.warn({ error: err }, 'failed to delete room during hangup'); + logger.warn({ err }, 'failed to delete room during hangup'); } }); }, diff --git a/plugins/assemblyai/src/stt.ts b/plugins/assemblyai/src/stt.ts index b572a2495..f2f0db277 100644 --- a/plugins/assemblyai/src/stt.ts +++ b/plugins/assemblyai/src/stt.ts @@ -161,8 +161,7 @@ export class STT extends stt.STT { const supportsCarryover = isU3ProModel(rawModel) || rawModel === 'u3-pro'; if (opts.agentContextCarryover && !supportsCarryover) { log().warn( - { model: rawModel }, - 'agentContextCarryover is enabled but the model does not support it; ignoring', + `agentContextCarryover is enabled but model '${rawModel}' does not support it; ignoring`, ); } super({ @@ -358,23 +357,12 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - { - retryDelaySeconds, - retries, - maxRetry, - error: e, - }, - 'failed to connect to AssemblyAI, retrying', + `failed to connect to AssemblyAI, retrying in ${retryDelaySeconds} seconds: ${e} (${retries}/${maxRetry})`, ); await delay(retryDelaySeconds * 1000); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'AssemblyAI disconnected', + `AssemblyAI disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } @@ -462,13 +450,7 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'AssemblyAI WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); reject(new Error('WebSocket closed')); } }); @@ -598,8 +580,7 @@ export class SpeechStream extends stt.SpeechStream { this.#sessionId = data.id ?? null; this.#expiresAt = data.expires_at ?? null; this.#logger.info( - { sessionId: this.#sessionId, expiresAt: this.#expiresAt }, - 'AssemblyAI session started', + `AssemblyAI session started id=${this.#sessionId} expires_at=${this.#expiresAt}`, ); return; } @@ -611,11 +592,7 @@ export class SpeechStream extends stt.SpeechStream { if (messageType === 'Termination') { this.#logger.debug( - { - audioDurationSeconds: data.audio_duration_seconds, - sessionDurationSeconds: data.session_duration_seconds, - }, - 'AssemblyAI session terminated', + `AssemblyAI session terminated audio_duration=${data.audio_duration_seconds}s session_duration=${data.session_duration_seconds}s`, ); return; } diff --git a/plugins/azure/src/stt.ts b/plugins/azure/src/stt.ts index 33ad3ea7f..0e1e76ccb 100644 --- a/plugins/azure/src/stt.ts +++ b/plugins/azure/src/stt.ts @@ -401,9 +401,9 @@ export class SpeechStream extends stt.SpeechStream { { code: evt.errorCode, reason: evt.reason, - 'lk.pii.error_details': evt.errorDetails, + errorDetails: evt.errorDetails, }, - 'Speech recognition canceled', + `Speech recognition canceled: ${evt.errorDetails || evt.reason}`, ); this._cancellationError = evt; this._sessionStoppedEvent.set(); diff --git a/plugins/baseten/src/stt.ts b/plugins/baseten/src/stt.ts index eb27d6c9f..723163fbb 100644 --- a/plugins/baseten/src/stt.ts +++ b/plugins/baseten/src/stt.ts @@ -138,23 +138,12 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - { - delay, - retries, - maxRetry, - error: e, - }, - 'failed to connect to Baseten, retrying', + `failed to connect to Baseten, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'Baseten disconnected', + `Baseten disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } diff --git a/plugins/bey/src/avatar.ts b/plugins/bey/src/avatar.ts index 9817e99c1..ce23bb31e 100644 --- a/plugins/bey/src/avatar.ts +++ b/plugins/bey/src/avatar.ts @@ -238,7 +238,7 @@ export class AvatarSession extends voice.AvatarSession { return; } catch (e) { if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call bey presence api'); + this.#logger.warn({ error: String(e) }, 'failed to call bey presence api'); } else { this.#logger.error({ error: e }, 'failed to call bey presence api'); } diff --git a/plugins/cartesia/src/stt.ts b/plugins/cartesia/src/stt.ts index 96ab0e05d..ac769628e 100644 --- a/plugins/cartesia/src/stt.ts +++ b/plugins/cartesia/src/stt.ts @@ -355,7 +355,7 @@ export class SpeechStream extends stt.SpeechStream { await Promise.all([sendPromise, recvPromise]); if (firstError !== undefined) throw firstError; } catch (error) { - this.#logger.error({ error }, 'Cartesia STT stream error'); + this.#logger.error('Cartesia STT stream error', { error }); throw error; } finally { abortController.abort(); @@ -419,8 +419,7 @@ export class SpeechStream extends stt.SpeechStream { // the stream wait forever. const drainTimer = setTimeout(() => { this.#logger.warn( - { timeoutMs: DRAIN_TIMEOUT_MS }, - 'Cartesia STT did not close after done; forcing close', + `Cartesia STT did not close within ${DRAIN_TIMEOUT_MS}ms after done; forcing close`, ); if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) { ws.terminate(); @@ -448,7 +447,7 @@ export class SpeechStream extends stt.SpeechStream { try { msg = JSON.parse(data.toString()); } catch (error) { - this.#logger.error({ error }, 'Failed to parse Cartesia STT message'); + this.#logger.error('Failed to parse Cartesia STT message', { error }); return; } try { @@ -473,7 +472,7 @@ export class SpeechStream extends stt.SpeechStream { this.#processStreamEvent(msg); } } catch (error) { - this.#logger.error({ error }, 'Failed to process Cartesia STT message'); + this.#logger.error('Failed to process Cartesia STT message', { error }); } }); @@ -514,13 +513,7 @@ export class SpeechStream extends stt.SpeechStream { settle(); return; } - this.#logger.warn( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Cartesia STT WebSocket closed', - ); + this.#logger.warn(`Cartesia STT WebSocket closed: ${code} ${reason.toString()}`); settle( new APIConnectionError({ message: `Cartesia STT connection closed unexpectedly (code=${code})`, diff --git a/plugins/cartesia/src/tts.ts b/plugins/cartesia/src/tts.ts index 6c515178d..725d3e564 100644 --- a/plugins/cartesia/src/tts.ts +++ b/plugins/cartesia/src/tts.ts @@ -120,7 +120,8 @@ const checkGenerationConfig = (opts: TTSOptions) => { if (opts.speed || opts.emotion) { logger.warn( { model: opts.model, speed: opts.speed, emotion: opts.emotion }, - 'speed and emotion controls are only supported for sonic-2-2025-04-16 or sonic-3 models', + `speed and emotion controls are only supported for model '${MODEL_WITH_EXPERIMENTAL_CONTROLS}' ` + + `or sonic-3 models, see https://docs.cartesia.ai/developer-tools/changelog for details`, ); } } @@ -352,7 +353,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Cartesia TTS response error'); + this.#logger.error({ err }, 'Cartesia TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -360,7 +361,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ error: err }, 'Cartesia TTS request error'); + this.#logger.error({ err }, 'Cartesia TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.on('close', () => { @@ -506,13 +507,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { // socket lifecycle and does not close it between turns. If it happens // before `done`, surface it so the turn retries rather than ending mid // speech, and so withConnection discards the dead socket. - this.#logger.debug( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Cartesia TTS WebSocket closed', - ); + this.#logger.debug(`WebSocket closed with code ${code}: ${reason.toString()}`); clearTTSChunkTimeout(); if (!completed && !timedOut && !streamError) { streamError = new APIConnectionError({ @@ -523,7 +518,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { }; const onError = (err: Error) => { - this.#logger.error({ error: err }, 'Cartesia WebSocket error'); + this.#logger.error({ err }, 'Cartesia WebSocket error'); if (!completed && !timedOut && !streamError) { streamError = err instanceof APIError ? err : toRetryableConnectionError(err); } @@ -551,7 +546,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { const json = JSON.parse(rawMsg.toString()); serverMsg = cartesiaMessageSchema.parse(json); } catch (parseErr) { - this.#logger.warn({ error: parseErr }, 'Failed to parse Cartesia message'); + this.#logger.warn({ parseErr }, 'Failed to parse Cartesia message'); continue; } @@ -592,8 +587,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { timeout = setTimeout(() => { // cartesia chunk timeout quite often, so we make it a debug log this.#logger.debug( - { timeoutMs: this.#opts.chunkTimeout }, - 'Cartesia WebSocket TTS chunk stream timed out', + `Cartesia WebSocket TTS chunk stream timeout after ${this.#opts.chunkTimeout}ms`, ); // The socket is stuck mid-generation, so it must not return to the // pool. Poison it and unblock the reader; the post-loop check turns @@ -664,11 +658,11 @@ export class SynthesizeStream extends tts.SynthesizeStream { err.message.includes('Channel is closed') ) { this.#logger.warn( - { error: err }, + { err }, 'Channel closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ error: err }, 'Error in recvTask from Cartesia WebSocket'); + this.#logger.error({ err }, 'Error in recvTask from Cartesia WebSocket'); } } } finally { diff --git a/plugins/deepgram/src/stt.ts b/plugins/deepgram/src/stt.ts index a260fd4c9..558a64120 100644 --- a/plugins/deepgram/src/stt.ts +++ b/plugins/deepgram/src/stt.ts @@ -234,8 +234,7 @@ export class STT extends stt.STT { ].includes(model) ) { this.#logger.warn( - { model, language, fallbackModel: 'nova-2-general' }, - 'model does not support language, falling back', + `${model} does not support language ${language}, falling back to nova-2-general`, ); return 'nova-2-general'; } @@ -340,23 +339,12 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - { - delay, - retries, - maxRetry, - error: e, - }, - 'failed to connect to Deepgram, retrying', + `failed to connect to Deepgram, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'Deepgram disconnected', + `Deepgram disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } @@ -403,13 +391,7 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise(async (_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Deepgram STT WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); reject(new Error('WebSocket closed')); } }); diff --git a/plugins/deepgram/src/stt_v2.ts b/plugins/deepgram/src/stt_v2.ts index 7162be41f..0d8d752e4 100644 --- a/plugins/deepgram/src/stt_v2.ts +++ b/plugins/deepgram/src/stt_v2.ts @@ -357,7 +357,7 @@ class SpeechStreamv2 extends stt.SpeechStream { break; } } catch (error) { - this.#logger.error({ error }, 'Deepgram stream error'); + this.#logger.error('Deepgram stream error', { error }); throw error; // Let Base Class handle retry logic } finally { if (this.#ws?.readyState === WebSocket.OPEN) { @@ -451,18 +451,12 @@ class SpeechStreamv2 extends stt.SpeechStream { const msg = JSON.parse(data.toString()); this.#processStreamEvent(msg); } catch (error) { - this.#logger.error({ error }, 'Failed to parse Deepgram message'); + this.#logger.error('Failed to parse Deepgram message', { error }); } }); this.#ws.on('close', (code, reason) => { - this.#logger.debug( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Deepgram WebSocket closed', - ); + this.#logger.debug(`Deepgram WebSocket closed: ${code} ${reason}`); resolve(); }); diff --git a/plugins/deepgram/src/tts.ts b/plugins/deepgram/src/tts.ts index 10007a07d..6a7f11092 100644 --- a/plugins/deepgram/src/tts.ts +++ b/plugins/deepgram/src/tts.ts @@ -195,7 +195,7 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Deepgram TTS response error'); + this.#logger.error({ err }, 'Deepgram TTS response error'); settle(() => reject(err)); }); @@ -220,7 +220,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ error: err }, 'Deepgram TTS request error'); + this.#logger.error({ err }, 'Deepgram TTS request error'); settle(() => reject(err)); }); @@ -282,7 +282,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - this.#logger.warn({ error: e }, 'error during WebSocket close sequence'); + console.warn(`Error during WebSocket close sequence: ${e}`); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); @@ -417,10 +417,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { resolve(); return; } else if (message.type === 'Warning') { - this.#logger.warn( - { 'lk.pii.warning': message.warn_msg }, - 'Deepgram returned a warning', - ); + this.#logger.warn(`Deepgram warning: ${message.warn_msg}`); } else if (message.type === 'Error' || message.type === 'error') { reject(new APIError('Deepgram TTS returned error', { body: message })); return; diff --git a/plugins/did/src/api.ts b/plugins/did/src/api.ts index c23ea23ed..184b84f4f 100644 --- a/plugins/did/src/api.ts +++ b/plugins/did/src/api.ts @@ -128,7 +128,7 @@ export class DIDAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call d-id api'); + this.#logger.warn({ error: String(e) }, 'failed to call d-id api'); } else { this.#logger.error({ error: e }, 'failed to call d-id api'); } diff --git a/plugins/elevenlabs/src/stt.ts b/plugins/elevenlabs/src/stt.ts index e76b317ba..0796e3bf4 100644 --- a/plugins/elevenlabs/src/stt.ts +++ b/plugins/elevenlabs/src/stt.ts @@ -306,13 +306,8 @@ export class STT extends stt.STT { }); } else { this.#logger.warn( - { - stt: this.label, - attempt: i + 1, - retryIntervalMs: retryInterval, - error, - }, - 'failed to recognize speech, retrying', + { stt: this.label, attempt: i + 1, error }, + `failed to recognize speech, retrying in ${retryInterval}ms`, ); } @@ -844,7 +839,7 @@ export class SpeechStream extends stt.SpeechStream { } else if (messageType === 'committed_transcript') { return; } else if (messageType === 'session_started') { - this.#logger.debug({ sessionId: data.session_id }, 'session started'); + this.#logger.debug(`Session started with ID: ${data.session_id ?? 'unknown'}`); } else if ( messageType === 'auth_error' || messageType === 'quota_exceeded' || @@ -854,13 +849,7 @@ export class SpeechStream extends stt.SpeechStream { ) { const errorMsg = data.message ?? 'Unknown error'; const detailsSuffix = data.details ? ` - ${data.details}` : ''; - this.#logger.error( - { - messageType, - error: `${errorMsg}${detailsSuffix}`, - }, - 'ElevenLabs STT returned an error', - ); + this.#logger.error(`ElevenLabs STT error [${messageType}]: ${errorMsg}${detailsSuffix}`); throw new APIConnectionError({ message: `${messageType}: ${errorMsg}${detailsSuffix}` }); } else if ( messageType === 'committed_transcript_with_timestamps' && diff --git a/plugins/fishaudio/src/tts.ts b/plugins/fishaudio/src/tts.ts index 5230069de..8767f3bd3 100644 --- a/plugins/fishaudio/src/tts.ts +++ b/plugins/fishaudio/src/tts.ts @@ -461,7 +461,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Fish Audio TTS error response stream error'); + this.#logger.error({ err }, 'Fish Audio TTS error response stream error'); if (!doneFut.done) { doneFut.reject( new APIStatusError({ @@ -498,7 +498,7 @@ export class ChunkedStream extends tts.ChunkedStream { }); res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Fish Audio TTS response error'); + this.#logger.error({ err }, 'Fish Audio TTS response error'); if (!doneFut.done) doneFut.reject(err); }); }, @@ -506,7 +506,7 @@ export class ChunkedStream extends tts.ChunkedStream { req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ error: err }, 'Fish Audio TTS request error'); + this.#logger.error({ err }, 'Fish Audio TTS request error'); if (!doneFut.done) doneFut.reject(err); }); req.write(payload); @@ -606,7 +606,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { try { parsed = decode(frame) as Record; } catch (err) { - this.#logger.warn({ error: err }, 'Fish Audio failed to decode message'); + this.#logger.warn({ err }, 'Fish Audio failed to decode message'); return; } diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 3c0e31fef..6c24871eb 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -331,8 +331,8 @@ export class LLM extends llm.LLM { // Gemini 3: only supports thinkingLevel if (thinkingBudget !== undefined && thinkingLevel === undefined) { log().warn( - { model: this.#opts.model }, - 'Gemini 3 model does not support thinkingBudget; use thinkingLevel instead', + `Model ${this.#opts.model} is Gemini 3 which does not support thinkingBudget. ` + + `Please use thinkingLevel ('low' or 'high') instead. Ignoring thinkingBudget.`, ); } extras.thinkingConfig = { @@ -345,8 +345,9 @@ export class LLM extends llm.LLM { // Gemini 2.5 and earlier: only supports thinkingBudget if (thinkingLevel !== undefined && thinkingBudget === undefined) { log().warn( - { model: this.#opts.model }, - 'model does not support thinkingLevel; use thinkingBudget instead', + `Model ${this.#opts.model} does not support thinkingLevel. ` + + `Please use thinkingBudget (number) instead for Gemini 2.5 and earlier models. ` + + `Ignoring thinkingLevel.`, ); extras.thinkingConfig = { includeThoughts }; } else if (thinkingBudget !== undefined) { @@ -465,7 +466,7 @@ export class LLMStream extends llm.LLMStream { } if (dropped.length > 0) { this.logger.warn( - { dropped, 'lk.pii.cached_content': cachedContent }, + { dropped, cachedContent }, 'dropping systemInstruction from Gemini request because cachedContent is set; this field must be baked into the CachedContent resource', ); } diff --git a/plugins/google/src/realtime/realtime_api.ts b/plugins/google/src/realtime/realtime_api.ts index e5dab55de..bf594a769 100644 --- a/plugins/google/src/realtime/realtime_api.ts +++ b/plugins/google/src/realtime/realtime_api.ts @@ -358,8 +358,7 @@ export class RealtimeModel extends llm.RealtimeModel { if (!mutableSession) { this.#logger.warn( - { model }, - 'model has limited mid-session update support; updates apply to the next session', + `'${model}' has limited mid-session update support. instructions, chat context, and tool updates will not be applied until the next session.`, ); } @@ -815,8 +814,7 @@ export class RealtimeSession extends llm.RealtimeSession { ): Promise { if (!this.realtimeModel.capabilities.midSessionChatCtxUpdate) { this.#logger.warn( - { model: this.options.model }, - 'generateReply is not compatible with this model and will be ignored', + `generateReply is not compatible with '${this.options.model}' and will be ignored.`, ); throw new Error(`generateReply is not compatible with '${this.options.model}'`); } @@ -1012,14 +1010,7 @@ export class RealtimeSession extends llm.RealtimeSession { ? ' (message may be truncated - check model name and API permissions)' : ''; const errorMsg = event.reason || `WebSocket closed with code ${event.code}`; - this.#logger.error( - { - code: event.code, - truncated: isTruncated, - error: `${errorMsg}${truncationNote}`, - }, - 'Gemini Live session error', - ); + this.#logger.error(`Gemini Live session error: ${errorMsg}${truncationNote}`); this.emitError( new APIStatusError({ @@ -1035,13 +1026,7 @@ export class RealtimeSession extends llm.RealtimeSession { false, ); } else { - this.#logger.debug( - { - code: event.code, - 'lk.pii.reason': event.reason, - }, - 'Gemini Live session closed', - ); + this.#logger.debug('Gemini Live session closed:', event.code, event.reason); } this.markCurrentGenerationDone(); }, @@ -1089,7 +1074,7 @@ export class RealtimeSession extends llm.RealtimeSession { await cancelAndWait([sendTask, restartWaitTask], 2000); } catch (error) { - this.#logger.error({ error }, 'Gemini Realtime API error'); + this.#logger.error(`Gemini Realtime API error: ${error}`); if (this.#closed) break; @@ -1114,9 +1099,8 @@ export class RealtimeSession extends llm.RealtimeSession { { attempt: this.numRetries, maxRetries, - retryIntervalMs: retryInterval, }, - 'Gemini Realtime API connection failed, retrying', + `Gemini Realtime API connection failed, retrying in ${retryInterval}ms`, ); await delay(retryInterval); @@ -1200,13 +1184,13 @@ export class RealtimeSession extends llm.RealtimeSession { if (activityEnd) await session.sendRealtimeInput({ activityEnd }); break; default: - this.#logger.warn({ messageType: msg.type }, 'received unhandled message type'); + this.#logger.warn(`Warning: Received unhandled message type: ${msg.type}`); break; } } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error({ error: e }, 'Gemini Live send task failed'); + this.#logger.error(`Error in send task: ${e}`); this.markRestartNeeded(); } } finally { @@ -1286,10 +1270,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (this.isNewGeneration(response)) { this.startNewGeneration(); if (LK_GOOGLE_DEBUG) { - this.#logger.debug( - { responseId: this.currentGeneration?.responseId }, - 'new generation started', - ); + this.#logger.debug(`new generation started: ${this.currentGeneration?.responseId}`); } } } @@ -1328,7 +1309,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } catch (e) { if (!this.sessionShouldClose.isSet) { - this.#logger.error({ error: e }, 'failed to process Gemini Live server event'); + this.#logger.error(`Error in onReceiveMessage: ${e}`); this.markRestartNeeded(); } } @@ -1657,7 +1638,7 @@ export class RealtimeSession extends llm.RealtimeSession { gen.audioChannel.write(audioFrame); } catch (error) { - this.#logger.error({ error }, 'Error processing Gemini Live audio data'); + this.#logger.error('Error processing audio data:', error); } } } diff --git a/plugins/inworld/src/stt.ts b/plugins/inworld/src/stt.ts index 13ce37020..8e4166165 100644 --- a/plugins/inworld/src/stt.ts +++ b/plugins/inworld/src/stt.ts @@ -276,23 +276,12 @@ export class SpeechStream extends stt.SpeechStream { const delay = Math.min(retries * 5, 10); retries++; this.#logger.warn( - { - delay, - retries, - maxRetry, - error: e, - }, - 'Failed to connect to Inworld STT, retrying', + `Failed to connect to Inworld STT, retrying in ${delay}s: ${e} (${retries}/${maxRetry})`, ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'Inworld STT disconnected', + `Inworld STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } @@ -332,13 +321,7 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Inworld STT WebSocket closed unexpectedly', - ); + this.#logger.error(`Inworld STT WebSocket closed with code ${code}: ${reason}`); reject(new Error('WebSocket closed')); } }); diff --git a/plugins/inworld/src/tts.ts b/plugins/inworld/src/tts.ts index 050ff2191..db081bcd5 100644 --- a/plugins/inworld/src/tts.ts +++ b/plugins/inworld/src/tts.ts @@ -262,7 +262,7 @@ class WSConnectionPool { }), ); } else { - this.#logger.error({ error: err, requestId }, 'Inworld WebSocket error'); + this.#logger.error({ err, requestId }, 'Inworld WebSocket error'); } }); diff --git a/plugins/krisp/src/_krisp.ts b/plugins/krisp/src/_krisp.ts index 47d202240..683188dd7 100644 --- a/plugins/krisp/src/_krisp.ts +++ b/plugins/krisp/src/_krisp.ts @@ -91,7 +91,7 @@ class KrispLicenseSdkManager { log().debug('Krisp Audio SDK (license) initialized'); } this.referenceCount += 1; - log().debug({ referenceCount: this.referenceCount }, 'Krisp SDK reference acquired'); + log().debug(`Krisp SDK (license) reference count: ${this.referenceCount}`); // Non-null once refCount > 0: release() only nulls the module at refCount 0. return this.module!; } @@ -102,13 +102,13 @@ class KrispLicenseSdkManager { return; } this.referenceCount -= 1; - log().debug({ referenceCount: this.referenceCount }, 'Krisp SDK reference released'); + log().debug(`Krisp SDK (license) reference count: ${this.referenceCount}`); if (this.referenceCount === 0 && this.module !== null) { try { this.module.globalDestroy(); log().debug('Krisp Audio SDK (license) destroyed'); } catch (e) { - log().error({ error: e }, 'error during Krisp SDK cleanup'); + log().error(`Error during Krisp SDK cleanup: ${e}`); } finally { this.module = null; } @@ -212,14 +212,14 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { ); } - log().info({ sampleRate }, 'Creating Krisp session'); + log().info(`Creating Krisp session for sample rate: ${sampleRate}Hz`); // Free the previous session (if any) before replacing it on a rate change. if (this.session !== null) { try { this.session.destroy(); } catch (e) { - log().error({ error: e }, 'error destroying Krisp session'); + log().error(`Error destroying Krisp session: ${e}`); } this.session = null; } @@ -277,8 +277,8 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { if (frame.channels !== 1) { if (!this.warnedChannels) { log().warn( - { channels: frame.channels }, - 'Krisp filter requires mono audio; frames are passed through unprocessed', + `Krisp filter not applied: expected mono audio but got ${frame.channels} ` + + 'channels; frames are passed through unprocessed.', ); this.warnedChannels = true; } @@ -313,13 +313,12 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { const outputBuf = this.session!.process(inputBuf, this.level); chunkOut = bufferToInt16(outputBuf); } catch (e) { - log().error({ error: e }, 'error processing frame'); + log().error(`Error processing frame: ${e}`); chunkOut = chunkIn; } if (chunkOut.length !== chunk) { log().warn( - { returnedSamples: chunkOut.length, expectedSamples: chunk }, - 'Krisp returned an unexpected sample count; using original audio', + `Krisp returned ${chunkOut.length} samples, expected ${chunk}; using original audio`, ); chunkOut = chunkIn; } @@ -348,7 +347,7 @@ export class KrispLicenseFrameProcessor extends FrameProcessor { try { this.session.destroy(); } catch (e) { - log().error({ error: e }, 'error destroying Krisp session'); + log().error(`Error destroying Krisp session: ${e}`); } this.session = null; } diff --git a/plugins/lemonslice/src/avatar.ts b/plugins/lemonslice/src/avatar.ts index 4b2ff48ca..19d5157d4 100644 --- a/plugins/lemonslice/src/avatar.ts +++ b/plugins/lemonslice/src/avatar.ts @@ -552,7 +552,7 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call lemonslice api'); + this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api'); } else { this.#logger.error({ error: e }, 'failed to call lemonslice api'); } @@ -642,7 +642,7 @@ export class AvatarSession extends voice.AvatarSession { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call lemonslice api'); + this.#logger.warn({ error: String(e) }, 'failed to call lemonslice api'); } else { this.#logger.error({ error: e }, 'failed to call lemonslice api'); } diff --git a/plugins/liveavatar/src/api.ts b/plugins/liveavatar/src/api.ts index 8a0903f4f..54472df53 100644 --- a/plugins/liveavatar/src/api.ts +++ b/plugins/liveavatar/src/api.ts @@ -187,7 +187,7 @@ export class LiveAvatarAPI { throw e; } this.#logger.warn( - { error: e, 'lk.pii.url': url, attempt: i }, + { error: String(e), 'lk.pii.url': url, attempt: i }, 'LiveAvatar API request failed', ); } diff --git a/plugins/liveavatar/src/avatar.ts b/plugins/liveavatar/src/avatar.ts index 24453973a..ce0575d3b 100644 --- a/plugins/liveavatar/src/avatar.ts +++ b/plugins/liveavatar/src/avatar.ts @@ -244,7 +244,7 @@ export class AvatarSession extends voice.AvatarSession { // protocol failure does not surface as an unhandled rejection. The main task // itself handles its own cleanup in finally. this.mainTaskPromise = this.mainTask().catch((e) => { - this.#logger.warn({ error: e }, 'LiveAvatar main task failed'); + this.#logger.warn({ error: String(e) }, 'LiveAvatar main task failed'); }); } @@ -462,7 +462,7 @@ export class AvatarSession extends voice.AvatarSession { wsRef.send(JSON.stringify(value)); resetKeepAlive(); } catch (e) { - this.#logger.warn({ error: e }, 'failed to send LiveAvatar event'); + this.#logger.warn({ error: String(e) }, 'failed to send LiveAvatar event'); break; } } @@ -573,7 +573,7 @@ export class AvatarSession extends voice.AvatarSession { await Promise.race([forwardAudio(), sendTask(), recvTask(), keepAliveTask()]); } catch (e) { - this.#logger.warn({ error: e }, 'LiveAvatar main task error'); + this.#logger.warn({ error: String(e) }, 'LiveAvatar main task error'); } finally { this.closing = true; closingResolver.resolve(); @@ -587,7 +587,7 @@ export class AvatarSession extends voice.AvatarSession { } } } catch (e) { - this.#logger.warn({ error: e }, 'Failed to stop LiveAvatar session'); + this.#logger.warn({ error: String(e) }, 'Failed to stop LiveAvatar session'); } if (this.audioBuffer) { diff --git a/plugins/livekit/src/hf_utils.ts b/plugins/livekit/src/hf_utils.ts index c511696bd..14b815623 100644 --- a/plugins/livekit/src/hf_utils.ts +++ b/plugins/livekit/src/hf_utils.ts @@ -75,7 +75,10 @@ async function getBranchHeadCommit( logger.error({ repo: toRepoId(repo), revision }, 'No commits found for revision'); return null; } catch (error) { - logger.error({ error, repo: toRepoId(repo), revision }, 'Error getting HEAD commit'); + logger.error( + { error: (error as Error).message, repo: toRepoId(repo), revision }, + 'Error getting HEAD commit', + ); throw error; } } @@ -118,7 +121,10 @@ async function createSymlink(sourcePath: string, targetPath: string): Promise { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Neuphonic TTS response error'); + this.#logger.error({ err }, 'Neuphonic TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ error: err }, 'Neuphonic TTS request error'); + this.#logger.error({ err }, 'Neuphonic TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -263,13 +263,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error({ error }, 'Error parsing Neuphonic WebSocket message'); + this.#logger.error(`Error parsing WebSocket message: ${error}`); reject(error); } }); ws.on('error', (error) => { - this.#logger.error({ error }, 'Neuphonic WebSocket error'); + this.#logger.error(`WebSocket error: ${error}`); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -280,13 +280,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { ws.on('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Neuphonic WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); this.queue.put(SynthesizeStream.END_OF_STREAM); } // Only reject if we haven't processed all expected frames @@ -301,11 +295,11 @@ export class SynthesizeStream extends tts.SynthesizeStream { if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { if (err.message.includes('Queue is closed')) { this.#logger.warn( - { error: err }, + { err }, 'Queue closed during transcript processing (expected during disconnect)', ); } else { - this.#logger.error({ error: err }, 'Error in recvTask from Neuphonic WebSocket'); + this.#logger.error({ err }, 'Error in recvTask from Neuphonic WebSocket'); } } break; diff --git a/plugins/openai/src/realtime/realtime_model.ts b/plugins/openai/src/realtime/realtime_model.ts index 2d4a71415..b1f0ddbb1 100644 --- a/plugins/openai/src/realtime/realtime_model.ts +++ b/plugins/openai/src/realtime/realtime_model.ts @@ -589,10 +589,7 @@ export class RealtimeSession extends llm.RealtimeSession { ); if (blockingErrors.length > 0) { this.#logger.error( - { - 'lk.pii.issues': validation.issues, - 'lk.pii.blocking_errors': blockingErrors, - }, + { issues: validation.issues, blockingErrors }, 'Invalid chat context supplied to updateChatCtx', ); throw new Error( @@ -609,7 +606,7 @@ export class RealtimeSession extends llm.RealtimeSession { this.#logger.debug( { warnings: validation.warnings, - 'lk.pii.issues': validation.issues, + issues: validation.issues, }, 'Chat context warnings detected before realtime update', ); @@ -676,10 +673,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } } catch (e) { - this.#logger.error( - { error: (e as Error).message }, - 'failed to update OpenAI Realtime chat context', - ); + this.#logger.error((e as Error).message); throw e; } finally { unlock(); @@ -1033,7 +1027,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (!this.currentGeneration._doneFut.done) { this.currentGeneration._doneFut.resolve(); } - this.#logger.warn({ 'lk.pii.reason': reason }, 'in-progress generation discarded'); + this.#logger.warn(`In-progress generation discarded due to ${reason}`); this.currentGeneration = undefined; } @@ -1230,10 +1224,9 @@ export class RealtimeSession extends llm.RealtimeSession { { attempt: numRetries, maxRetries, - retryIntervalMs: retryInterval, error, }, - 'OpenAI Realtime API connection failed, retrying', + `OpenAI Realtime API connection failed, retrying in ${retryInterval / 1000}s`, ); await delay(retryInterval); @@ -1378,7 +1371,7 @@ export class RealtimeSession extends llm.RealtimeSession { break; default: if (lkOaiDebug) { - this.#logger.debug({ eventType: event.type }, 'unhandled event'); + this.#logger.debug(`unhandled event: ${event.type}`); } break; } @@ -1719,7 +1712,7 @@ export class RealtimeSession extends llm.RealtimeSession { const itemGeneration = this.currentGeneration.messages.get(itemId); if (!itemGeneration) { - this.#logger.warn({ itemId }, 'itemGeneration not found'); + this.#logger.warn(`itemGeneration not found for itemId=${itemId}`); return; } @@ -2035,9 +2028,9 @@ export class RealtimeSession extends llm.RealtimeSession { eventId: event.response.id, eventResponseStatus: event.response.status, eventResponseStatusType: statusType, - 'lk.pii.event_response_status_reason': statusReason, + eventResponseStatusReason: statusReason, }, - 'OpenAI Realtime API response finished without completing', + `OpenAI Realtime API response done but not complete with status: ${event.response.status} (type=${statusType}, reason=${statusReason})`, ); } else { this.#logger.debug({ eventResponseStatus: event.response.status }, 'Unknown response status'); diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index f7374e7e5..77982b880 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -362,14 +362,7 @@ export class RealtimeSession extends llm.RealtimeSession { const item = chatCtx.getById(itemId); if (item?.type === 'function_call_output' && this.pendingToolCallIds.has(item.callId)) { this.pendingToolCallIds.delete(item.callId); - this.#logger.info( - { - function: item.name, - callId: item.callId, - 'lk.pii.output': item.output, - }, - 'sending tool call output', - ); + this.#logger.info(`Sending tool call output for ${item.name} (call_id: ${item.callId})`); this.socket?.sendToolCallOutput({ type: 'tool_call_output', tool_call_id: item.callId, @@ -730,7 +723,7 @@ export class RealtimeSession extends llm.RealtimeSession { break; case 'conversation_created': this.conversationId = message.conversation_id; - this.#logger.info({ conversationId: this.conversationId }, 'Phonic conversation began'); + this.#logger.info(`Phonic Conversation began with ID: ${this.conversationId}`); this.options.onConversationCreated?.(this.conversationId); break; case 'tool_call_interrupted': @@ -838,11 +831,7 @@ export class RealtimeSession extends llm.RealtimeSession { private handleToolCallInterrupted(message: Phonic.ToolCallInterruptedPayload): void { this.pendingToolCallIds.delete(message.tool_call_id); this.#logger.warn( - { - toolName: message.tool_name, - callId: message.tool_call_id, - }, - 'tool call was cancelled due to user interruption', + `Tool call for ${message.tool_name} (call_id: ${message.tool_call_id}) was cancelled due to user interruption.`, ); } diff --git a/plugins/protoface/src/avatar.ts b/plugins/protoface/src/avatar.ts index b9693ec9d..017004aa8 100644 --- a/plugins/protoface/src/avatar.ts +++ b/plugins/protoface/src/avatar.ts @@ -132,7 +132,7 @@ export class AvatarSession extends voice.AvatarSession { this.sessionIdValue = session.id; this.#logger.debug( - { sessionId: this.sessionIdValue, avatarId: this.avatarId }, + { sessionId: this.sessionIdValue, 'lk.pii.avatar_id': this.avatarId }, 'protoface session started', ); @@ -152,7 +152,7 @@ export class AvatarSession extends voice.AvatarSession { try { await this.api.endSession(sessionId); } catch (error) { - this.#logger.warn({ error, sessionId }, 'failed to end protoface session'); + this.#logger.warn({ error: String(error), sessionId }, 'failed to end protoface session'); } } } finally { diff --git a/plugins/resemble/src/tts.ts b/plugins/resemble/src/tts.ts index b377c4507..b393d7cd2 100644 --- a/plugins/resemble/src/tts.ts +++ b/plugins/resemble/src/tts.ts @@ -161,7 +161,7 @@ export class ChunkedStream extends tts.ChunkedStream { this.queue.close(); doneFut.resolve(); } catch (error) { - this.#logger.error({ error }, 'Error processing Resemble API response'); + this.#logger.error('Error processing Resemble API response:', error); this.queue.close(); doneFut.resolve(); } @@ -174,14 +174,14 @@ export class ChunkedStream extends tts.ChunkedStream { res.on('error', (err) => { if (err.message === 'aborted') return; - this.#logger.error({ error: err }, 'Resemble TTS response error'); + this.#logger.error({ err }, 'Resemble TTS response error'); }); }, ); req.on('error', (err) => { if (err.name === 'AbortError') return; - this.#logger.error({ error: err }, 'Resemble TTS request error'); + this.#logger.error({ err }, 'Resemble TTS request error'); }); req.on('close', () => doneFut.resolve()); req.write(JSON.stringify(json)); @@ -268,10 +268,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { lastFrame = frame; } } catch (audioError) { - this.#logger.error( - { error: audioError }, - 'Error processing Resemble audio content', - ); + this.#logger.error(`Error processing audio content: ${audioError}`); } } else if ('type' in json && json.type === 'audio_end') { for (const frame of bstream.flush()) { @@ -294,11 +291,8 @@ export class SynthesizeStream extends tts.SynthesizeStream { const errorName = json.error_name || 'Unknown'; const explanation = json.error_params?.explanation || 'No details provided'; this.#logger - .child({ - error: errorName, - 'lk.pii.explanation': explanation, - }) - .error('Resemble API returned an error'); + .child({ error: errorName }) + .error(`Resemble API error: ${explanation}`); closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -308,13 +302,13 @@ export class SynthesizeStream extends tts.SynthesizeStream { } resolve(); } catch (error) { - this.#logger.error({ error }, 'Error parsing Resemble WebSocket message'); + this.#logger.error(`Error parsing WebSocket message: ${error}`); reject(error); } }); ws.on('error', (error) => { - this.#logger.error({ error }, 'Resemble WebSocket error'); + this.#logger.error(`WebSocket error: ${error}`); if (!closing) { closing = true; this.queue.put(SynthesizeStream.END_OF_STREAM); @@ -325,13 +319,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { ws.on('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Resemble WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); this.queue.put(SynthesizeStream.END_OF_STREAM); } // Only reject if we haven't received all expected frames @@ -345,7 +333,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } catch (err) { // Skip log error for normal websocket close if (err instanceof Error && !err.message.includes('WebSocket closed prematurely')) { - this.#logger.error({ error: err }, 'Error in recvTask from Resemble WebSocket'); + this.#logger.error({ err }, 'Error in recvTask from Resemble WebSocket'); } break; } diff --git a/plugins/runway/src/avatar.ts b/plugins/runway/src/avatar.ts index cd56d7a6c..f41387952 100644 --- a/plugins/runway/src/avatar.ts +++ b/plugins/runway/src/avatar.ts @@ -220,7 +220,7 @@ export class AvatarSession extends voice.AvatarSession { if (e instanceof APIStatusError && !e.retryable) throw e; if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call Runway API'); + this.#logger.warn({ error: String(e) }, 'failed to call Runway API'); } else { this.#logger.error({ error: e }, 'failed to call Runway API'); } @@ -268,7 +268,10 @@ export class AvatarSession extends voice.AvatarSession { this.#logger.debug('sent Runway realtime session end call'); return; } catch (error) { - this.#logger.warn({ error }, 'error ending Runway realtime session via data channel'); + this.#logger.warn( + { error: String(error) }, + 'error ending Runway realtime session via data channel', + ); } } @@ -309,7 +312,10 @@ export class AvatarSession extends voice.AvatarSession { ); } } catch (error) { - this.#logger.warn({ sessionId, error }, 'error cancelling Runway realtime session'); + this.#logger.warn( + { sessionId, error: String(error) }, + 'error cancelling Runway realtime session', + ); } } diff --git a/plugins/sarvam/src/stt.ts b/plugins/sarvam/src/stt.ts index dc66cef2c..27defbdb4 100644 --- a/plugins/sarvam/src/stt.ts +++ b/plugins/sarvam/src/stt.ts @@ -629,23 +629,12 @@ export class SpeechStream extends stt.SpeechStream { const delay = Math.min(retries * 5, 10); retries++; this.#logger.warn( - { - delay, - retries, - maxRetry, - error: e, - }, - 'Failed to connect to Sarvam STT, retrying', + `Failed to connect to Sarvam STT, retrying in ${delay}s: ${e} (${retries}/${maxRetry})`, ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'Sarvam STT disconnected', + `Sarvam STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } @@ -676,13 +665,7 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise((_, reject) => { ws.once('close', (code: number, reason: Buffer) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'Sarvam STT WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); reject(new Error('WebSocket closed')); } }); @@ -815,11 +798,7 @@ export class SpeechStream extends stt.SpeechStream { // Log metrics when available if (td.metrics) { this.#logger.debug( - { - audioDurationSeconds: td.metrics.audio_duration, - latencySeconds: td.metrics.processing_latency, - }, - 'Sarvam STT metrics', + `Sarvam STT metrics: audio_duration=${td.metrics.audio_duration}s, latency=${td.metrics.processing_latency}s`, ); } @@ -854,13 +833,7 @@ export class SpeechStream extends stt.SpeechStream { json['message'] ?? 'Unknown error'; const errorCode = nested?.code ?? json['code'] ?? ''; - this.#logger.error( - { - errorCode, - error: errorInfo, - }, - 'Sarvam STT WebSocket returned an error', - ); + this.#logger.error(`Sarvam STT WebSocket error [${errorCode}]: ${errorInfo}`); reject(new Error(`Sarvam STT API error [${errorCode}]: ${errorInfo}`)); return; } diff --git a/plugins/sarvam/src/tts.ts b/plugins/sarvam/src/tts.ts index e086399d0..13ff43b59 100644 --- a/plugins/sarvam/src/tts.ts +++ b/plugins/sarvam/src/tts.ts @@ -495,7 +495,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } } } catch (e) { - this.#logger.warn({ error: e }, 'Error during Sarvam WebSocket close sequence'); + this.#logger.warn(`Error during WebSocket close sequence: ${e}`); } finally { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); diff --git a/plugins/soniox/src/stt.ts b/plugins/soniox/src/stt.ts index 010254da5..186ad8617 100644 --- a/plugins/soniox/src/stt.ts +++ b/plugins/soniox/src/stt.ts @@ -249,13 +249,7 @@ export class SpeechStream extends stt.SpeechStream { if (content.error_code || content.error_message) { const statusCode = parseStatusCode(content.error_code); const errorMessage = content.error_message ?? 'Unknown Soniox STT error'; - this.#logger.error( - { - errorCode: content.error_code ?? '', - error: errorMessage, - }, - 'Soniox STT WebSocket returned an error', - ); + this.#logger.error(`WebSocket error: ${content.error_code ?? ''} - ${errorMessage}`); reject( new APIStatusError({ message: `Soniox STT error: ${content.error_code ?? ''} - ${errorMessage}`, diff --git a/plugins/soniox/src/tts.ts b/plugins/soniox/src/tts.ts index 3947b188d..7f963c2b2 100644 --- a/plugins/soniox/src/tts.ts +++ b/plugins/soniox/src/tts.ts @@ -656,7 +656,7 @@ class Connection { this.#logger.error( { errorCode: response.error_code, - 'lk.pii.error_message': response.error_message, + errorMessage: response.error_message, 'lk.pii.response': response, }, 'Soniox TTS connection-level error', @@ -667,7 +667,7 @@ class Connection { const stream = this.#streams.get(streamId); if (stream === undefined) { - this.#logger.debug({ streamId }, 'Ignoring message for unknown Soniox TTS stream'); + this.#logger.debug(`Ignoring message for unknown Soniox TTS stream ${streamId}`); return; } diff --git a/plugins/tavus/src/api.test.ts b/plugins/tavus/src/api.test.ts index c00240530..16a0b4b94 100644 --- a/plugins/tavus/src/api.test.ts +++ b/plugins/tavus/src/api.test.ts @@ -49,14 +49,9 @@ describe('Tavus TavusAPI.createConversation', () => { const body = sentBody(f); expect(body.face_id).toBe('r1'); expect(body.pal_id).toBe('x1'); - expect(warnMock).toHaveBeenCalledWith( - { deprecatedName: 'replicaId', replacementName: 'faceId' }, - 'deprecated option used', - ); - expect(warnMock).toHaveBeenCalledWith( - { deprecatedName: 'personaId', replacementName: 'palId' }, - 'deprecated option used', - ); + const msgs = warnMock.mock.calls.map((c) => String(c[0])); + expect(msgs.some((m) => m.includes('replicaId') && m.includes('faceId'))).toBe(true); + expect(msgs.some((m) => m.includes('personaId') && m.includes('palId'))).toBe(true); }); it('does not warn when both the new and deprecated options are supplied', async () => { diff --git a/plugins/tavus/src/api.ts b/plugins/tavus/src/api.ts index ef3755930..9a2b2a358 100644 --- a/plugins/tavus/src/api.ts +++ b/plugins/tavus/src/api.ts @@ -53,7 +53,7 @@ function coalesceWithDeprecated( ): string | undefined { // Prefer the new option; fall back to the deprecated alias and warn only when it's used. if (!newValue && deprecatedValue) { - log().warn({ deprecatedName, replacementName: newName }, 'deprecated option used'); + log().warn(`\`${deprecatedName}\` is deprecated, use \`${newName}\` instead`); } return newValue || deprecatedValue; } @@ -62,10 +62,7 @@ function deprecatedEnv(deprecatedName: string, newName: string): string | undefi // Read a deprecated env var, warning if it's set so callers migrate to `newName`. const value = process.env[deprecatedName]; if (value) { - log().warn( - { deprecatedName, replacementName: newName }, - 'deprecated environment variable used', - ); + log().warn(`\`${deprecatedName}\` is deprecated, use \`${newName}\` instead`); } return value; } @@ -224,7 +221,7 @@ export class TavusAPI { throw e; } if (e instanceof APIConnectionError) { - this.#logger.warn({ error: e }, 'failed to call tavus api'); + this.#logger.warn({ error: String(e) }, 'failed to call tavus api'); } else { this.#logger.error({ error: e }, 'failed to call tavus api'); } diff --git a/plugins/trugen/src/avatar.ts b/plugins/trugen/src/avatar.ts index 7c71a682c..4b719523b 100644 --- a/plugins/trugen/src/avatar.ts +++ b/plugins/trugen/src/avatar.ts @@ -209,7 +209,7 @@ export class AvatarSession extends voice.AvatarSession { throw e; } } else { - this.#logger.warn({ error: e }, 'failed to call trugen api'); + this.#logger.warn({ error: String(e) }, 'failed to call trugen api'); } if (i < this.connOptions.maxRetry) { await new Promise((resolve) => diff --git a/plugins/xai/src/stt.ts b/plugins/xai/src/stt.ts index b0a15f58e..2d5ce9a4e 100644 --- a/plugins/xai/src/stt.ts +++ b/plugins/xai/src/stt.ts @@ -218,23 +218,12 @@ export class SpeechStream extends stt.SpeechStream { retries++; this.#logger.warn( - { - delay, - retries, - maxRetry, - error: e, - }, - 'failed to connect to xAI STT, retrying', + `failed to connect to xAI STT, retrying in ${delay} seconds: ${e} (${retries}/${maxRetry})`, ); await new Promise((resolve) => setTimeout(resolve, delay * 1000)); } else { this.#logger.warn( - { - inputClosed: this.input.closed, - isClosed: this.closed, - error: e, - }, - 'xAI STT disconnected', + `xAI STT disconnected, connection is closed: ${e} (inputClosed: ${this.input.closed}, isClosed: ${this.closed})`, ); } } @@ -252,13 +241,7 @@ export class SpeechStream extends stt.SpeechStream { const closed = new Promise(async (_, reject) => { ws.once('close', (code, reason) => { if (!closing) { - this.#logger.error( - { - code, - 'lk.pii.reason': reason.toString(), - }, - 'xAI STT WebSocket closed unexpectedly', - ); + this.#logger.error(`WebSocket closed with code ${code}: ${reason}`); reject(new Error('WebSocket closed')); } }); @@ -435,12 +418,9 @@ export class SpeechStream extends stt.SpeechStream { this.#putMessage({ type: stt.SpeechEventType.END_OF_SPEECH }); } } else if (msgType === 'error') { - this.#logger.error( - { error: (data['message'] as string) ?? 'unknown error' }, - 'xAI STT returned an error', - ); + this.#logger.error(`xAI STT error: ${(data['message'] as string) ?? 'unknown error'}`); } else { - this.#logger.warn({ messageType: msgType }, 'received unexpected message from xAI'); + this.#logger.warn(`received unexpected message from xAI: ${msgType}`); } } } diff --git a/plugins/xai/src/tts.ts b/plugins/xai/src/tts.ts index a4f84e5e2..1617f1dbd 100644 --- a/plugins/xai/src/tts.ts +++ b/plugins/xai/src/tts.ts @@ -419,7 +419,7 @@ export class SynthesizeStream extends tts.SynthesizeStream { } catch (e) { this.#logger.warn( { - error: e, + err: e, 'lk.pii.data': event.data.toString(), }, 'Unexpected xAI message', From e9f494d07a7cbfef718aa8b9a9e2fcfc68e4c014 Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Mon, 27 Jul 2026 13:53:43 +0100 Subject: [PATCH 5/6] fix(telemetry): keep framework URLs non-PII --- .../inference/interruption/interruption_detector.ts | 2 +- agents/src/inference/tts.ts | 5 +---- agents/src/log.test.ts | 10 +++++++++- agents/src/worker.ts | 2 +- agents/src/workflows/warm_transfer.ts | 5 +---- plugins/anam/src/api.ts | 8 ++++---- plugins/anam/src/avatar.ts | 6 +++--- plugins/cartesia/src/stt.ts | 2 +- plugins/liveavatar/src/api.ts | 5 +---- plugins/livekit/src/turn_detector/multilingual.ts | 2 +- plugins/openai/src/realtime/realtime_model.ts | 2 +- plugins/sarvam/src/stt.ts | 2 +- 12 files changed, 25 insertions(+), 26 deletions(-) diff --git a/agents/src/inference/interruption/interruption_detector.ts b/agents/src/inference/interruption/interruption_detector.ts index 4013397d0..0f4c341a9 100644 --- a/agents/src/inference/interruption/interruption_detector.ts +++ b/agents/src/inference/interruption/interruption_detector.ts @@ -82,7 +82,7 @@ export class AdaptiveInterruptionDetector extends (EventEmitter as new () => Typ this.logger.debug( { - 'lk.pii.base_url': this.options.baseUrl, + baseUrl: this.options.baseUrl, detectionIntervalInS: this.options.detectionIntervalInS, audioPrefixDurationInS: this.options.audioPrefixDurationInS, maxAudioDurationInS: this.options.maxAudioDurationInS, diff --git a/agents/src/inference/tts.ts b/agents/src/inference/tts.ts index c9f21ceee..f1cd4e301 100644 --- a/agents/src/inference/tts.ts +++ b/agents/src/inference/tts.ts @@ -521,10 +521,7 @@ export class TTS extends BaseTTS { }; } - this.#logger.debug( - { 'lk.pii.url': url }, - 'inference.TTS creating new websocket connection (pool miss)', - ); + this.#logger.debug({ url }, 'inference.TTS creating new websocket connection (pool miss)'); const socket = await connectWs(url, headers, timeout); socket.send(JSON.stringify(params)); return socket; diff --git a/agents/src/log.test.ts b/agents/src/log.test.ts index da5f76bdc..01a758012 100644 --- a/agents/src/log.test.ts +++ b/agents/src/log.test.ts @@ -67,7 +67,7 @@ describe('OTEL logging', () => { }); }); - it('keeps operational errors and reasons in non-PII fields', async () => { + it('keeps operational errors, reasons, and framework URLs in non-PII fields', async () => { initializeLogger({ pretty: false, level: 'info' }); const emitSpy = vi.spyOn(PinoCloudExporter.prototype, 'emit').mockImplementation(() => {}); @@ -80,6 +80,7 @@ describe('OTEL logging', () => { log().error({ error: new Error('connection failed') }, 'provider failed'); log().warn({ reason: 'remote close' }, 'provider disconnected'); + log().info({ baseUrl: 'wss://example.livekit.cloud' }, 'connecting to framework'); await vi.waitFor(() => { const records = emitSpy.mock.calls.map(([logObj]) => logObj); @@ -99,6 +100,13 @@ describe('OTEL logging', () => { reason: 'remote close', }); expect(reasonRecord).not.toHaveProperty('lk.pii.reason'); + + const connectionRecord = records.find((logObj) => logObj.msg === 'connecting to framework'); + expect(connectionRecord).toMatchObject({ + msg: 'connecting to framework', + baseUrl: 'wss://example.livekit.cloud', + }); + expect(connectionRecord).not.toHaveProperty('lk.pii.base_url'); }); }); }); diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 4a9332585..ce2cafe15 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -485,7 +485,7 @@ export class AgentServer { this.#logger.warn( { - 'lk.pii.url': this.#opts.wsURL, + url: this.#opts.wsURL, error: e, retry_count: retries, max_retry: this.#opts.maxRetry, diff --git a/agents/src/workflows/warm_transfer.ts b/agents/src/workflows/warm_transfer.ts index 573691eb5..fbf870dfa 100644 --- a/agents/src/workflows/warm_transfer.ts +++ b/agents/src/workflows/warm_transfer.ts @@ -415,10 +415,7 @@ export function createWarmTransferTask({ canSubscribe: true, } as VideoGrant); - logger.debug( - { 'lk.pii.ws_url': ctx.info.url, humanAgentRoomName }, - 'connecting to human agent room', - ); + logger.debug({ wsUrl: ctx.info.url, humanAgentRoomName }, 'connecting to human agent room'); const jwt = await token.toJwt(); room.on(RoomEvent.Disconnected, onHumanAgentRoomClose); diff --git a/plugins/anam/src/api.ts b/plugins/anam/src/api.ts index e496aa1a6..8663be15b 100644 --- a/plugins/anam/src/api.ts +++ b/plugins/anam/src/api.ts @@ -64,7 +64,7 @@ export class AnamAPI { logger.debug( { - 'lk.pii.url': url, + url, method: 'POST', headers: redactedHeaders, 'lk.pii.body': redactedBody, @@ -83,7 +83,7 @@ export class AnamAPI { const text = await res.text(); logger.error( { - 'lk.pii.url': url, + url, method: 'POST', headers: redactedHeaders, 'lk.pii.body': redactedBody, @@ -95,14 +95,14 @@ export class AnamAPI { throw new AnamException(`Anam ${path} failed: ${res.status} ${text}`); } const json = (await res.json()) as T; - logger.debug({ 'lk.pii.url': url }, 'Anam API request succeeded'); + logger.debug({ url }, 'Anam API request succeeded'); return json; } catch (e) { lastErr = e; if (attempt === maxRetry - 1) break; logger.warn( { - 'lk.pii.url': url, + url, method: 'POST', 'lk.pii.body': body && typeof body === 'object' diff --git a/plugins/anam/src/avatar.ts b/plugins/anam/src/avatar.ts index cd63c461d..faa881b73 100644 --- a/plugins/anam/src/avatar.ts +++ b/plugins/anam/src/avatar.ts @@ -100,8 +100,8 @@ export class AvatarSession extends voice.AvatarSession { 'lk.pii.persona_name': this.opts.personaConfig?.name, 'lk.pii.avatar_id': this.opts.personaConfig?.avatarId, 'lk.pii.persona_id': this.opts.personaConfig?.personaId, - 'lk.pii.api_url': apiUrl ?? '(default https://api.anam.ai)', - 'lk.pii.livekit_url': livekitUrl, + apiUrl: apiUrl ?? '(default https://api.anam.ai)', + livekitUrl, avatarParticipantIdentity: this.opts.avatarParticipantIdentity ?? 'anam-avatar-agent', publishOnBehalf: localIdentity, }, @@ -117,7 +117,7 @@ export class AvatarSession extends voice.AvatarSession { }); const anam = new AnamAPI(apiKey, apiUrl, this.opts.connOptions); - logger.debug({ 'lk.pii.livekit_url': livekitUrl }, 'requesting Anam session token'); + logger.debug({ livekitUrl }, 'requesting Anam session token'); const { sessionToken } = await anam.createSessionToken({ personaConfig: this.opts.personaConfig, diff --git a/plugins/cartesia/src/stt.ts b/plugins/cartesia/src/stt.ts index ac769628e..7753ae8ce 100644 --- a/plugins/cartesia/src/stt.ts +++ b/plugins/cartesia/src/stt.ts @@ -293,7 +293,7 @@ export class SpeechStream extends stt.SpeechStream { try { const url = this.#getCartesiaUrl(); - this.#logger.debug({ 'lk.pii.url': url }, 'connecting to Cartesia STT'); + this.#logger.debug({ url }, 'connecting to Cartesia STT'); const ws = new WebSocket(url, { headers: { diff --git a/plugins/liveavatar/src/api.ts b/plugins/liveavatar/src/api.ts index 54472df53..cfb9148ba 100644 --- a/plugins/liveavatar/src/api.ts +++ b/plugins/liveavatar/src/api.ts @@ -186,10 +186,7 @@ export class LiveAvatarAPI { if (e instanceof APIStatusError && !e.retryable) { throw e; } - this.#logger.warn( - { error: String(e), 'lk.pii.url': url, attempt: i }, - 'LiveAvatar API request failed', - ); + this.#logger.warn({ error: String(e), url, attempt: i }, 'LiveAvatar API request failed'); } if (i < maxRetry) { diff --git a/plugins/livekit/src/turn_detector/multilingual.ts b/plugins/livekit/src/turn_detector/multilingual.ts index 94e20fe7e..18fff0d85 100644 --- a/plugins/livekit/src/turn_detector/multilingual.ts +++ b/plugins/livekit/src/turn_detector/multilingual.ts @@ -104,7 +104,7 @@ export class MultilingualModel extends EOUModel { const startedAt = performance.now(); - this.#logger.debug({ 'lk.pii.url': url, 'lk.pii.request': request }, 'remote EOU inference'); + this.#logger.debug({ url, 'lk.pii.request': request }, 'remote EOU inference'); const resp = await fetch(url, { method: 'POST', diff --git a/plugins/openai/src/realtime/realtime_model.ts b/plugins/openai/src/realtime/realtime_model.ts index b1f0ddbb1..c69bfbfd8 100644 --- a/plugins/openai/src/realtime/realtime_model.ts +++ b/plugins/openai/src/realtime/realtime_model.ts @@ -1075,7 +1075,7 @@ export class RealtimeSession extends llm.RealtimeSession { }); if (lkOaiDebug) { - this.#logger.debug({ 'lk.pii.url': url }, 'connecting to OpenAI Realtime API'); + this.#logger.debug({ url }, 'connecting to OpenAI Realtime API'); } return new Promise((resolve, reject) => { diff --git a/plugins/sarvam/src/stt.ts b/plugins/sarvam/src/stt.ts index 27defbdb4..518c0180e 100644 --- a/plugins/sarvam/src/stt.ts +++ b/plugins/sarvam/src/stt.ts @@ -593,7 +593,7 @@ export class SpeechStream extends stt.SpeechStream { while (!this.input.closed && !this.closed) { const wsUrl = buildWsUrl(this.#opts); - this.#logger.info({ 'lk.pii.url': wsUrl }, 'Sarvam STT connecting'); + this.#logger.info({ url: wsUrl }, 'Sarvam STT connecting'); const ws = new WebSocket(wsUrl, { headers: { 'api-subscription-key': this.#opts.apiKey }, }); From c851b2baae65150bed1b74cfc730f182814249d9 Mon Sep 17 00:00:00 2001 From: Chenghao Mou Date: Mon, 27 Jul 2026 14:16:51 +0100 Subject: [PATCH 6/6] fix(telemetry): limit PII tags to content-bearing data --- agents/src/inference/stt.ts | 2 +- agents/src/inference/tts.ts | 2 +- agents/src/log.test.ts | 10 +++++++++- agents/src/worker.ts | 10 ++-------- plugins/anam/src/api.ts | 8 ++++---- plugins/anam/src/avatar.ts | 6 +++--- plugins/cartesia/src/stt.ts | 2 +- plugins/google/src/llm.ts | 5 +---- plugins/liveavatar/src/api.ts | 5 ++++- plugins/openai/src/realtime/realtime_model.ts | 2 +- plugins/protoface/src/avatar.ts | 2 +- plugins/runway/src/avatar.ts | 8 ++------ plugins/sarvam/src/stt.ts | 8 ++------ plugins/soniox/src/tts.ts | 8 ++------ plugins/trugen/src/avatar.ts | 2 +- 15 files changed, 35 insertions(+), 45 deletions(-) diff --git a/agents/src/inference/stt.ts b/agents/src/inference/stt.ts index 6cdd11e27..ab826c071 100644 --- a/agents/src/inference/stt.ts +++ b/agents/src/inference/stt.ts @@ -922,7 +922,7 @@ export class SpeechStream extends BaseSpeechStream { this.processTranscript(event, SpeechEventType.PREFLIGHT_TRANSCRIPT); break; case 'error': - this.#logger.error({ 'lk.pii.event': event }, 'Received error from LiveKit STT'); + this.#logger.error({ error: event }, 'Received error from LiveKit STT'); resourceCleanup(); throw new APIError(`LiveKit STT returned error: ${JSON.stringify(event)}`); } diff --git a/agents/src/inference/tts.ts b/agents/src/inference/tts.ts index f1cd4e301..166edb62d 100644 --- a/agents/src/inference/tts.ts +++ b/agents/src/inference/tts.ts @@ -839,7 +839,7 @@ export class SynthesizeStream extends BaseSynthesizeSt return; case 'error': this.#logger.error( - { 'lk.pii.server_event': serverEvent }, + { serverEvent }, 'Received error message from LiveKit TTS WebSocket', ); await resourceCleanup(); diff --git a/agents/src/log.test.ts b/agents/src/log.test.ts index 01a758012..d2f27cd48 100644 --- a/agents/src/log.test.ts +++ b/agents/src/log.test.ts @@ -67,7 +67,7 @@ describe('OTEL logging', () => { }); }); - it('keeps operational errors, reasons, and framework URLs in non-PII fields', async () => { + it('keeps operational errors, reasons, framework URLs, and resource IDs in non-PII fields', async () => { initializeLogger({ pretty: false, level: 'info' }); const emitSpy = vi.spyOn(PinoCloudExporter.prototype, 'emit').mockImplementation(() => {}); @@ -81,6 +81,7 @@ describe('OTEL logging', () => { log().error({ error: new Error('connection failed') }, 'provider failed'); log().warn({ reason: 'remote close' }, 'provider disconnected'); log().info({ baseUrl: 'wss://example.livekit.cloud' }, 'connecting to framework'); + log().info({ avatarId: 'avatar-123' }, 'avatar session started'); await vi.waitFor(() => { const records = emitSpy.mock.calls.map(([logObj]) => logObj); @@ -107,6 +108,13 @@ describe('OTEL logging', () => { baseUrl: 'wss://example.livekit.cloud', }); expect(connectionRecord).not.toHaveProperty('lk.pii.base_url'); + + const avatarRecord = records.find((logObj) => logObj.msg === 'avatar session started'); + expect(avatarRecord).toMatchObject({ + msg: 'avatar session started', + avatarId: 'avatar-123', + }); + expect(avatarRecord).not.toHaveProperty('lk.pii.avatar_id'); }); }); }); diff --git a/agents/src/worker.ts b/agents/src/worker.ts index ce2cafe15..9a23c2f42 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -484,14 +484,8 @@ export class AgentServer { const delay = Math.min(retries * 2, 10); this.#logger.warn( - { - url: this.#opts.wsURL, - error: e, - retry_count: retries, - max_retry: this.#opts.maxRetry, - retryDelaySeconds: delay, - }, - 'failed to connect to LiveKit server, retrying', + { error: e, retry_count: retries, max_retry: this.#opts.maxRetry }, + `failed to connect to LiveKit server (${this.#opts.wsURL}), retrying in ${delay} seconds: (${retries}/${this.#opts.maxRetry})`, ); await new ThrowsPromise((resolve) => setTimeout(resolve, delay * 1000)); diff --git a/plugins/anam/src/api.ts b/plugins/anam/src/api.ts index 8663be15b..9f9601e8c 100644 --- a/plugins/anam/src/api.ts +++ b/plugins/anam/src/api.ts @@ -67,7 +67,7 @@ export class AnamAPI { url, method: 'POST', headers: redactedHeaders, - 'lk.pii.body': redactedBody, + body: redactedBody, attempt: attempt + 1, }, 'calling Anam API', @@ -86,9 +86,9 @@ export class AnamAPI { url, method: 'POST', headers: redactedHeaders, - 'lk.pii.body': redactedBody, + body: redactedBody, status: res.status, - 'lk.pii.response': text, + response: text, }, 'Anam API request failed', ); @@ -104,7 +104,7 @@ export class AnamAPI { { url, method: 'POST', - 'lk.pii.body': + body: body && typeof body === 'object' ? { ...(body as Record), livekitToken: '****' } : body, diff --git a/plugins/anam/src/avatar.ts b/plugins/anam/src/avatar.ts index faa881b73..90e038d33 100644 --- a/plugins/anam/src/avatar.ts +++ b/plugins/anam/src/avatar.ts @@ -97,9 +97,9 @@ export class AvatarSession extends voice.AvatarSession { logger.debug( { - 'lk.pii.persona_name': this.opts.personaConfig?.name, - 'lk.pii.avatar_id': this.opts.personaConfig?.avatarId, - 'lk.pii.persona_id': this.opts.personaConfig?.personaId, + personaName: this.opts.personaConfig?.name, + avatarId: this.opts.personaConfig?.avatarId, + personaId: this.opts.personaConfig?.personaId, apiUrl: apiUrl ?? '(default https://api.anam.ai)', livekitUrl, avatarParticipantIdentity: this.opts.avatarParticipantIdentity ?? 'anam-avatar-agent', diff --git a/plugins/cartesia/src/stt.ts b/plugins/cartesia/src/stt.ts index 7753ae8ce..9b19b3485 100644 --- a/plugins/cartesia/src/stt.ts +++ b/plugins/cartesia/src/stt.ts @@ -293,7 +293,7 @@ export class SpeechStream extends stt.SpeechStream { try { const url = this.#getCartesiaUrl(); - this.#logger.debug({ url }, 'connecting to Cartesia STT'); + this.#logger.debug(`Connecting to Cartesia STT: ${url}`); const ws = new WebSocket(url, { headers: { diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 6c24871eb..51874b26f 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -513,10 +513,7 @@ export class LLMStream extends llm.LLMStream { } if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) { - this.logger.warn( - { 'lk.pii.response': chunk }, - 'Google LLM response contained no content', - ); + this.logger.warn(`No content in the response: ${JSON.stringify(chunk)}`); if (retryable) { throw new APIStatusError({ message: 'Google LLM: no content in the response', diff --git a/plugins/liveavatar/src/api.ts b/plugins/liveavatar/src/api.ts index cfb9148ba..a010662b7 100644 --- a/plugins/liveavatar/src/api.ts +++ b/plugins/liveavatar/src/api.ts @@ -186,7 +186,10 @@ export class LiveAvatarAPI { if (e instanceof APIStatusError && !e.retryable) { throw e; } - this.#logger.warn({ error: String(e), url, attempt: i }, 'LiveAvatar API request failed'); + this.#logger.warn( + { error: String(e), url, attempt: i }, + `API request to ${url} failed on attempt ${i}`, + ); } if (i < maxRetry) { diff --git a/plugins/openai/src/realtime/realtime_model.ts b/plugins/openai/src/realtime/realtime_model.ts index c69bfbfd8..efa1e85ac 100644 --- a/plugins/openai/src/realtime/realtime_model.ts +++ b/plugins/openai/src/realtime/realtime_model.ts @@ -1075,7 +1075,7 @@ export class RealtimeSession extends llm.RealtimeSession { }); if (lkOaiDebug) { - this.#logger.debug({ url }, 'connecting to OpenAI Realtime API'); + this.#logger.debug(`Connecting to OpenAI Realtime API at ${url}`); } return new Promise((resolve, reject) => { diff --git a/plugins/protoface/src/avatar.ts b/plugins/protoface/src/avatar.ts index 017004aa8..fefa1e488 100644 --- a/plugins/protoface/src/avatar.ts +++ b/plugins/protoface/src/avatar.ts @@ -132,7 +132,7 @@ export class AvatarSession extends voice.AvatarSession { this.sessionIdValue = session.id; this.#logger.debug( - { sessionId: this.sessionIdValue, 'lk.pii.avatar_id': this.avatarId }, + { sessionId: this.sessionIdValue, avatarId: this.avatarId }, 'protoface session started', ); diff --git a/plugins/runway/src/avatar.ts b/plugins/runway/src/avatar.ts index f41387952..c1a23c228 100644 --- a/plugins/runway/src/avatar.ts +++ b/plugins/runway/src/avatar.ts @@ -211,7 +211,7 @@ export class AvatarSession extends voice.AvatarSession { this.realtimeSessionId = payload.id; } else { this.#logger.warn( - { 'lk.pii.payload': payload }, + { payload: JSON.stringify(payload) }, 'Runway API response missing session id; API cancellation fallback will be unavailable', ); } @@ -303,11 +303,7 @@ export class AvatarSession extends voice.AvatarSession { this.#logger.debug({ sessionId }, 'cancelled Runway realtime session'); } else { this.#logger.warn( - { - sessionId, - status: response.status, - 'lk.pii.body': await response.text(), - }, + { sessionId, status: response.status, body: await response.text() }, 'could not cancel Runway realtime session', ); } diff --git a/plugins/sarvam/src/stt.ts b/plugins/sarvam/src/stt.ts index 518c0180e..6ca9dece6 100644 --- a/plugins/sarvam/src/stt.ts +++ b/plugins/sarvam/src/stt.ts @@ -377,11 +377,7 @@ function extractConfidence( } if (value != null) { logger.debug( - { - valueType: typeof value, - 'lk.pii.value': value, - }, - 'unexpected language_probability type; falling back to confidence=1.0', + `Unexpected language_probability type: ${typeof value} (value=${JSON.stringify(value)}); falling back to confidence=1.0`, ); } return 1; @@ -593,7 +589,7 @@ export class SpeechStream extends stt.SpeechStream { while (!this.input.closed && !this.closed) { const wsUrl = buildWsUrl(this.#opts); - this.#logger.info({ url: wsUrl }, 'Sarvam STT connecting'); + this.#logger.info(`Sarvam STT connecting to: ${wsUrl}`); const ws = new WebSocket(wsUrl, { headers: { 'api-subscription-key': this.#opts.apiKey }, }); diff --git a/plugins/soniox/src/tts.ts b/plugins/soniox/src/tts.ts index 7f963c2b2..ba745836c 100644 --- a/plugins/soniox/src/tts.ts +++ b/plugins/soniox/src/tts.ts @@ -654,12 +654,8 @@ class Connection { if (typeof streamId !== 'string') { if (response.error_code !== undefined) { this.#logger.error( - { - errorCode: response.error_code, - errorMessage: response.error_message, - 'lk.pii.response': response, - }, - 'Soniox TTS connection-level error', + { response }, + `Soniox TTS connection-level error: ${response.error_code} - ${response.error_message}`, ); } return; diff --git a/plugins/trugen/src/avatar.ts b/plugins/trugen/src/avatar.ts index 4b719523b..71276e012 100644 --- a/plugins/trugen/src/avatar.ts +++ b/plugins/trugen/src/avatar.ts @@ -202,7 +202,7 @@ export class AvatarSession extends voice.AvatarSession { } catch (e) { if (e instanceof APIStatusError) { this.#logger.warn( - { statusCode: e.statusCode, 'lk.pii.body': e.body }, + { statusCode: e.statusCode, body: e.body }, 'failed to call trugen api', ); if (!e.retryable) {