Skip to content

Commit 2b543e4

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. The job context is stored separately so enableOtelLogging() can re-apply it when reconfiguring the logger mid-job. Context is cleared after shutdown callbacks complete to prevent stale fields leaking to the next job on the same worker.
1 parent 54f108e commit 2b543e4

4 files changed

Lines changed: 45 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: 3 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,8 @@ 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+
setJobContext({ jobId: this.#info.job.id, roomName: this.#info.job.room?.name });
143+
this.#logger = log();
146144
this.#inferenceExecutor = inferenceExecutor;
147145
this._sessionDirectory = path.join(os.tmpdir(), 'livekit-agents', `job-${this.#info.job.id}`);
148146
}

agents/src/log.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ 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');
21+
const JOB_CONTEXT_KEY = Symbol.for('@livekit/agents:jobContext');
2022
const LOGGER_OPTIONS_KEY = Symbol.for('@livekit/agents:loggerOptions');
2123
const OTEL_ENABLED_KEY = Symbol.for('@livekit/agents:otelEnabled');
2224

2325
type GlobalState = {
2426
[LOGGER_KEY]?: Logger;
27+
[BASE_LOGGER_KEY]?: Logger;
28+
[JOB_CONTEXT_KEY]?: Record<string, unknown>;
2529
[LOGGER_OPTIONS_KEY]?: LoggerOptions;
2630
[OTEL_ENABLED_KEY]?: boolean;
2731
};
@@ -40,6 +44,29 @@ export const log = () => {
4044
return logger;
4145
};
4246

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

93-
globals[LOGGER_KEY] = pino(
120+
const newBase = pino(
94121
{ level: logLevel, serializers: { error: pino.stdSerializers.err } },
95122
multistream(streams),
96123
);
124+
globals[BASE_LOGGER_KEY] = newBase;
125+
const activeJobCtx = globals[JOB_CONTEXT_KEY];
126+
globals[LOGGER_KEY] = activeJobCtx ? newBase.child(activeJobCtx) : newBase;
97127
};

0 commit comments

Comments
 (0)