Skip to content

Commit 2f1f1dc

Browse files
committed
feat: add per-job context to global Pino logger
Set jobId and roomName on the global logger when a job starts so that all SDK-internal logs (TTS/STT metrics, speech events, AgentSession lifecycle) are filterable by job in log aggregation tools. Context is cleared after shutdown callbacks complete to prevent stale fields leaking to the next job on the same worker.
1 parent 54f108e commit 2f1f1dc

4 files changed

Lines changed: 41 additions & 6 deletions

File tree

.changeset/per-job-log-context.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@livekit/agents': minor
3+
---
4+
5+
Add per-job context to the global Pino logger for session-level log filtering
6+
7+
The global logger now automatically includes `jobId` and `roomName` on every log line during an active job. This makes it possible to filter all SDK-internal logs (TTS/STT metrics, speech events, AgentSession lifecycle) by job in log aggregation tools like NewRelic, Datadog, or Grafana.
8+
9+
Context is set when a job starts and cleared after shutdown callbacks complete.

agents/src/ipc/job_proc_lazy_main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { pathToFileURL } from 'node:url';
88
import type { Logger } from 'pino';
99
import { type Agent, isAgent } from '../generator.js';
1010
import { JobContext, JobProcess, type RunningJobInfo, runWithJobContextAsync } from '../job.js';
11+
import { setJobContext } from '../log.js';
1112
import { initializeLogger, log } from '../log.js';
1213
import { Future, shortuuid } from '../utils.js';
1314
import { defaultInitializeProcessFunc } from '../worker.js';
@@ -184,6 +185,7 @@ const startJob = (
184185
logger.error({ error }, 'error while shutting down the job'),
185186
);
186187

188+
setJobContext({});
187189
safeSend({ case: 'done', value: undefined });
188190
joinFuture.resolve();
189191
})();

agents/src/job.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as os from 'node:os';
1616
import * as path from 'node:path';
1717
import type { Logger } from 'pino';
1818
import type { InferenceExecutor } from './ipc/inference_executor.js';
19-
import { log } from './log.js';
19+
import { log, setJobContext } from './log.js';
2020
import { flushOtelLogs, setupCloudTracer, uploadSessionReport } from './telemetry/index.js';
2121
import { isCloud } from './utils.js';
2222
import type { AgentSession } from './voice/agent_session.js';
@@ -139,10 +139,9 @@ export class JobContext<ProcessUserData = Record<string, unknown>> {
139139
this.#onShutdown = onShutdown;
140140
this.onParticipantConnected = this.onParticipantConnected.bind(this);
141141
this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
142-
this.#logger = log().child({
143-
jobId: this.#info.job.id,
144-
roomName: this.#info.job.room?.name,
145-
});
142+
const jobCtx = { jobId: this.#info.job.id, roomName: this.#info.job.room?.name };
143+
setJobContext(jobCtx);
144+
this.#logger = log().child(jobCtx);
146145
this.#inferenceExecutor = inferenceExecutor;
147146
this._sessionDirectory = path.join(os.tmpdir(), 'livekit-agents', `job-${this.#info.job.id}`);
148147
}

agents/src/log.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ export type LoggerOptions = {
1717
// This avoids the "dual package hazard". Symbol.for() returns the same Symbol
1818
// across all module instances, and globalThis is shared process-wide.
1919
const LOGGER_KEY = Symbol.for('@livekit/agents:logger');
20+
const BASE_LOGGER_KEY = Symbol.for('@livekit/agents:baseLogger');
2021
const LOGGER_OPTIONS_KEY = Symbol.for('@livekit/agents:loggerOptions');
2122
const OTEL_ENABLED_KEY = Symbol.for('@livekit/agents:otelEnabled');
2223

2324
type GlobalState = {
2425
[LOGGER_KEY]?: Logger;
26+
[BASE_LOGGER_KEY]?: Logger;
2527
[LOGGER_OPTIONS_KEY]?: LoggerOptions;
2628
[OTEL_ENABLED_KEY]?: boolean;
2729
};
@@ -40,6 +42,27 @@ export const log = () => {
4042
return logger;
4143
};
4244

45+
/**
46+
* Sets per-job context fields on the global logger. All subsequent calls to
47+
* {@link log} will return a child logger that includes these fields on every
48+
* log line (e.g. `jobId`, `roomName`).
49+
*
50+
* Call with an empty object to clear the context (e.g. after a job ends).
51+
*
52+
* @remarks
53+
* LiveKit workers process one job at a time, so mutating the global logger
54+
* is safe — there is no risk of concurrent jobs interleaving context.
55+
*
56+
* @internal
57+
*/
58+
export const setJobContext = (ctx: Record<string, unknown>) => {
59+
if (!globals[BASE_LOGGER_KEY]) {
60+
globals[BASE_LOGGER_KEY] = globals[LOGGER_KEY];
61+
}
62+
const base = globals[BASE_LOGGER_KEY]!;
63+
globals[LOGGER_KEY] = Object.keys(ctx).length ? base.child(ctx) : base;
64+
};
65+
4366
/** @internal */
4467
export const initializeLogger = ({ pretty, level }: LoggerOptions) => {
4568
globals[LOGGER_OPTIONS_KEY] = { pretty, level };
@@ -90,8 +113,10 @@ export const enableOtelLogging = () => {
90113
{ stream: new OtelDestination(), level: 'debug' },
91114
];
92115

93-
globals[LOGGER_KEY] = pino(
116+
const newBase = pino(
94117
{ level: logLevel, serializers: { error: pino.stdSerializers.err } },
95118
multistream(streams),
96119
);
120+
globals[LOGGER_KEY] = newBase;
121+
globals[BASE_LOGGER_KEY] = newBase;
97122
};

0 commit comments

Comments
 (0)