Skip to content

Commit 70d58ad

Browse files
committed
Add otlpIntegration for @sentry/node-core/light/otlp
1 parent 1ec9937 commit 70d58ad

5 files changed

Lines changed: 306 additions & 9 deletions

File tree

‎packages/node-core/package.json‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@
5454
"require": {
5555
"default": "./build/cjs/init.js"
5656
}
57+
},
58+
"./light/otlp": {
59+
"import": {
60+
"types": "./build/types/light/integrations/otlpIntegration.d.ts",
61+
"default": "./build/esm/light/integrations/otlpIntegration.js"
62+
},
63+
"require": {
64+
"types": "./build/types/light/integrations/otlpIntegration.d.ts",
65+
"default": "./build/cjs/light/integrations/otlpIntegration.js"
66+
}
5767
}
5868
},
5969
"typesVersions": {
@@ -73,7 +83,8 @@
7383
"@opentelemetry/instrumentation": ">=0.57.1 <1",
7484
"@opentelemetry/resources": "^1.30.1 || ^2.1.0",
7585
"@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0",
76-
"@opentelemetry/semantic-conventions": "^1.39.0"
86+
"@opentelemetry/semantic-conventions": "^1.39.0",
87+
"@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1"
7788
},
7889
"peerDependenciesMeta": {
7990
"@opentelemetry/api": {
@@ -96,6 +107,9 @@
96107
},
97108
"@opentelemetry/semantic-conventions": {
98109
"optional": true
110+
},
111+
"@opentelemetry/exporter-trace-otlp-http": {
112+
"optional": true
99113
}
100114
},
101115
"dependencies": {
@@ -107,6 +121,7 @@
107121
"@opentelemetry/api": "^1.9.0",
108122
"@opentelemetry/context-async-hooks": "^2.6.0",
109123
"@opentelemetry/core": "^2.6.0",
124+
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
110125
"@opentelemetry/instrumentation": "^0.213.0",
111126
"@opentelemetry/resources": "^2.6.0",
112127
"@opentelemetry/sdk-trace-base": "^2.6.0",

‎packages/node-core/rollup.npm.config.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default [
1919
localVariablesWorkerConfig,
2020
...makeNPMConfigVariants(
2121
makeBaseNPMConfig({
22-
entrypoints: ['src/index.ts','src/init.ts','src/light/index.ts'],
22+
entrypoints: ['src/index.ts','src/init.ts','src/light/index.ts','src/light/integrations/otlpIntegration.ts'],
2323
packageSpecificConfig: {
2424
output: {
2525
// set exports to 'named' or 'auto' so that rollup doesn't warn
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import{trace}from'@opentelemetry/api';
2+
import{OTLPTraceExporter}from'@opentelemetry/exporter-trace-otlp-http';
3+
importtype{SpanExporter}from'@opentelemetry/sdk-trace-base';
4+
import{BasicTracerProvider,BatchSpanProcessor}from'@opentelemetry/sdk-trace-base';
5+
importtype{Client,IntegrationFn}from'@sentry/core';
6+
import{debug,defineIntegration,registerExternalPropagationContext,SENTRY_API_VERSION}from'@sentry/core';
7+
8+
interfaceOtlpIntegrationOptions{
9+
/**
10+
* Whether to set up the OTLP traces exporter that sends spans to Sentry.
11+
* Default: true
12+
*/
13+
setupOtlpTracesExporter?: boolean;
14+
15+
/**
16+
* URL of your own OpenTelemetry collector.
17+
* When set, the exporter will send traces to this URL instead of the Sentry OTLP endpoint derived from the DSN.
18+
* Default: undefined (uses DSN-derived endpoint)
19+
*/
20+
collectorUrl?: string;
21+
}
22+
23+
constINTEGRATION_NAME='OtlpIntegration';
24+
25+
const_otlpIntegration=((userOptions: OtlpIntegrationOptions={})=>{
26+
constoptions={
27+
setupOtlpTracesExporter: userOptions.setupOtlpTracesExporter??true,
28+
collectorUrl: userOptions.collectorUrl,
29+
};
30+
31+
let_spanProcessor: BatchSpanProcessor|undefined;
32+
let_tracerProvider: BasicTracerProvider|undefined;
33+
34+
return{
35+
name: INTEGRATION_NAME,
36+
37+
setup(_client: Client): void{
38+
// Always register external propagation context so that Sentry error/log events
39+
// are linked to the active OTel trace context.
40+
registerExternalPropagationContext(()=>{
41+
constactiveSpan=trace.getActiveSpan();
42+
if(!activeSpan){
43+
returnundefined;
44+
}
45+
constspanContext=activeSpan.spanContext();
46+
return{traceId: spanContext.traceId,spanId: spanContext.spanId};
47+
});
48+
49+
debug.log(`[${INTEGRATION_NAME}] External propagation context registered.`);
50+
},
51+
52+
afterAllSetup(client: Client): void{
53+
if(options.setupOtlpTracesExporter){
54+
setupTracesExporter(client);
55+
}
56+
},
57+
};
58+
59+
functionsetupTracesExporter(client: Client): void{
60+
letendpoint: string;
61+
letheaders: Record<string,string>|undefined;
62+
63+
if(options.collectorUrl){
64+
endpoint=options.collectorUrl;
65+
debug.log(`[${INTEGRATION_NAME}] Sending traces to collector at ${endpoint}`);
66+
}else{
67+
constdsn=client.getDsn();
68+
if(!dsn){
69+
debug.warn(`[${INTEGRATION_NAME}] No DSN found. OTLP exporter not set up.`);
70+
return;
71+
}
72+
73+
const{ protocol, host, port, path, projectId, publicKey }=dsn;
74+
75+
constbasePath=path ? `/${path}` : '';
76+
constportStr=port ? `:${port}` : '';
77+
endpoint=`${protocol}://${host}${portStr}${basePath}/api/${projectId}/integration/otlp/v1/traces/`;
78+
79+
constsdkInfo=client.getSdkMetadata()?.sdk;
80+
constsentryClient=sdkInfo ? `, sentry_client=${sdkInfo.name}/${sdkInfo.version}` : '';
81+
headers={
82+
'X-Sentry-Auth': `Sentry sentry_version=${SENTRY_API_VERSION}, sentry_key=${publicKey}${sentryClient}`,
83+
};
84+
}
85+
86+
letexporter: SpanExporter;
87+
try{
88+
exporter=newOTLPTraceExporter({
89+
url: endpoint,
90+
headers,
91+
});
92+
}catch(e){
93+
debug.warn(`[${INTEGRATION_NAME}] Failed to create OTLPTraceExporter:`,e);
94+
return;
95+
}
96+
97+
_spanProcessor=newBatchSpanProcessor(exporter);
98+
99+
// Add span processor to existing global tracer provider.
100+
// trace.getTracerProvider() returns a ProxyTracerProvider; unwrap it to get the real provider.
101+
constglobalProvider=trace.getTracerProvider();
102+
constdelegate=
103+
'getDelegate'inglobalProvider
104+
? (globalProviderasunknownas{getDelegate(): unknown}).getDelegate()
105+
: globalProvider;
106+
107+
// In OTel v2, addSpanProcessor was removed. We push into the internal _spanProcessors
108+
// array on the MultiSpanProcessor, which is how OTel's own forceFlush() accesses it.
109+
constactiveProcessor=(delegateasRecord<string,unknown>)?._activeSpanProcessoras
110+
|{_spanProcessors?: unknown[]}
111+
|undefined;
112+
if(activeProcessor?._spanProcessors){
113+
activeProcessor._spanProcessors.push(_spanProcessor);
114+
debug.log(`[${INTEGRATION_NAME}] Added span processor to existing TracerProvider.`);
115+
}else{
116+
// No user-configured provider; create a minimal one and set it as global
117+
_tracerProvider=newBasicTracerProvider({
118+
spanProcessors: [_spanProcessor],
119+
});
120+
trace.setGlobalTracerProvider(_tracerProvider);
121+
debug.log(`[${INTEGRATION_NAME}] Created new TracerProvider with OTLP span processor.`);
122+
}
123+
124+
client.on('flush',()=>{
125+
void_spanProcessor?.forceFlush();
126+
});
127+
128+
client.on('close',()=>{
129+
void_spanProcessor?.shutdown();
130+
void_tracerProvider?.shutdown();
131+
});
132+
}
133+
})satisfiesIntegrationFn;
134+
135+
/**
136+
* OTLP integration for the Sentry light SDK.
137+
*
138+
* Bridges an existing OpenTelemetry setup with Sentry by:
139+
* 1. Linking Sentry error/log events to the active OTel trace context
140+
* 2. Exporting OTel spans to Sentry via OTLP (or to a custom collector)
141+
*/
142+
exportconstotlpIntegration=defineIntegration(_otlpIntegration);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import{afterEach,describe,expect,it}from'vitest';
2+
import{otlpIntegration}from'../../../src/light/integrations/otlpIntegration';
3+
import{cleanupLightSdk,mockLightSdkInit}from'../../helpers/mockLightSdkInit';
4+
5+
describe('Light Mode | otlpIntegration',()=>{
6+
afterEach(()=>{
7+
cleanupLightSdk();
8+
});
9+
10+
it('has correct integration name',()=>{
11+
constintegration=otlpIntegration();
12+
expect(integration.name).toBe('OtlpIntegration');
13+
});
14+
15+
it('accepts empty options',()=>{
16+
constintegration=otlpIntegration();
17+
expect(integration.name).toBe('OtlpIntegration');
18+
});
19+
20+
it('accepts all options',()=>{
21+
constintegration=otlpIntegration({
22+
setupOtlpTracesExporter: false,
23+
collectorUrl: 'https://my-collector.example.com/v1/traces',
24+
});
25+
expect(integration.name).toBe('OtlpIntegration');
26+
});
27+
28+
describe('endpoint construction',()=>{
29+
it('constructs correct endpoint from DSN',()=>{
30+
constclient=mockLightSdkInit({
31+
integrations: [otlpIntegration()],
32+
});
33+
34+
constdsn=client?.getDsn();
35+
expect(dsn).toBeDefined();
36+
expect(dsn?.host).toBe('domain');
37+
expect(dsn?.projectId).toBe('123');
38+
});
39+
40+
it('handles DSN with port and path',()=>{
41+
constclient=mockLightSdkInit({
42+
dsn: 'https://key@sentry.example.com:9000/mypath/456',
43+
integrations: [otlpIntegration()],
44+
});
45+
46+
constdsn=client?.getDsn();
47+
expect(dsn?.host).toBe('sentry.example.com');
48+
expect(dsn?.port).toBe('9000');
49+
expect(dsn?.path).toBe('mypath');
50+
expect(dsn?.projectId).toBe('456');
51+
});
52+
});
53+
54+
describe('auth header',()=>{
55+
it('constructs correct X-Sentry-Auth header format with sentry_client',()=>{
56+
constclient=mockLightSdkInit({
57+
integrations: [otlpIntegration()],
58+
});
59+
60+
constdsn=client?.getDsn();
61+
expect(dsn?.publicKey).toBe('username');
62+
63+
constsdkInfo=client?.getSdkMetadata()?.sdk;
64+
expect(sdkInfo?.name).toBe('sentry.javascript.node-light');
65+
expect(sdkInfo?.version).toBeDefined();
66+
67+
constexpectedAuth=`Sentry sentry_version=7, sentry_key=${dsn?.publicKey}, sentry_client=${sdkInfo?.name}/${sdkInfo?.version}`;
68+
expect(expectedAuth).toMatch(
69+
/^Sentrysentry_version=7,sentry_key=username,sentry_client=sentry\.javascript\.node-light\/.+$/,
70+
);
71+
});
72+
});
73+
});

0 commit comments

Comments
 (0)