Skip to content

Commit e88c084

Browse files
authored
feat(node): pino integration (#17584)
- Adds `@apm-js-collab/tracing-hooks` as a dependency - Integrations can register instrumentations which causes the `@apm-js-collab/tracing-hooks` ESM hook and require patching to be initialised later - The `@apm-js-collab/*` dependencies only get included in a bundle when they are used by an integration - Adds a `pinoIntegration` that: - Is not enabled by default (see below) - Registers where it needs code injecting into the pino library - Hooks the tracing channel events, including the channel added to `[email protected]` - Captures Sentry logs for pino logs - Captures in the correct tracing context because this is all sync! - Captures exception/message events for the configured `eventLevels` ## Supported Node versions We can't enable this integration by default because `TracingChannel`, injected by `@apm-js-collab/code-transformer` requires Node >= v19.9.0 or v18.19.0.
1 parent 51a9def commit e88c084

File tree

18 files changed

+481
-8
lines changed

18 files changed

+481
-8
lines changed

dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const DEPENDENTS: Dependent[] = [
5353
'NODE_VERSION',
5454
'childProcessIntegration',
5555
'systemErrorIntegration',
56+
'pinoIntegration',
5657
],
5758
},
5859
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
"node-schedule": "^2.1.1",
6767
"openai": "5.18.1",
6868
"pg": "8.16.0",
69+
"pino": "9.9.4",
70+
"pino-next": "npm:pino@^9.12.0",
6971
"postgres": "^3.4.7",
7072
"prisma": "6.15.0",
7173
"proxy": "^2.1.1",
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({ error: { levels: ['error', 'fatal'] } })],
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-next';
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: 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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { join } from 'path';
2+
import { expect, test } from 'vitest';
3+
import { conditionalTest } from '../../utils';
4+
import { createRunner } from '../../utils/runner';
5+
6+
conditionalTest({ min: 20 })('Pino integration', () => {
7+
test('has different trace ids for logs from different spans', async () => {
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+
const instrumentPath = join(__dirname, 'instrument.mjs');
27+
28+
await createRunner(__dirname, 'scenario.mjs')
29+
.withMockSentryServer()
30+
.withInstrument(instrumentPath)
31+
.expect({
32+
event: {
33+
exception: {
34+
values: [
35+
{
36+
type: 'Error',
37+
value: 'oh no',
38+
mechanism: {
39+
type: 'pino',
40+
handled: true,
41+
},
42+
stacktrace: {
43+
frames: expect.arrayContaining([
44+
expect.objectContaining({
45+
function: '?',
46+
in_app: true,
47+
module: 'scenario',
48+
context_line: " logger.error(new Error('oh no'));",
49+
}),
50+
]),
51+
},
52+
},
53+
],
54+
},
55+
},
56+
})
57+
.expect({
58+
log: {
59+
items: [
60+
{
61+
timestamp: expect.any(Number),
62+
level: 'info',
63+
body: 'hello world',
64+
trace_id: expect.any(String),
65+
severity_number: 9,
66+
attributes: expect.objectContaining({
67+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
68+
'sentry.pino.level': { value: 30, type: 'integer' },
69+
user: { value: 'user-id', type: 'string' },
70+
something: {
71+
type: 'string',
72+
value: '{"more":3,"complex":"nope"}',
73+
},
74+
'sentry.release': { value: '1.0', type: 'string' },
75+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
76+
}),
77+
},
78+
{
79+
timestamp: expect.any(Number),
80+
level: 'error',
81+
body: 'oh no',
82+
trace_id: expect.any(String),
83+
severity_number: 17,
84+
attributes: expect.objectContaining({
85+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
86+
'sentry.pino.level': { value: 50, type: 'integer' },
87+
err: { value: '{}', type: 'string' },
88+
'sentry.release': { value: '1.0', type: 'string' },
89+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
90+
}),
91+
},
92+
],
93+
},
94+
})
95+
.start()
96+
.completed();
97+
});
98+
99+
test('captures with Pino integrated channel', async () => {
100+
const instrumentPath = join(__dirname, 'instrument.mjs');
101+
102+
await createRunner(__dirname, 'scenario-next.mjs')
103+
.withMockSentryServer()
104+
.withInstrument(instrumentPath)
105+
.expect({
106+
event: {
107+
exception: {
108+
values: [
109+
{
110+
type: 'Error',
111+
value: 'oh no',
112+
mechanism: {
113+
type: 'pino',
114+
handled: true,
115+
},
116+
stacktrace: {
117+
frames: expect.arrayContaining([
118+
expect.objectContaining({
119+
function: '?',
120+
in_app: true,
121+
module: 'scenario-next',
122+
context_line: " logger.error(new Error('oh no'));",
123+
}),
124+
]),
125+
},
126+
},
127+
],
128+
},
129+
},
130+
})
131+
.expect({
132+
log: {
133+
items: [
134+
{
135+
timestamp: expect.any(Number),
136+
level: 'info',
137+
body: 'hello world',
138+
trace_id: expect.any(String),
139+
severity_number: 9,
140+
attributes: expect.objectContaining({
141+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
142+
'sentry.pino.level': { value: 30, type: 'integer' },
143+
user: { value: 'user-id', type: 'string' },
144+
something: {
145+
type: 'string',
146+
value: '{"more":3,"complex":"nope"}',
147+
},
148+
'sentry.release': { value: '1.0', type: 'string' },
149+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
150+
}),
151+
},
152+
{
153+
timestamp: expect.any(Number),
154+
level: 'error',
155+
body: 'oh no',
156+
trace_id: expect.any(String),
157+
severity_number: 17,
158+
attributes: expect.objectContaining({
159+
'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
160+
'sentry.pino.level': { value: 50, type: 'integer' },
161+
err: { value: '{}', type: 'string' },
162+
'sentry.release': { value: '1.0', type: 'string' },
163+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
164+
}),
165+
},
166+
],
167+
},
168+
})
169+
.start()
170+
.completed();
171+
});
172+
});

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export {
9494
onUnhandledRejectionIntegration,
9595
openAIIntegration,
9696
parameterize,
97+
pinoIntegration,
9798
postgresIntegration,
9899
postgresJsIntegration,
99100
prismaIntegration,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export {
106106
mysql2Integration,
107107
redisIntegration,
108108
tediousIntegration,
109+
pinoIntegration,
109110
postgresIntegration,
110111
postgresJsIntegration,
111112
prismaIntegration,

packages/core/src/utils/worldwide.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export type InternalGlobal = {
4848
*/
4949
_sentryModuleMetadata?: Record<string, any>;
5050
_sentryEsmLoaderHookRegistered?: boolean;
51+
_sentryInjectLoaderHookRegister?: () => void;
52+
_sentryInjectLoaderHookRegistered?: boolean;
5153
} & Carrier;
5254

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

packages/google-cloud-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export {
106106
mysql2Integration,
107107
redisIntegration,
108108
tediousIntegration,
109+
pinoIntegration,
109110
postgresIntegration,
110111
postgresJsIntegration,
111112
prismaIntegration,

0 commit comments

Comments
 (0)