diff --git a/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs new file mode 100644 index 000000000000..d2a0408eebcd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + enableLogs: true, + integrations: [Sentry.pinoIntegration({ autoInstrument: false })], +}); diff --git a/dev-packages/node-integration-tests/suites/pino/scenario-track.mjs b/dev-packages/node-integration-tests/suites/pino/scenario-track.mjs new file mode 100644 index 000000000000..2e968444a74f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/pino/scenario-track.mjs @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; +import pino from 'pino'; + +const logger = pino({ name: 'myapp' }); +Sentry.pinoIntegration.trackLogger(logger); + +const loggerIgnore = pino({ name: 'ignore' }); + +loggerIgnore.info('this should be ignored'); + +Sentry.withIsolationScope(() => { + Sentry.startSpan({ name: 'startup' }, () => { + logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world'); + }); +}); + +setTimeout(() => { + Sentry.withIsolationScope(() => { + Sentry.startSpan({ name: 'later' }, () => { + logger.error(new Error('oh no')); + }); + }); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/pino/scenario.mjs b/dev-packages/node-integration-tests/suites/pino/scenario.mjs index ea8dc5e223d0..beb080ac3c42 100644 --- a/dev-packages/node-integration-tests/suites/pino/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/pino/scenario.mjs @@ -3,6 +3,11 @@ import pino from 'pino'; const logger = pino({ name: 'myapp' }); +const ignoredLogger = pino({ name: 'ignored' }); +Sentry.pinoIntegration.untrackLogger(ignoredLogger); + +ignoredLogger.info('this will not be tracked'); + Sentry.withIsolationScope(() => { Sentry.startSpan({ name: 'startup' }, () => { logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world'); diff --git a/dev-packages/node-integration-tests/suites/pino/test.ts b/dev-packages/node-integration-tests/suites/pino/test.ts index cc88f650203b..1982c8d686fc 100644 --- a/dev-packages/node-integration-tests/suites/pino/test.ts +++ b/dev-packages/node-integration-tests/suites/pino/test.ts @@ -173,4 +173,54 @@ conditionalTest({ min: 20 })('Pino integration', () => { .start() .completed(); }); + + test('captures logs when autoInstrument is false and logger is tracked', async () => { + const instrumentPath = join(__dirname, 'instrument-auto-off.mjs'); + + await createRunner(__dirname, 'scenario-track.mjs') + .withMockSentryServer() + .withInstrument(instrumentPath) + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'hello world', + trace_id: expect.any(String), + severity_number: 9, + attributes: expect.objectContaining({ + 'pino.logger.name': { value: 'myapp', type: 'string' }, + 'pino.logger.level': { value: 30, type: 'integer' }, + user: { value: 'user-id', type: 'string' }, + something: { + type: 'string', + value: '{"more":3,"complex":"nope"}', + }, + 'sentry.origin': { value: 'auto.logging.pino', type: 'string' }, + 'sentry.release': { value: '1.0', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + }), + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'oh no', + trace_id: expect.any(String), + severity_number: 17, + attributes: expect.objectContaining({ + 'pino.logger.name': { value: 'myapp', type: 'string' }, + 'pino.logger.level': { value: 50, type: 'integer' }, + err: { value: '{}', type: 'string' }, + 'sentry.origin': { value: 'auto.logging.pino', type: 'string' }, + 'sentry.release': { value: '1.0', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + }), + }, + ], + }, + }) + .start() + .completed(); + }); }); diff --git a/packages/node-core/src/integrations/pino.ts b/packages/node-core/src/integrations/pino.ts index 6b78bcdb4386..dfc51d5022ff 100644 --- a/packages/node-core/src/integrations/pino.ts +++ b/packages/node-core/src/integrations/pino.ts @@ -1,5 +1,5 @@ import { tracingChannel } from 'node:diagnostics_channel'; -import type { IntegrationFn, LogSeverityLevel } from '@sentry/core'; +import type { Integration, IntegrationFn, LogSeverityLevel } from '@sentry/core'; import { _INTERNAL_captureLog, addExceptionMechanism, @@ -11,6 +11,8 @@ import { } from '@sentry/core'; import { addInstrumentationConfig } from '../sdk/injectLoader'; +const SENTRY_TRACK_SYMBOL = Symbol('sentry-track-pino-logger'); + type LevelMapping = { // Fortunately pino uses the same levels as Sentry labels: { [level: number]: LogSeverityLevel }; @@ -18,6 +20,7 @@ type LevelMapping = { type Pino = { levels: LevelMapping; + [SENTRY_TRACK_SYMBOL]?: 'track' | 'ignore'; }; type MergeObject = { @@ -28,6 +31,17 @@ type MergeObject = { type PinoHookArgs = [MergeObject, string, number]; type PinoOptions = { + /** + * Automatically instrument all Pino loggers. + * + * When set to `false`, only loggers marked with `pinoIntegration.trackLogger(logger)` will be captured. + * + * @default true + */ + autoInstrument: boolean; + /** + * Options to enable capturing of error events. + */ error: { /** * Levels that trigger capturing of events. @@ -43,6 +57,9 @@ type PinoOptions = { */ handled: boolean; }; + /** + * Options to enable capturing of logs. + */ log: { /** * Levels that trigger capturing of logs. Logs are only captured if @@ -55,6 +72,7 @@ type PinoOptions = { }; const DEFAULT_OPTIONS: PinoOptions = { + autoInstrument: true, error: { levels: [], handled: true }, log: { levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] }, }; @@ -63,18 +81,18 @@ type DeepPartial = { [P in keyof T]?: T[P] extends object ? Partial : T[P]; }; -/** - * Integration for Pino logging library. - * Captures Pino logs as Sentry logs and optionally captures some log levels as events. - * - * Requires Pino >=v8.0.0 and Node >=20.6.0 or >=18.19.0 - */ -export const pinoIntegration = defineIntegration((userOptions: DeepPartial = {}) => { +const _pinoIntegration = defineIntegration((userOptions: DeepPartial = {}) => { const options: PinoOptions = { + autoInstrument: userOptions.autoInstrument === false ? userOptions.autoInstrument : DEFAULT_OPTIONS.autoInstrument, error: { ...DEFAULT_OPTIONS.error, ...userOptions.error }, log: { ...DEFAULT_OPTIONS.log, ...userOptions.log }, }; + function shouldTrackLogger(logger: Pino): boolean { + const override = logger[SENTRY_TRACK_SYMBOL]; + return override === 'track' || (override !== 'ignore' && options.autoInstrument); + } + return { name: 'Pino', setup: client => { @@ -95,6 +113,10 @@ export const pinoIntegration = defineIntegration((userOptions: DeepPartial): Integration; + /** + * Marks a Pino logger to be tracked by the Pino integration. + * + * @param logger A Pino logger instance. + */ + trackLogger(logger: unknown): void; + /** + * Marks a Pino logger to be ignored by the Pino integration. + * + * @param logger A Pino logger instance. + */ + untrackLogger(logger: unknown): void; +} + +/** + * Integration for Pino logging library. + * Captures Pino logs as Sentry logs and optionally captures some log levels as events. + * + * By default, all Pino loggers will be captured. To ignore a specific logger, use `pinoIntegration.untrackLogger(logger)`. + * + * If you disable automatic instrumentation with `autoInstrument: false`, you can mark specific loggers to be tracked with `pinoIntegration.trackLogger(logger)`. + * + * Requires Pino >=v8.0.0 and Node >=20.6.0 or >=18.19.0 + */ +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'; + } + }, + untrackLogger(logger: unknown): void { + if (logger && typeof logger === 'object' && 'levels' in logger) { + (logger as Pino)[SENTRY_TRACK_SYMBOL] = 'ignore'; + } + }, +}) as PinoIntegrationFunction;