Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 })],
});
23 changes: 23 additions & 0 deletions dev-packages/node-integration-tests/suites/pino/scenario-track.mjs
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 5 additions & 0 deletions dev-packages/node-integration-tests/suites/pino/scenario.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
50 changes: 50 additions & 0 deletions dev-packages/node-integration-tests/suites/pino/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
77 changes: 69 additions & 8 deletions packages/node-core/src/integrations/pino.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,13 +11,16 @@ 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 };
};

type Pino = {
levels: LevelMapping;
[SENTRY_TRACK_SYMBOL]?: 'track' | 'ignore';
};

type MergeObject = {
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -55,6 +72,7 @@ type PinoOptions = {
};

const DEFAULT_OPTIONS: PinoOptions = {
autoInstrument: true,
error: { levels: [], handled: true },
log: { levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] },
};
Expand All @@ -63,18 +81,18 @@ type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? Partial<T[P]> : 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<PinoOptions> = {}) => {
const _pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoOptions> = {}) => {
const options: PinoOptions = {
autoInstrument: 'autoInstrument' in userOptions ? !!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 => {
Expand All @@ -95,6 +113,10 @@ export const pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoO
const integratedChannel = tracingChannel('pino_asJson');

function onPinoStart(self: Pino, args: PinoHookArgs, result: string): void {
if (!shouldTrackLogger(self)) {
return;
}

const [obj, message, levelNumber] = args;
const level = self?.levels?.labels?.[levelNumber] || 'info';

Expand Down Expand Up @@ -157,3 +179,42 @@ export const pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoO
},
};
}) satisfies IntegrationFn;

interface PinoIntegrationFunction {
(userOptions?: DeepPartial<PinoOptions>): 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;
Loading