Skip to content

Commit 594286c

Browse files
committed
feat: pino integration
1 parent 94a22de commit 594286c

File tree

13 files changed

+353
-0
lines changed

13 files changed

+353
-0
lines changed

dev-packages/node-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"node-schedule": "^2.1.1",
6161
"openai": "5.18.1",
6262
"pg": "8.16.0",
63+
"pino": "^9.9.4",
6364
"postgres": "^3.4.7",
6465
"proxy": "^2.1.1",
6566
"redis-4": "npm:redis@^4.6.14",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/node';
2+
3+
Sentry.init({
4+
dsn: process.env.SENTRY_DSN,
5+
release: '1.0',
6+
enableLogs: true,
7+
integrations: [Sentry.pinoIntegration()],
8+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Sentry from '@sentry/node';
2+
import pino from 'pino';
3+
4+
const logger = pino({});
5+
6+
Sentry.withIsolationScope(() => {
7+
Sentry.startSpan({ name: 'startup' }, () => {
8+
logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world');
9+
});
10+
});
11+
12+
setTimeout(() => {
13+
Sentry.withIsolationScope(() => {
14+
Sentry.startSpan({ name: 'later' }, () => {
15+
logger.error(new Error('oh no'));
16+
});
17+
});
18+
}, 1000);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { join } from 'path';
2+
import { describe, expect, test } from 'vitest';
3+
import { createRunner } from '../../utils/runner';
4+
5+
describe('Pino integration', () => {
6+
test('has different trace ids for logs from different spans', async () => {
7+
// expect.assertions(1);
8+
const instrumentPath = join(__dirname, 'instrument.mjs');
9+
10+
await createRunner(__dirname, 'scenario.mjs')
11+
.withMockSentryServer()
12+
.withInstrument(instrumentPath)
13+
.ignore('event')
14+
.expect({
15+
log: log => {
16+
const traceId1 = log.items?.[0]?.trace_id;
17+
const traceId2 = log.items?.[1]?.trace_id;
18+
expect(traceId1).not.toBe(traceId2);
19+
},
20+
})
21+
.start()
22+
.completed();
23+
});
24+
25+
test('captures event and logs', async () => {
26+
// expect.assertions(1);
27+
const instrumentPath = join(__dirname, 'instrument.mjs');
28+
29+
await createRunner(__dirname, 'scenario.mjs')
30+
.withMockSentryServer()
31+
.withInstrument(instrumentPath)
32+
.expect({
33+
event: {
34+
exception: {
35+
values: [
36+
{
37+
type: 'Error',
38+
value: 'oh no',
39+
mechanism: {
40+
type: 'pino',
41+
handled: true,
42+
},
43+
stacktrace: {
44+
frames: expect.arrayContaining([
45+
expect.objectContaining({
46+
function: '?',
47+
in_app: true,
48+
module: 'scenario',
49+
context_line: " logger.error(new Error('oh no'));",
50+
}),
51+
]),
52+
},
53+
},
54+
],
55+
},
56+
},
57+
})
58+
.expect({
59+
log: {
60+
items: [
61+
{
62+
timestamp: expect.any(Number),
63+
level: 'info',
64+
body: 'hello world',
65+
trace_id: expect.any(String),
66+
severity_number: 9,
67+
attributes: expect.objectContaining({
68+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
69+
'sentry.pino.level': { value: 30, type: 'integer' },
70+
user: { value: 'user-id', type: 'string' },
71+
'something.more': { value: 3, type: 'integer' },
72+
'something.complex': { value: 'nope', type: 'string' },
73+
'sentry.release': { value: '1.0', type: 'string' },
74+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
75+
}),
76+
},
77+
{
78+
timestamp: expect.any(Number),
79+
level: 'error',
80+
body: 'oh no',
81+
trace_id: expect.any(String),
82+
severity_number: 17,
83+
attributes: expect.objectContaining({
84+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
85+
'sentry.pino.level': { value: 50, type: 'integer' },
86+
err: { value: '{}', type: 'string' },
87+
'sentry.release': { value: '1.0', type: 'string' },
88+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
89+
}),
90+
},
91+
],
92+
},
93+
})
94+
.start()
95+
.completed();
96+
});
97+
});

packages/core/src/utils/worldwide.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type InternalGlobal = {
4848
*/
4949
_sentryModuleMetadata?: Record<string, any>;
5050
_sentryEsmLoaderHookRegistered?: boolean;
51+
_sentryInjectLoaderHookRegistered?: boolean;
5152
} & Carrier;
5253

5354
/** Get's the global object for the current JavaScript runtime */

packages/node-core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@
6666
"@opentelemetry/semantic-conventions": "^1.34.0"
6767
},
6868
"dependencies": {
69+
"@apm-js-collab/tracing-hooks": "^0.1.1",
6970
"@sentry/core": "10.11.0",
7071
"@sentry/opentelemetry": "10.11.0",
7172
"import-in-the-middle": "^1.14.2"
7273
},
7374
"devDependencies": {
75+
"@apm-js-collab/code-transformer": "^0.7.0",
7476
"@opentelemetry/api": "^1.9.0",
7577
"@opentelemetry/context-async-hooks": "^2.0.0",
7678
"@opentelemetry/core": "^2.0.0",

packages/node-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export { spotlightIntegration } from './integrations/spotlight';
2424
export { systemErrorIntegration } from './integrations/systemError';
2525
export { childProcessIntegration } from './integrations/childProcess';
2626
export { createSentryWinstonTransport } from './integrations/winston';
27+
export { pinoIntegration } from './integrations/pino';
2728

2829
export { SentryContextManager } from './otel/contextManager';
2930
export { setupOpenTelemetryLogger } from './otel/logger';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { tracingChannel } from 'node:diagnostics_channel';
2+
import type { IntegrationFn, LogSeverityLevel } from '@sentry/core';
3+
import {
4+
_INTERNAL_captureLog,
5+
addExceptionMechanism,
6+
captureException,
7+
captureMessage,
8+
severityLevelFromString,
9+
withScope,
10+
} from '@sentry/core';
11+
import { addInstrumentationConfig } from '../sdk/injectLoader';
12+
13+
type LevelMapping = {
14+
// Fortunately pino uses the same levels as Sentry
15+
labels: { [level: number]: LogSeverityLevel };
16+
};
17+
18+
type Pino = {
19+
levels: LevelMapping;
20+
};
21+
22+
type MergeObject = {
23+
[key: string]: unknown;
24+
err?: Error;
25+
};
26+
27+
type PinoHookArgs = {
28+
self: Pino;
29+
arguments: [MergeObject, string, number];
30+
};
31+
32+
type Options = {
33+
/**
34+
* Levels that trigger capturing of events.
35+
*
36+
* @default ["error", "fatal"]
37+
*/
38+
eventLevels?: LogSeverityLevel[];
39+
/**
40+
* By default, Sentry will mark captured console messages as handled.
41+
* Set this to `false` if you want to mark them as unhandled instead.
42+
*
43+
* @default true
44+
*/
45+
handled?: boolean;
46+
};
47+
48+
function attributesFromObject(obj: object, attr: Record<string, unknown>, key?: string): Record<string, unknown> {
49+
for (const [k, v] of Object.entries(obj)) {
50+
const newKey = key ? `${key}.${k}` : k;
51+
if (v && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Error)) {
52+
attributesFromObject(v as object, attr, newKey);
53+
} else {
54+
attr[newKey] = v;
55+
}
56+
}
57+
return attr;
58+
}
59+
60+
export const pinoIntegration = ((options: Options = { eventLevels: ['error', 'fatal'], handled: true }) => {
61+
return {
62+
name: 'Pino',
63+
setup: () => {
64+
addInstrumentationConfig({
65+
channelName: 'pino-log',
66+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
67+
// @ts-ignore https://github.com/apm-js-collab/orchestrion-js/pull/35
68+
module: { name: 'pino', versionRange: '>=8.0.0', filePath: 'lib/tools.js' },
69+
functionQuery: {
70+
functionName: 'asJson',
71+
kind: 'Sync',
72+
},
73+
});
74+
75+
const channel = tracingChannel('orchestrion:pino:pino-log');
76+
77+
channel.start.subscribe(data => {
78+
const { self, arguments: args } = data as PinoHookArgs;
79+
const [obj, message, levelNumber] = args;
80+
const level = self?.levels?.labels?.[levelNumber] || 'info';
81+
82+
const attributes = attributesFromObject(obj, {
83+
'sentry.origin': 'auto.logging.pino',
84+
'sentry.pino.level': levelNumber,
85+
});
86+
87+
_INTERNAL_captureLog({ level, message, attributes });
88+
89+
if (options.eventLevels?.includes(level)) {
90+
const captureContext = {
91+
level: severityLevelFromString(level),
92+
};
93+
94+
withScope(scope => {
95+
scope.addEventProcessor(event => {
96+
event.logger = 'pino';
97+
98+
addExceptionMechanism(event, {
99+
handled: !!options.handled,
100+
type: 'pino',
101+
});
102+
103+
return event;
104+
});
105+
106+
if (obj.err) {
107+
captureException(obj.err, captureContext);
108+
return;
109+
}
110+
111+
captureMessage(message, captureContext);
112+
});
113+
}
114+
});
115+
},
116+
};
117+
}) satisfies IntegrationFn;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare module '@apm-js-collab/tracing-hooks' {
2+
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
3+
4+
type PatchConfig = { instrumentations: InstrumentationConfig[]; packages: Set<string> };
5+
6+
/** Hooks require */
7+
export default class ModulePatch {
8+
public constructor(config: PatchConfig): ModulePatch;
9+
public patch(): void;
10+
}
11+
}

packages/node-core/src/sdk/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { envToBool } from '../utils/envToBool';
4040
import { defaultStackParser, getSentryRelease } from './api';
4141
import { NodeClient } from './client';
4242
import { initializeEsmLoader } from './esmLoader';
43+
import { initializeInjectionLoader } from './injectLoader';
4344

4445
/**
4546
* Get default integrations for the Node-Core SDK.
@@ -131,6 +132,8 @@ function _init(
131132

132133
client.init();
133134

135+
initializeInjectionLoader();
136+
134137
debug.log(`SDK initialized from ${isCjs() ? 'CommonJS' : 'ESM'}`);
135138

136139
client.startClientReportTracking();

0 commit comments

Comments
 (0)