Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bf70c17
feat: Tidy existing loader hook
timfish Sep 8, 2025
9ee96ab
Oh linty
timfish Sep 8, 2025
9a57643
fix detection
timfish Sep 8, 2025
9fb250e
Use `NODE_MAJOR` and `NODE_MINOR`
timfish Sep 9, 2025
94a22de
Merge branch 'develop' into timfish/feat/tidy-loader-hooks
timfish Sep 10, 2025
594286c
feat: `pino` integration
timfish Sep 10, 2025
1699704
Merge remote-tracking branch 'upstream/develop' into timfish/feat/pin…
timfish Sep 10, 2025
630a8c7
Update deps
timfish Sep 12, 2025
fd32e77
Merge remote-tracking branch 'upstream/develop' into timfish/feat/pin…
timfish Sep 12, 2025
c2f81e1
Oh lint!
timfish Sep 12, 2025
850c19d
Correct yarn.lock
timfish Sep 12, 2025
144b7db
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 12, 2025
7610cda
Tests
timfish Sep 15, 2025
e598c6a
Merge branch 'timfish/feat/pino-integration' of github.com:getsentry/…
timfish Sep 15, 2025
2647b20
More test fixes
timfish Sep 15, 2025
e0f6a40
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 15, 2025
6089389
Fix size limit
timfish Sep 15, 2025
687a619
Merge branch 'timfish/feat/pino-integration' of github.com:getsentry/…
timfish Sep 15, 2025
6b8f76e
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 15, 2025
421ad50
Make hook tree-shakable
timfish Sep 18, 2025
b12150c
Pino added tracing channel
timfish Sep 18, 2025
d61b2e6
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 18, 2025
40bf557
Merge branch 'timfish/feat/pino-integration' of github.com:getsentry/…
timfish Sep 18, 2025
30a6688
Better bundling?
timfish Sep 18, 2025
412cc67
Fixes
timfish Sep 18, 2025
2e14fbe
Fix Pino >= 9.10.0
timfish Sep 18, 2025
1115467
Lint
timfish Sep 18, 2025
ab7bce3
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 19, 2025
8b5a821
Fix yarn.lock
timfish Sep 19, 2025
0715f38
Update deps
timfish Sep 19, 2025
7a207f4
Merge branch 'develop' into timfish/feat/pino-integration
timfish Sep 19, 2025
1a72eab
Just copy attributes
timfish Sep 19, 2025
ac49a64
PR review
timfish Sep 21, 2025
4f50a93
Merge remote-tracking branch 'upstream/develop' into timfish/feat/pin…
timfish Oct 2, 2025
c1f6870
update deps
timfish Oct 2, 2025
1a4b10e
remove yarn.lock dupes
timfish Oct 2, 2025
11f77b5
Test on latest Pino with integrated channel
timfish Oct 2, 2025
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
Expand Up @@ -53,6 +53,7 @@ const DEPENDENTS: Dependent[] = [
'NODE_VERSION',
'childProcessIntegration',
'systemErrorIntegration',
'pinoIntegration',
],
},
{
Expand Down
1 change: 1 addition & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"node-schedule": "^2.1.1",
"openai": "5.18.1",
"pg": "8.16.0",
"pino": "9.9.4",
"postgres": "^3.4.7",
"prisma": "6.15.0",
"proxy": "^2.1.1",
Expand Down
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({ error: { levels: ['error', 'fatal'] } })],
});
18 changes: 18 additions & 0 deletions dev-packages/node-integration-tests/suites/pino/scenario.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Sentry from '@sentry/node';
import pino from 'pino';

const logger = pino({});

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);
99 changes: 99 additions & 0 deletions dev-packages/node-integration-tests/suites/pino/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { conditionalTest } from '../../utils';
import { createRunner } from '../../utils/runner';

