-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(pino): support proxied loggers like cls-proxify #18943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: process.env.SENTRY_DSN, | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| enableLogs: true, | ||
| integrations: [Sentry.pinoIntegration({ autoInstrument: false })], | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import pino from 'pino'; | ||
|
|
||
| function createProxiedLogger(originalLogger) { | ||
| return new Proxy(originalLogger, { | ||
| get(target, prop) { | ||
| const value = target[prop]; | ||
| if (typeof value === 'function') { | ||
| return (...args) => { | ||
| return value.apply(target, args); | ||
| }; | ||
| } | ||
| return value; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const baseLogger = pino({ name: 'proxied-app' }); | ||
| const proxiedLogger = createProxiedLogger(baseLogger); | ||
|
|
||
| Sentry.pinoIntegration.trackLogger(proxiedLogger); | ||
|
|
||
| Sentry.withIsolationScope(() => { | ||
| Sentry.startSpan({ name: 'startup' }, () => { | ||
| proxiedLogger.info({ user: 'user-id', context: 'proxied' }, 'hello from proxied logger'); | ||
| }); | ||
| }); | ||
|
|
||
| setTimeout(() => { | ||
| Sentry.withIsolationScope(() => { | ||
| Sentry.startSpan({ name: 'later' }, () => { | ||
| const child = proxiedLogger.child({ module: 'authentication' }); | ||
| child.error(new Error('oh no from proxied logger')); | ||
| }); | ||
| }); | ||
| }, 1000); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,9 @@ import { addInstrumentationConfig } from '../sdk/injectLoader'; | |
|
|
||
| const SENTRY_TRACK_SYMBOL = Symbol('sentry-track-pino-logger'); | ||
|
|
||
| const trackedLoggers = new WeakSet<Pino>(); | ||
| const ignoredLoggers = new WeakSet<Pino>(); | ||
|
|
||
| type LevelMapping = { | ||
| // Fortunately pino uses the same levels as Sentry | ||
| labels: { [level: number]: LogSeverityLevel }; | ||
|
|
@@ -24,6 +27,29 @@ type Pino = { | |
| [SENTRY_TRACK_SYMBOL]?: 'track' | 'ignore'; | ||
| }; | ||
|
|
||
| function isPinoLogger(obj: unknown): obj is Pino { | ||
| if (!obj || typeof obj !== 'object') { | ||
| return false; | ||
| } | ||
|
|
||
| if ('levels' in obj && obj.levels && typeof obj.levels === 'object' && 'labels' in obj.levels) { | ||
| return true; | ||
| } | ||
|
|
||
| const hasPinoSymbols = Object.getOwnPropertySymbols(obj).some(sym => | ||
| sym.toString().includes('pino') | ||
| ); | ||
| let proto = Object.getPrototypeOf(obj); | ||
| while (proto && proto !== Object.prototype) { | ||
| if ('levels' in proto && proto.levels && typeof proto.levels === 'object' && 'labels' in proto.levels) { | ||
| return true; | ||
| } | ||
| proto = Object.getPrototypeOf(proto); | ||
| } | ||
|
|
||
| return hasPinoSymbols; | ||
| } | ||
|
|
||
| /** | ||
| * Gets a custom Pino key from a logger instance by searching for the symbol. | ||
| * Pino uses non-global symbols like Symbol('pino.messageKey'): https://github.com/pinojs/pino/blob/8a816c0b1f72de5ae9181f3bb402109b66f7d812/lib/symbols.js | ||
|
|
@@ -118,9 +144,22 @@ const _pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoOptions | |
| log: { ...DEFAULT_OPTIONS.log, ...userOptions.log }, | ||
| }; | ||
|
|
||
| function shouldTrackLogger(logger: Pino): boolean { | ||
| const override = logger[SENTRY_TRACK_SYMBOL]; | ||
| return override === 'track' || (override !== 'ignore' && options.autoInstrument); | ||
| function shouldTrackLogger(logger: unknown): boolean { | ||
| if (!isPinoLogger(logger)) { | ||
| return false; | ||
| } | ||
|
|
||
| const pinoLogger = logger as Pino; | ||
|
|
||
| if (ignoredLoggers.has(pinoLogger)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (trackedLoggers.has(pinoLogger)) { | ||
| return true; | ||
| } | ||
|
|
||
| return options.autoInstrument; | ||
| } | ||
|
|
||
| return { | ||
|
|
@@ -235,15 +274,51 @@ interface PinoIntegrationFunction { | |
| * | ||
| * Requires Pino >=v8.0.0 and Node >=20.6.0 or >=18.19.0 | ||
| */ | ||
| function trackLoggerInstance(logger: Pino): void { | ||
| trackedLoggers.add(logger); | ||
| ignoredLoggers.delete(logger); | ||
|
|
||
| try { | ||
| // @ts-expect-error - Checking proxy internals | ||
| if (logger.constructor === Proxy) { | ||
| const target = (logger as any).__target__ || (logger as any).target; | ||
| if (target && isPinoLogger(target)) { | ||
| trackedLoggers.add(target); | ||
| ignoredLoggers.delete(target); | ||
| } | ||
| } | ||
|
Comment on lines
+283
to
+289
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The proxy detection logic using Suggested FixThe proxy detection logic needs to be rewritten. Instead of checking Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proxy detection check will never executeHigh Severity The check Additional Locations (1) |
||
| } catch { | ||
| // Ignore errors when trying to access proxy internals | ||
| } | ||
| } | ||
|
|
||
| function untrackLoggerInstance(logger: Pino): void { | ||
| ignoredLoggers.add(logger); | ||
| trackedLoggers.delete(logger); | ||
|
|
||
| try { | ||
| // @ts-expect-error - Checking proxy internals | ||
| if (logger.constructor === Proxy) { | ||
| const target = (logger as any).__target__ || (logger as any).target; | ||
| if (target && isPinoLogger(target)) { | ||
| ignoredLoggers.add(target); | ||
| trackedLoggers.delete(target); | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore errors | ||
| } | ||
| } | ||
|
|
||
| export const pinoIntegration = Object.assign(_pinoIntegration, { | ||
| trackLogger(logger: unknown): void { | ||
| if (logger && typeof logger === 'object' && 'levels' in logger) { | ||
| (logger as Pino)[SENTRY_TRACK_SYMBOL] = 'track'; | ||
| if (isPinoLogger(logger)) { | ||
| trackLoggerInstance(logger as Pino); | ||
| } | ||
| }, | ||
| untrackLogger(logger: unknown): void { | ||
| if (logger && typeof logger === 'object' && 'levels' in logger) { | ||
| (logger as Pino)[SENTRY_TRACK_SYMBOL] = 'ignore'; | ||
| if (isPinoLogger(logger)) { | ||
| untrackLoggerInstance(logger as Pino); | ||
| } | ||
| }, | ||
| }) as PinoIntegrationFunction; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Child loggers not tracked when parent tracked
High Severity
Child loggers created from tracked parent loggers aren't automatically tracked with
autoInstrument: false. Whenlogger.child()is called, it returns a new logger instance not present intrackedLoggers. TheshouldTrackLoggerfunction checks if the child is in the WeakSet, finds it missing, and returnsoptions.autoInstrument(false), causing child loggers to not be captured. Existing tests likescenario-track.mjsexpect child loggers to inherit tracking status from parents but this functionality is missing.