conditionalTest({ min: 20 })('Pino integration', () => {
test('has different trace ids for logs from different spans', async () => {
const instrumentPath = join(__dirname, 'instrument.mjs');

await createRunner(__dirname, 'scenario.mjs')
.withMockSentryServer()
.withInstrument(instrumentPath)
.ignore('event')
.expect({
log: log => {
const traceId1 = log.items?.[0]?.trace_id;
const traceId2 = log.items?.[1]?.trace_id;
expect(traceId1).not.toBe(traceId2);
},
})
.start()
.completed();
});

test('captures event and logs', async () => {
// expect.assertions(1);
const instrumentPath = join(__dirname, 'instrument.mjs');

await createRunner(__dirname, 'scenario.mjs')
.withMockSentryServer()
.withInstrument(instrumentPath)
.expect({
event: {
exception: {
values: [
{
type: 'Error',
value: 'oh no',
mechanism: {
type: 'pino',
handled: true,
},
stacktrace: {
frames: expect.arrayContaining([
expect.objectContaining({
function: '?',
in_app: true,
module: 'scenario',
context_line: " logger.error(new Error('oh no'));",
}),
]),
},
},
],
},
},
})
.expect({
log: {
items: [
{
timestamp: expect.any(Number),
level: 'info',
body: 'hello world',
trace_id: expect.any(String),
severity_number: 9,
attributes: expect.objectContaining({
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
'sentry.pino.level': { value: 30, type: 'integer' },
user: { value: 'user-id', type: 'string' },
something: {
type: 'string',
value: '{"more":3,"complex":"nope"}',
},
'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({
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
'sentry.pino.level': { value: 50, type: 'integer' },
err: { value: '{}', type: 'string' },
'sentry.release': { value: '1.0', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
}),
},
],
},
})
.start()
.completed();
});
});
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export {
onUnhandledRejectionIntegration,
openAIIntegration,
parameterize,
pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export {
mysql2Integration,
redisIntegration,
tediousIntegration,
pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export type InternalGlobal = {
*/
_sentryModuleMetadata?: Record<string, any>;
_sentryEsmLoaderHookRegistered?: boolean;
_sentryInjectLoaderHookRegister?: () => void;
_sentryInjectLoaderHookRegistered?: boolean;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export {
mysql2Integration,
redisIntegration,
tediousIntegration,
pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
Expand Down
2 changes: 2 additions & 0 deletions packages/node-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@
"@opentelemetry/semantic-conventions": "^1.37.0"
},
"dependencies": {
"@apm-js-collab/tracing-hooks": "^0.3.0",
"@sentry/core": "10.12.0",
"@sentry/opentelemetry": "10.12.0",
"import-in-the-middle": "^1.14.2"
},
"devDependencies": {
"@apm-js-collab/code-transformer": "^0.8.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.1.0",
"@opentelemetry/core": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions packages/node-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { spotlightIntegration } from './integrations/spotlight';
export { systemErrorIntegration } from './integrations/systemError';
export { childProcessIntegration } from './integrations/childProcess';
export { createSentryWinstonTransport } from './integrations/winston';
export { pinoIntegration } from './integrations/pino';

export { SentryContextManager } from './otel/contextManager';
export { setupOpenTelemetryLogger } from './otel/logger';
Expand Down
149 changes: 149 additions & 0 deletions packages/node-core/src/integrations/pino.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { IntegrationFn, LogSeverityLevel } from '@sentry/core';
import {
_INTERNAL_captureLog,
addExceptionMechanism,
captureException,
captureMessage,
defineIntegration,
severityLevelFromString,
withScope,
} from '@sentry/core';
import { addInstrumentationConfig } from '../sdk/injectLoader';

type LevelMapping = {
// Fortunately pino uses the same levels as Sentry
labels: { [level: number]: LogSeverityLevel };
};

type Pino = {
levels: LevelMapping;
};

type MergeObject = {
[key: string]: unknown;
err?: Error;
};

type PinoHookArgs = [MergeObject, string, number];

type PinoOptions = {
error: {
/**
* Levels that trigger capturing of events.
*
* @default []
*/
levels: LogSeverityLevel[];
/**
* By default, Sentry will mark captured errors as handled.
* Set this to `false` if you want to mark them as unhandled instead.
*
* @default true
*/
handled: boolean;
};
log: {
/**
* Levels that trigger capturing of logs. Logs are only captured if
* `enableLogs` is enabled.
*
* @default ["trace", "debug", "info", "warn", "error", "fatal"]
*/
levels: LogSeverityLevel[];
};
};

const DEFAULT_OPTIONS: PinoOptions = {
error: { levels: [], handled: true },
log: { levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] },
};

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 options: PinoOptions = {
error: { ...DEFAULT_OPTIONS.error, ...userOptions.error },
log: { ...DEFAULT_OPTIONS.log, ...userOptions.log },
};

return {
name: 'Pino',
setup: client => {
const enableLogs = !!client.getOptions().enableLogs;

addInstrumentationConfig({
channelName: 'pino-log',
// From Pino v9.10.0 a tracing channel is available directly from Pino:
// https://github.com/pinojs/pino/pull/2281
module: { name: 'pino', versionRange: '>=8.0.0 < 9.10.0', filePath: 'lib/tools.js' },
functionQuery: {
functionName: 'asJson',
kind: 'Sync',
},
});

const injectedChannel = tracingChannel('orchestrion:pino:pino-log');
const integratedChannel = tracingChannel('tracing:pino_asJson');

function onPinoStart(self: Pino, args: PinoHookArgs): void {
const [obj, message, levelNumber] = args;
const level = self?.levels?.labels?.[levelNumber] || 'info';

const attributes = {
...obj,
'sentry.origin': 'auto.logging.pino',
'sentry.pino.level': levelNumber,
};

if (enableLogs && options.log.levels.includes(level)) {
_INTERNAL_captureLog({ level, message, attributes });
}

if (options.error.levels.includes(level)) {
const captureContext = {
level: severityLevelFromString(level),
};

withScope(scope => {
scope.addEventProcessor(event => {
event.logger = 'pino';

addExceptionMechanism(event, {
handled: options.error.handled,
type: 'pino',
});

return event;
});

if (obj.err) {
captureException(obj.err, captureContext);
return;
}

captureMessage(message, captureContext);
});
}
}

injectedChannel.start.subscribe(data => {
const { self, arguments: args } = data as { self: Pino; arguments: PinoHookArgs };
onPinoStart(self, args);
});

integratedChannel.start.subscribe(data => {
const { instance, arguments: args } = data as { instance: Pino; arguments: PinoHookArgs };
onPinoStart(instance, args);
});
},
};
}) satisfies IntegrationFn;
11 changes: 11 additions & 0 deletions packages/node-core/src/sdk/apm-js-collab-tracing-hooks.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare module '@apm-js-collab/tracing-hooks' {
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';

type PatchConfig = { instrumentations: InstrumentationConfig[] };

/** Hooks require */
export default class ModulePatch {
public constructor(config: PatchConfig): ModulePatch;
public patch(): void;
}
}
3 changes: 3 additions & 0 deletions packages/node-core/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
GLOBAL_OBJ,
hasSpansEnabled,
inboundFiltersIntegration,
linkedErrorsIntegration,
Expand Down Expand Up @@ -131,6 +132,8 @@ function _init(

client.init();

GLOBAL_OBJ._sentryInjectLoaderHookRegister?.();

debug.log(`SDK initialized from ${isCjs() ? 'CommonJS' : 'ESM'}`);

client.startClientReportTracking();
Expand Down
Loading