@ogcio/o11y-sdk-node 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,6 +129,109 @@ instrumentNode({
129
129
  });
130
130
  ```
131
131
 
132
+ ## Extension Points
133
+
134
+ The SDK provides extension points for integrating with third-party observability vendors (e.g. Sentry) or adding custom processing logic.
135
+
136
+ ### Span Processors
137
+
138
+ Add custom span processors that run before the default OTLP exporters:
139
+
140
+ ```typescript
141
+ import { instrumentNode } from "@ogcio/o11y-sdk-node";
142
+ import { MyCustomSpanProcessor } from "./my-processor";
143
+
144
+ await instrumentNode({
145
+ collectorUrl: "http://localhost:4317",
146
+ serviceName: "my-service",
147
+
148
+ // Processors that run before OTLP export (e.g., vendor processors)
149
+ prependSpanProcessors: [new MyCustomSpanProcessor()],
150
+ });
151
+ ```
152
+
153
+ ### Log Processors
154
+
155
+ Similarly, add custom log record processors:
156
+
157
+ ```typescript
158
+ await instrumentNode({
159
+ collectorUrl: "http://localhost:4317",
160
+ serviceName: "my-service",
161
+
162
+ prependLogProcessors: [new MyLogProcessor()],
163
+ });
164
+ ```
165
+
166
+ ### Custom Sampler
167
+
168
+ Wrap or replace the default sampler using `samplerWrapper`:
169
+
170
+ ```typescript
171
+ import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
172
+
173
+ await instrumentNode({
174
+ collectorUrl: "http://localhost:4317",
175
+ serviceName: "my-service",
176
+
177
+ // Wrap the default sampler with custom logic
178
+ samplerWrapper: (_defaultSampler) => {
179
+ // Discard the default sampler and return your custom sampler, or wrap it and leverage it as default
180
+ return new MyCustomSampler();
181
+ },
182
+ });
183
+ ```
184
+
185
+ ### Custom Context Manager
186
+
187
+ Override the default context manager:
188
+
189
+ ```typescript
190
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
191
+
192
+ await instrumentNode({
193
+ collectorUrl: "http://localhost:4317",
194
+ serviceName: "my-service",
195
+
196
+ contextManager: new AsyncLocalStorageContextManager(), //(the current default)
197
+ });
198
+ ```
199
+
200
+ ### Additional Propagators
201
+
202
+ Add propagators to the composite propagator (W3CTraceContextPropagator is always included):
203
+
204
+ ```typescript
205
+ import { B3Propagator } from "@opentelemetry/propagator-b3";
206
+
207
+ await instrumentNode({
208
+ collectorUrl: "http://localhost:4317",
209
+ serviceName: "my-service",
210
+
211
+ additionalPropagators: [new B3Propagator()],
212
+ });
213
+ ```
214
+
215
+ ### Post-Start Callback
216
+
217
+ Execute code after the SDK has started:
218
+
219
+ ```typescript
220
+ await instrumentNode({
221
+ collectorUrl: "http://localhost:4317",
222
+ serviceName: "my-service",
223
+
224
+ onSdkStarted: (sdk) => {
225
+ console.log("SDK started successfully");
226
+ // Perform vendor-specific validation, register additional hooks, etc.
227
+ },
228
+ });
229
+ ```
230
+
231
+ ## Vendor Presets
232
+
233
+ For common integrations, the SDK provides preset functions that configure all extension points automatically. See [Presets Documentation](./lib/presets/README.md) for details.
234
+
132
235
  ## Utils:
133
236
 
134
237
  - ## `withSpan`:
@@ -357,13 +460,19 @@ export type SDKLogLevel =
357
460
 
358
461
  ### Config
359
462
 
360
- | Parameter | Type | Description |
361
- | :--------------------------- | :------------------------- | :---------------------------------------------------------------------------------------------------------------- |
362
- | `collectorUrl` | `string` | **Required**. The opentelemetry collector entrypoint url, if null, instrumentation will not be activated |
363
- | `serviceName` | `string` | Name of your application used for the collector to group logs |
364
- | `diagLogLevel` | `SDKLogLevel` | Diagnostic log level for the internal runtime instrumentation |
365
- | `collectorMode` | `single \| batch` | Signals sending mode, default is batch for performance |
366
- | `enableFS` | `boolean` | **Deprecated**. Use `autoInstrumentationConfig` instead. Flag to enable or disable the tracing for node:fs module |
367
- | `autoInstrumentationConfig` | `InstrumentationConfigMap` | Configuration object for auto instrumentations. Default: `{"@opentelemetry/instrumentation-fs":{enabled:false}}` |
368
- | `additionalInstrumentations` | `Instrumentation[]` | Additional custom instrumentations to be added to the NodeSDK. Default: `[]` |
369
- | `protocol` | `string` | Type of the protocol used to send signals |
463
+ | Parameter | Type | Description |
464
+ | :--------------------------- | :------------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
465
+ | `collectorUrl` | `string` | **Required**. The opentelemetry collector entrypoint url, if null, instrumentation will not be activated |
466
+ | `serviceName` | `string` | Name of your application used for the collector to group logs |
467
+ | `diagLogLevel` | `SDKLogLevel` | Diagnostic log level for the internal runtime instrumentation |
468
+ | `collectorMode` | `single \| batch` | Signals sending mode, default is batch for performance |
469
+ | `enableFS` | `boolean` | **Deprecated**. Use `autoInstrumentationConfig` instead. Flag to enable or disable the tracing for node:fs module |
470
+ | `autoInstrumentationConfig` | `InstrumentationConfigMap` | Configuration object for auto instrumentations. Default: `{"@opentelemetry/instrumentation-fs":{enabled:false}}` |
471
+ | `additionalInstrumentations` | `Instrumentation[]` | Additional custom instrumentations to be added to the NodeSDK. Default: `[]` |
472
+ | `protocol` | `string` | Type of the protocol used to send signals |
473
+ | `prependSpanProcessors` | `SpanProcessor[]` | Span processors to run before OTLP export. Default: `[]` |
474
+ | `prependLogProcessors` | `LogRecordProcessor[]` | Log processors to run before OTLP export. Default: `[]` |
475
+ | `samplerWrapper` | `(defaultSampler: Sampler) => Sampler` | Function to wrap or replace the default sampler |
476
+ | `contextManager` | `ContextManager` | Custom context manager to use |
477
+ | `additionalPropagators` | `TextMapPropagator[]` | Additional propagators to add to the composite propagator. Default: `[]` |
478
+ | `onSdkStarted` | `(sdk: NodeSDK) => void` | Callback invoked after the SDK has started |
@@ -1,17 +1,44 @@
1
1
  import { BasicRedactor } from "./basic-redactor.js";
2
2
  export class IpRedactor extends BasicRedactor {
3
- static IPV4_REGEX = /(?<!\d)(?:%[0-9A-Fa-f]{2})?(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?:%[0-9A-Fa-f]{2})?(?!\d)/gi;
4
- static IPV6_REGEX = /(?<![0-9a-f:])(?:%[0-9A-Fa-f]{2})?((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|:(?::[0-9A-Fa-f]{1,4}){1,7}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}|:(?::[0-9A-Fa-f]{1,4}){1,7}:?|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?:%[0-9A-Fa-f]{2})?(?![0-9a-f:])/gi;
3
+ // iOS < 16.6 compatible: no lookbehind/lookahead
4
+ static IPV4_REGEX = /(?:%[0-9A-Fa-f]{2})?(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?:%[0-9A-Fa-f]{2})?/gi;
5
+ // Comprehensive IPv6 regex without lookbehind - matches all standard formats including :: compression
6
+ static IPV6_REGEX = /(?:%[0-9A-Fa-f]{2})?((?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|::(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}|[0-9A-Fa-f]{1,4}::(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){2}::(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){3}::(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){4}::(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){5}::(?:[0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){6}::[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3})(?:%[0-9A-Fa-f]{2})?/gi;
5
7
  redact(value) {
6
8
  const counters = {};
7
- const redactedValue = value
8
- .replace(IpRedactor.IPV4_REGEX, () => {
9
- counters["IPv4"] = (counters["IPv4"] || 0) + 1;
10
- return "[REDACTED IPV4]";
11
- })
12
- .replace(IpRedactor.IPV6_REGEX, () => {
13
- counters["IPv6"] = (counters["IPv6"] || 0) + 1;
14
- return "[REDACTED IPV6]";
9
+ // First pass: redact IPv4 with boundary checking
10
+ const tempValue = value.replace(IpRedactor.IPV4_REGEX, (match, ...args) => {
11
+ const offset = args[args.length - 2];
12
+ const before = offset > 0 ? value[offset - 1] : "";
13
+ const after = offset + match.length < value.length
14
+ ? value[offset + match.length]
15
+ : "";
16
+ // Check if surrounded by non-digit characters
17
+ const isValidBefore = !before || !/\d/.test(before);
18
+ const isValidAfter = !after || !/\d/.test(after);
19
+ if (isValidBefore && isValidAfter) {
20
+ counters["IPv4"] = (counters["IPv4"] || 0) + 1;
21
+ return "[REDACTED IPV4]";
22
+ }
23
+ return match;
24
+ });
25
+ // Second pass: redact IPv6 with boundary checking
26
+ const redactedValue = tempValue.replace(IpRedactor.IPV6_REGEX, (match, ...args) => {
27
+ const offset = args[args.length - 2];
28
+ const before = offset > 0 ? tempValue[offset - 1] : "";
29
+ const after = offset + match.length < tempValue.length
30
+ ? tempValue[offset + match.length]
31
+ : "";
32
+ // For IPv6:
33
+ // Reject if there's a colon before (indicating :::)
34
+ // Reject if there's a hex digit or colon after
35
+ const isValidBefore = !before || !/[0-9a-f:]/i.test(before);
36
+ const isValidAfter = !after || !/[0-9a-f:]/i.test(after);
37
+ if (isValidBefore && isValidAfter) {
38
+ counters["IPv6"] = (counters["IPv6"] || 0) + 1;
39
+ return "[REDACTED IPV6]";
40
+ }
41
+ return match;
15
42
  });
16
43
  return {
17
44
  redactedValue: redactedValue,
@@ -1,11 +1,23 @@
1
1
  import { BasicRedactor } from "./basic-redactor.js";
2
2
  export class PpsnRedactor extends BasicRedactor {
3
- static PPSN_REGEX = /(?<!([0-9]|[a-z]))[0-9]{7}[a-z]{1,2}(?!([0-9]|[a-z]))/gi;
3
+ // iOS < 16.6 compatible: no lookbehind/lookahead
4
+ static PPSN_REGEX = /[0-9]{7}[a-z]{1,2}/gi;
4
5
  redact(value) {
5
6
  let redactedCounter = 0;
6
- const redactedValue = value.replace(PpsnRedactor.PPSN_REGEX, (_) => {
7
- redactedCounter++;
8
- return "[REDACTED PPSN]";
7
+ // Boundary checking to replace lookbehind/lookahead
8
+ const redactedValue = value.replace(PpsnRedactor.PPSN_REGEX, (match, offset) => {
9
+ const before = offset > 0 ? value[offset - 1] : "";
10
+ const after = offset + match.length < value.length
11
+ ? value[offset + match.length]
12
+ : "";
13
+ // Check if surrounded by non-alphanumeric characters
14
+ const isValidBefore = !before || !/[0-9a-z]/i.test(before);
15
+ const isValidAfter = !after || !/[0-9a-z]/i.test(after);
16
+ if (isValidBefore && isValidAfter) {
17
+ redactedCounter++;
18
+ return "[REDACTED PPSN]";
19
+ }
20
+ return match;
9
21
  });
10
22
  return {
11
23
  redactedValue: redactedValue,
@@ -1,5 +1,6 @@
1
1
  import type { NodeSDK } from "@opentelemetry/sdk-node";
2
2
  import buildNodeInstrumentation from "./lib/instrumentation.node.js";
3
+ export type { SpanKind, SpanStatus, SpanStatusCode, TraceFlags, } from "@opentelemetry/api";
3
4
  export type * from "./lib/index.js";
4
5
  export type { NodeSDK };
5
6
  export { buildNodeInstrumentation as instrumentNode };
@@ -1,7 +1,11 @@
1
1
  import type { Metadata } from "@grpc/grpc-js";
2
- import { BasicRedactor } from "../../sdk-core/lib/redaction/basic-redactor.js";
2
+ import { BasicRedactor } from "../../sdk-core/lib/index.js";
3
3
  import { InstrumentationConfigMap } from "@opentelemetry/auto-instrumentations-node";
4
4
  import { Instrumentation } from "@opentelemetry/instrumentation";
5
+ import { Sampler, SpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
7
+ import { ContextManager, TextMapPropagator } from "@opentelemetry/api";
8
+ import { NodeSDK } from "@opentelemetry/sdk-node";
5
9
  export interface NodeSDKConfig {
6
10
  /**
7
11
  * The opentelemetry collector entrypoint GRPC url.
@@ -127,6 +131,43 @@ export interface NodeSDKConfig {
127
131
  */
128
132
  enabled: boolean;
129
133
  };
134
+ /**
135
+ * Additional span processors to prepend to the processing pipeline.
136
+ * These run before the default OTLP exporters.
137
+ * You may use this for vendor-specific processors (e.g. sentry).
138
+ * @default []
139
+ */
140
+ prependSpanProcessors?: SpanProcessor[];
141
+ /**
142
+ * Additional log record processors to prepend to the pipeline.
143
+ * @default []
144
+ */
145
+ prependLogProcessors?: LogRecordProcessor[];
146
+ /**
147
+ * Custom sampler factory function.
148
+ * Receives the default o11y sampler and should return the sampler to use.
149
+ * You may use this to wrap the default sampler with vendor-specific samplers.
150
+ *
151
+ * @default undefined (uses default sampler)
152
+ */
153
+ samplerWrapper?: (defaultSampler: Sampler) => Sampler;
154
+ /**
155
+ * Custom context manager to use, required by some vendors (e.g. sentry requires its own SentryContextManager).
156
+ * @default undefined (uses AsyncLocalStorageContextManager from otel)
157
+ */
158
+ contextManager?: ContextManager;
159
+ /**
160
+ * Additional propagators to include in the composite propagator.
161
+ * W3CTraceContextPropagator is always included by default.
162
+ * @default []
163
+ */
164
+ additionalPropagators?: TextMapPropagator[];
165
+ /**
166
+ * Callback function that is invoked once the SDK has been started.
167
+ *
168
+ * @param sdk The started NodeSDK instance.
169
+ */
170
+ onSdkStarted?: (sdk: NodeSDK) => void;
130
171
  }
131
172
  export interface SamplerCondition {
132
173
  type: "endsWith" | "includes" | "equals";
@@ -1,6 +1,6 @@
1
1
  import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
2
2
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
- import { W3CTraceContextPropagator } from "@opentelemetry/core";
3
+ import { CompositePropagator, W3CTraceContextPropagator, } from "@opentelemetry/core";
4
4
  import { NodeSDK } from "@opentelemetry/sdk-node";
5
5
  import { AlwaysOffSampler, ParentBasedSampler, TraceIdRatioBasedSampler, } from "@opentelemetry/sdk-trace-base";
6
6
  import { setNodeSdkConfig } from "./config-manager.js";
@@ -25,14 +25,19 @@ export default async function buildNodeInstrumentation(config) {
25
25
  }
26
26
  // Init configManager to make it available to all o11y utils.
27
27
  setNodeSdkConfig(config);
28
+ // Build base sampler
28
29
  const urlSampler = new UrlSampler(config.ignoreUrls, new TraceIdRatioBasedSampler(config.traceRatio ?? 1));
29
- const mainSampler = new ParentBasedSampler({
30
+ const defaultSampler = new ParentBasedSampler({
30
31
  root: urlSampler,
31
32
  remoteParentSampled: urlSampler,
32
33
  remoteParentNotSampled: new AlwaysOffSampler(),
33
34
  localParentSampled: urlSampler,
34
35
  localParentNotSampled: new AlwaysOffSampler(),
35
36
  });
37
+ // Allow sampler customization via config
38
+ const finalSampler = config.samplerWrapper
39
+ ? config.samplerWrapper(defaultSampler)
40
+ : defaultSampler;
36
41
  diag.setLogger(new DiagConsoleLogger(), config.diagLogLevel ? DiagLogLevel[config.diagLogLevel] : DiagLogLevel.INFO);
37
42
  try {
38
43
  let exporter;
@@ -45,16 +50,32 @@ export default async function buildNodeInstrumentation(config) {
45
50
  else {
46
51
  exporter = await buildGrpcExporters(config);
47
52
  }
53
+ // Build span processors with custom processors from config
54
+ const spanProcessors = [
55
+ ...(config.prependSpanProcessors ?? []),
56
+ ...exporter.spans,
57
+ ];
58
+ // Build log processors with custom processors from config
59
+ const logProcessors = [
60
+ ...(config.prependLogProcessors ?? []),
61
+ ...exporter.logs,
62
+ ];
48
63
  const sdk = new NodeSDK({
49
64
  resourceDetectors: [
50
65
  new ObservabilityResourceDetector(config.resourceAttributes),
51
66
  ],
52
- spanProcessors: exporter.spans,
67
+ spanProcessors,
53
68
  serviceName: config.serviceName,
54
69
  metricReaders: [exporter.metrics],
55
- logRecordProcessors: exporter.logs,
56
- sampler: mainSampler,
57
- textMapPropagator: new W3CTraceContextPropagator(),
70
+ logRecordProcessors: logProcessors,
71
+ sampler: finalSampler,
72
+ textMapPropagator: new CompositePropagator({
73
+ propagators: [
74
+ new W3CTraceContextPropagator(),
75
+ ...(config.additionalPropagators ?? []),
76
+ ],
77
+ }),
78
+ contextManager: config.contextManager,
58
79
  instrumentations: [
59
80
  getNodeAutoInstrumentations(config.autoInstrumentationConfig ?? {
60
81
  "@opentelemetry/instrumentation-fs": {
@@ -67,6 +88,7 @@ export default async function buildNodeInstrumentation(config) {
67
88
  sdk.start();
68
89
  console.log("NodeJS OpenTelemetry instrumentation started successfully.");
69
90
  _shutdownHook(sdk);
91
+ config.onSdkStarted?.(sdk);
70
92
  return sdk;
71
93
  }
72
94
  catch (error) {
@@ -0,0 +1,2 @@
1
+ export * from "./sentry-next.js";
2
+ export * from "./sentry-node.js";
@@ -0,0 +1,2 @@
1
+ export * from "./sentry-next.js";
2
+ export * from "./sentry-node.js";
@@ -0,0 +1,29 @@
1
+ import type { NodeSDKConfig } from "../index.js";
2
+ import type { NodeOptions as SentryNextjsOptions } from "@sentry/nextjs";
3
+ /**
4
+ * Options for the withSentryNextjs preset function.
5
+ */
6
+ export interface WithSentryNextjsOptions {
7
+ /**
8
+ * Sentry-specific configuration options for Next.js.
9
+ * Passed to Sentry.init() with skipOpenTelemetrySetup: true.
10
+ *
11
+ * Uses NodeOptions from @sentry/nextjs (server-side configuration).
12
+ */
13
+ sentryOptions?: SentryNextjsOptions;
14
+ /**
15
+ * If true, Sentry's sampling logic (tracesSampleRate/tracesSampler) takes precedence.
16
+ * If false, o11y's sampler handles all decisions and Sentry receives all sampled traces.
17
+ *
18
+ * @default false
19
+ */
20
+ useSentrySampling?: boolean;
21
+ }
22
+ /**
23
+ * Enhances a NodeSDKConfig with Sentry integration for Next.js server-side.
24
+ *
25
+ * includes Next.js-specific integrations
26
+ * filters Next.js internal spans (static assets, _next routes, etc...)
27
+ * nextjs proper source map resolution
28
+ */
29
+ export declare function withSentryNextjs(config: NodeSDKConfig, options?: WithSentryNextjsOptions): Promise<NodeSDKConfig>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Enhances a NodeSDKConfig with Sentry integration for Next.js server-side.
3
+ *
4
+ * includes Next.js-specific integrations
5
+ * filters Next.js internal spans (static assets, _next routes, etc...)
6
+ * nextjs proper source map resolution
7
+ */
8
+ export async function withSentryNextjs(config, options = {}) {
9
+ const { sentryOptions = {}, useSentrySampling = false } = options;
10
+ const components = await buildSentryNextjsComponents(sentryOptions, useSentrySampling);
11
+ return {
12
+ ...config,
13
+ // Prepend SentrySpanProcessor so it processes spans before OTLP export
14
+ prependSpanProcessors: [
15
+ components.spanProcessor,
16
+ ...(config.prependSpanProcessors ?? []),
17
+ ],
18
+ // Wrap sampler with SentrySampler, using o11y's sampler as fallback
19
+ samplerWrapper: (defaultSampler) => {
20
+ // Chain with any existing wrapper
21
+ const baseSampler = config.samplerWrapper
22
+ ? config.samplerWrapper(defaultSampler)
23
+ : defaultSampler;
24
+ return components.sampler(baseSampler);
25
+ },
26
+ // Add SentryPropagator for distributed tracing
27
+ additionalPropagators: [
28
+ components.propagator,
29
+ ...(config.additionalPropagators ?? []),
30
+ ],
31
+ // Use SentryContextManager for proper async context tracking
32
+ contextManager: config.contextManager ?? components.contextManager,
33
+ // Validate Sentry setup after SDK starts
34
+ onSdkStarted: (sdk) => {
35
+ components.validate();
36
+ config.onSdkStarted?.(sdk);
37
+ },
38
+ };
39
+ }
40
+ /**
41
+ * Builds Sentry OTEL components for Next.js by dynamically importing @sentry packages.
42
+ * This keeps @sentry/* as optional peer dependencies.
43
+ */
44
+ async function buildSentryNextjsComponents(sentryOptions, useSentrySampling = false) {
45
+ // Dynamic imports to keep @sentry/* as optional peer dependencies
46
+ const Sentry = await import("@sentry/nextjs");
47
+ const SentryOtel = await import("@sentry/opentelemetry");
48
+ // Destructure common options to be defaulted, pass rest to Sentry.init
49
+ const { dsn, environment, tracesSampleRate, tracesSampler, integrations, sendDefaultPii, enableLogs, ...restOptions } = sentryOptions;
50
+ Sentry.init({
51
+ dsn: dsn ?? process.env.SENTRY_DSN,
52
+ environment: environment ?? process.env.SENTRY_ENV ?? process.env.NODE_ENV,
53
+ skipOpenTelemetrySetup: true,
54
+ // Sentry Next.js defaults (which include distDirRewriteFramesIntegration, httpIntegration, etc...)
55
+ integrations: integrations ?? [],
56
+ tracesSampleRate: useSentrySampling ? (tracesSampleRate ?? 1.0) : 1.0,
57
+ tracesSampler: useSentrySampling ? tracesSampler : undefined,
58
+ sendDefaultPii: sendDefaultPii ?? false,
59
+ enableLogs: enableLogs ?? true,
60
+ // Pass through any additional options
61
+ ...restOptions,
62
+ });
63
+ const sentryClient = Sentry.getClient();
64
+ if (!sentryClient) {
65
+ console.warn("[@ogcio/o11y-sdk-node] Unable to initialize Sentry client for Next.js. Telemetry may not work as expected.");
66
+ }
67
+ return {
68
+ spanProcessor: new SentryOtel.SentrySpanProcessor(),
69
+ sampler: (fallbackSampler) => sentryClient
70
+ ? new SentryOtel.SentrySampler(sentryClient)
71
+ : fallbackSampler,
72
+ propagator: new SentryOtel.SentryPropagator(),
73
+ contextManager: new Sentry.SentryContextManager(),
74
+ validate: Sentry.validateOpenTelemetrySetup,
75
+ };
76
+ }
@@ -0,0 +1,25 @@
1
+ import { NodeSDKConfig } from "../index.js";
2
+ import type { NodeOptions as SentryNodeOptions } from "@sentry/node";
3
+ /**
4
+ * Options for the withSentry preset function.
5
+ */
6
+ export interface WithSentryOptions {
7
+ /**
8
+ * Sentry-specific configuration options.
9
+ * Passed to Sentry.init() with skipOpenTelemetrySetup: true.
10
+ */
11
+ sentryOptions?: SentryNodeOptions;
12
+ /**
13
+ * If true, Sentry's sampling logic (tracesSampleRate/tracesSampler) takes precedence.
14
+ * If false, o11y's sampler handles all decisions and Sentry receives all sampled traces.
15
+ *
16
+ * @default false
17
+ */
18
+ useSentrySampling?: boolean;
19
+ }
20
+ /**
21
+ * Enhances a NodeSDKConfig with Sentry for Node integration.
22
+ *
23
+ * Includes sentry own processors and context management.
24
+ */
25
+ export declare function withSentry(config: NodeSDKConfig, options?: WithSentryOptions): Promise<NodeSDKConfig>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Enhances a NodeSDKConfig with Sentry for Node integration.
3
+ *
4
+ * Includes sentry own processors and context management.
5
+ */
6
+ export async function withSentry(config, options = {}) {
7
+ const { sentryOptions = {}, useSentrySampling = true } = options;
8
+ const components = await buildSentryComponents(sentryOptions, useSentrySampling);
9
+ return {
10
+ ...config,
11
+ prependSpanProcessors: [
12
+ components.spanProcessor,
13
+ ...(config.prependSpanProcessors ?? []),
14
+ ],
15
+ // Wrap sampler with SentrySampler, using o11y's sampler as fallback
16
+ samplerWrapper: (defaultSampler) => {
17
+ // Chain with any existing wrapper
18
+ const baseSampler = config.samplerWrapper
19
+ ? config.samplerWrapper(defaultSampler)
20
+ : defaultSampler;
21
+ return components.sampler(baseSampler);
22
+ },
23
+ additionalPropagators: [
24
+ components.propagator,
25
+ ...(config.additionalPropagators ?? []),
26
+ ],
27
+ // Sentry own async context tracking
28
+ contextManager: config.contextManager ?? components.contextManager,
29
+ onSdkStarted: components.validate,
30
+ };
31
+ }
32
+ /**
33
+ * Builds Sentry OTEL components by dynamically importing @sentry packages.
34
+ * This keeps @sentry/* as optional peer dependencies.
35
+ */
36
+ async function buildSentryComponents(sentryOptions, useSentrySampling = false) {
37
+ // Dynamic imports to keep @sentry/* as optional peer dependencies
38
+ const Sentry = await import("@sentry/node");
39
+ const SentryOtel = await import("@sentry/opentelemetry");
40
+ // Destructure common options to be defaulted, pass rest to Sentry.init
41
+ const { dsn, environment, tracesSampleRate, tracesSampler, integrations, sendDefaultPii, enableLogs, ...restOptions } = sentryOptions;
42
+ // Initialize Sentry with skipOpenTelemetrySetup
43
+ Sentry.init({
44
+ dsn: dsn ?? process.env.SENTRY_DSN,
45
+ environment: environment ?? process.env.SENTRY_ENV ?? process.env.NODE_ENV,
46
+ skipOpenTelemetrySetup: true,
47
+ integrations: integrations ?? [
48
+ Sentry.redisIntegration(),
49
+ Sentry.extraErrorDataIntegration(),
50
+ Sentry.postgresIntegration(),
51
+ Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
52
+ ],
53
+ tracesSampleRate: useSentrySampling ? (tracesSampleRate ?? 1.0) : 1.0,
54
+ tracesSampler: useSentrySampling ? tracesSampler : undefined,
55
+ sendDefaultPii: sendDefaultPii ?? false,
56
+ enableLogs: enableLogs ?? true,
57
+ ...restOptions,
58
+ });
59
+ const sentryClient = Sentry.getClient();
60
+ if (!sentryClient) {
61
+ console.warn("Unable to initialize Sentry client. Telemetry may not work as expected.");
62
+ }
63
+ return {
64
+ spanProcessor: new SentryOtel.SentrySpanProcessor(),
65
+ sampler: (fallbackSampler) => sentryClient
66
+ ? new SentryOtel.SentrySampler(sentryClient)
67
+ : fallbackSampler,
68
+ propagator: new SentryOtel.SentryPropagator(),
69
+ contextManager: new Sentry.SentryContextManager(),
70
+ validate: Sentry.validateOpenTelemetrySetup,
71
+ };
72
+ }
@@ -1,6 +1,5 @@
1
- import { Context } from "@opentelemetry/api";
2
1
  import { Span } from "@opentelemetry/sdk-trace-base";
3
2
  import { OnEndingHookSpanProcessor } from "./on-ending-hook-span-processor.js";
4
3
  export declare class NextJsSpanProcessor extends OnEndingHookSpanProcessor {
5
- onEnding(span: Span, _context: Context): void;
4
+ onEnding(span: Span): void;
6
5
  }
@@ -1,7 +1,7 @@
1
1
  import { SpanStatusCode } from "@opentelemetry/api";
2
2
  import { OnEndingHookSpanProcessor } from "./on-ending-hook-span-processor.js";
3
3
  export class NextJsSpanProcessor extends OnEndingHookSpanProcessor {
4
- onEnding(span, _context) {
4
+ onEnding(span) {
5
5
  // ref. https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/quickstart/nextjs/#instrument-the-nextjs-backend
6
6
  if (span.name.startsWith("GET /_next/static")) {
7
7
  span.updateName("GET /_next/static");
@@ -27,9 +27,8 @@ export declare abstract class OnEndingHookSpanProcessor implements SpanProcessor
27
27
  /**
28
28
  * Hook method called just before a span is ended.
29
29
  * @param span The span that is about to end.
30
- * @param context The context in which the span is ending.
31
30
  */
32
- abstract onEnding(span: Span, context: Context): void;
31
+ abstract onEnding(span: Span): void;
33
32
  onStart(span: Span, _context: Context): void;
34
33
  onEnd(_span: ReadableSpan): void;
35
34
  forceFlush(): Promise<void>;
@@ -27,7 +27,7 @@ export class OnEndingHookSpanProcessor {
27
27
  const originalEnd = span.end.bind(span);
28
28
  // Wrap the end method to inject custom logic before ending the span
29
29
  span.end = (endTime) => {
30
- this.onEnding(span, _context);
30
+ this.onEnding(span);
31
31
  originalEnd(endTime);
32
32
  };
33
33
  }
@@ -11,7 +11,7 @@ import { OnEndingHookSpanProcessor } from "./on-ending-hook-span-processor.js";
11
11
  export declare class PseudoProfilingSpanProcessor extends OnEndingHookSpanProcessor {
12
12
  private _spanProfilingData;
13
13
  onStart(span: Span, _context: Context): void;
14
- onEnding(span: Span, _context: Context): void;
14
+ onEnding(span: Span): void;
15
15
  /**
16
16
  * Captures profiling data and sets attributes on the span.
17
17
  * Called just before the span is ended, while it's still writable.
@@ -19,7 +19,7 @@ export class PseudoProfilingSpanProcessor extends OnEndingHookSpanProcessor {
19
19
  memoryUsageStart,
20
20
  });
21
21
  }
22
- onEnding(span, _context) {
22
+ onEnding(span) {
23
23
  this._captureProfilingData(span);
24
24
  }
25
25
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "exports": {
19
19
  ".": "./dist/sdk-node/index.js",
20
- "./*": "./dist/*.js"
20
+ "./*": "./dist/*.js",
21
+ "./presets/*": "./dist/sdk-node/lib/presets/*.js"
21
22
  },
22
23
  "files": [
23
24
  "dist"
@@ -35,31 +36,48 @@
35
36
  "dependencies": {
36
37
  "@grpc/grpc-js": "1.14.3",
37
38
  "@opentelemetry/api": "1.9.0",
38
- "@opentelemetry/api-logs": "0.208.0",
39
- "@opentelemetry/auto-instrumentations-node": "0.67.2",
40
- "@opentelemetry/core": "2.2.0",
41
- "@opentelemetry/exporter-logs-otlp-grpc": "0.208.0",
42
- "@opentelemetry/exporter-logs-otlp-http": "0.208.0",
43
- "@opentelemetry/exporter-metrics-otlp-grpc": "0.208.0",
44
- "@opentelemetry/exporter-metrics-otlp-http": "0.208.0",
45
- "@opentelemetry/exporter-trace-otlp-grpc": "0.208.0",
46
- "@opentelemetry/exporter-trace-otlp-http": "0.208.0",
47
- "@opentelemetry/instrumentation": "0.208.0",
48
- "@opentelemetry/otlp-exporter-base": "0.208.0",
49
- "@opentelemetry/resources": "2.2.0",
50
- "@opentelemetry/sdk-logs": "0.208.0",
51
- "@opentelemetry/sdk-metrics": "2.2.0",
52
- "@opentelemetry/sdk-node": "0.208.0",
53
- "@opentelemetry/sdk-trace-base": "2.2.0"
39
+ "@opentelemetry/api-logs": "0.211.0",
40
+ "@opentelemetry/auto-instrumentations-node": "0.69.0",
41
+ "@opentelemetry/context-async-hooks": "2.5.0",
42
+ "@opentelemetry/core": "2.5.0",
43
+ "@opentelemetry/exporter-logs-otlp-grpc": "0.211.0",
44
+ "@opentelemetry/exporter-logs-otlp-http": "0.211.0",
45
+ "@opentelemetry/exporter-metrics-otlp-grpc": "0.211.0",
46
+ "@opentelemetry/exporter-metrics-otlp-http": "0.211.0",
47
+ "@opentelemetry/exporter-trace-otlp-grpc": "0.211.0",
48
+ "@opentelemetry/exporter-trace-otlp-http": "0.211.0",
49
+ "@opentelemetry/instrumentation": "0.211.0",
50
+ "@opentelemetry/otlp-exporter-base": "0.211.0",
51
+ "@opentelemetry/resources": "2.5.0",
52
+ "@opentelemetry/sdk-logs": "0.211.0",
53
+ "@opentelemetry/sdk-metrics": "2.5.0",
54
+ "@opentelemetry/sdk-node": "0.211.0",
55
+ "@opentelemetry/sdk-trace-base": "2.5.0"
54
56
  },
55
57
  "devDependencies": {
56
- "@types/node": "25.0.2",
57
- "@vitest/coverage-v8": "4.0.15",
58
+ "@types/node": "25.0.10",
59
+ "@vitest/coverage-v8": "4.0.18",
58
60
  "tsx": "4.21.0",
59
61
  "typescript": "5.9.3",
60
- "vitest": "4.0.15"
62
+ "vitest": "4.0.18"
61
63
  },
62
64
  "engines": {
63
65
  "node": ">=20.6.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@sentry/nextjs": "^10.36.0",
69
+ "@sentry/node": "^10.36.0",
70
+ "@sentry/opentelemetry": "^10.36.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@sentry/opentelemetry": {
74
+ "optional": true
75
+ },
76
+ "@sentry/node": {
77
+ "optional": true
78
+ },
79
+ "@sentry/nextjs": {
80
+ "optional": true
81
+ }
64
82
  }
65
83
  }
package/package.json CHANGED
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
+ "scripts": {
8
+ "build": "rm -rf dist && tsc -p tsconfig.json",
9
+ "test": "vitest",
10
+ "prepublishOnly": "pnpm i && rm -rf dist && tsc -p tsconfig.json",
11
+ "test:unit": "vitest --project unit",
12
+ "test:integration": "pnpm test:integration:setup && pnpm test:integration:run && pnpm test:integration:assert && pnpm test:integration:teardown",
13
+ "test:integration:setup": "sh ./test/integration/setup.sh integration",
14
+ "test:integration:run": "pnpm --filter @ogcio/o11y run prepare:integration",
15
+ "test:integration:assert": "vitest --project integration",
16
+ "test:integration:teardown": "sh ./test/integration/teardown.sh integration"
17
+ },
7
18
  "exports": {
8
19
  ".": "./dist/sdk-node/index.js",
9
- "./*": "./dist/*.js"
20
+ "./*": "./dist/*.js",
21
+ "./presets/*": "./dist/sdk-node/lib/presets/*.js"
10
22
  },
11
23
  "files": [
12
24
  "dist"
@@ -24,41 +36,48 @@
24
36
  "dependencies": {
25
37
  "@grpc/grpc-js": "1.14.3",
26
38
  "@opentelemetry/api": "1.9.0",
27
- "@opentelemetry/api-logs": "0.208.0",
28
- "@opentelemetry/auto-instrumentations-node": "0.67.2",
29
- "@opentelemetry/core": "2.2.0",
30
- "@opentelemetry/exporter-logs-otlp-grpc": "0.208.0",
31
- "@opentelemetry/exporter-logs-otlp-http": "0.208.0",
32
- "@opentelemetry/exporter-metrics-otlp-grpc": "0.208.0",
33
- "@opentelemetry/exporter-metrics-otlp-http": "0.208.0",
34
- "@opentelemetry/exporter-trace-otlp-grpc": "0.208.0",
35
- "@opentelemetry/exporter-trace-otlp-http": "0.208.0",
36
- "@opentelemetry/instrumentation": "0.208.0",
37
- "@opentelemetry/otlp-exporter-base": "0.208.0",
38
- "@opentelemetry/resources": "2.2.0",
39
- "@opentelemetry/sdk-logs": "0.208.0",
40
- "@opentelemetry/sdk-metrics": "2.2.0",
41
- "@opentelemetry/sdk-node": "0.208.0",
42
- "@opentelemetry/sdk-trace-base": "2.2.0"
39
+ "@opentelemetry/api-logs": "0.211.0",
40
+ "@opentelemetry/auto-instrumentations-node": "0.69.0",
41
+ "@opentelemetry/context-async-hooks": "2.5.0",
42
+ "@opentelemetry/core": "2.5.0",
43
+ "@opentelemetry/exporter-logs-otlp-grpc": "0.211.0",
44
+ "@opentelemetry/exporter-logs-otlp-http": "0.211.0",
45
+ "@opentelemetry/exporter-metrics-otlp-grpc": "0.211.0",
46
+ "@opentelemetry/exporter-metrics-otlp-http": "0.211.0",
47
+ "@opentelemetry/exporter-trace-otlp-grpc": "0.211.0",
48
+ "@opentelemetry/exporter-trace-otlp-http": "0.211.0",
49
+ "@opentelemetry/instrumentation": "0.211.0",
50
+ "@opentelemetry/otlp-exporter-base": "0.211.0",
51
+ "@opentelemetry/resources": "2.5.0",
52
+ "@opentelemetry/sdk-logs": "0.211.0",
53
+ "@opentelemetry/sdk-metrics": "2.5.0",
54
+ "@opentelemetry/sdk-node": "0.211.0",
55
+ "@opentelemetry/sdk-trace-base": "2.5.0"
43
56
  },
44
57
  "devDependencies": {
45
- "@types/node": "25.0.2",
46
- "@vitest/coverage-v8": "4.0.15",
58
+ "@types/node": "25.0.10",
59
+ "@vitest/coverage-v8": "4.0.18",
47
60
  "tsx": "4.21.0",
48
61
  "typescript": "5.9.3",
49
- "vitest": "4.0.15"
62
+ "vitest": "4.0.18"
50
63
  },
51
64
  "engines": {
52
65
  "node": ">=20.6.0"
53
66
  },
54
- "scripts": {
55
- "build": "rm -rf dist && tsc -p tsconfig.json",
56
- "test": "vitest",
57
- "test:unit": "vitest --project unit",
58
- "test:integration": "pnpm test:integration:setup && pnpm test:integration:run && pnpm test:integration:assert && pnpm test:integration:teardown",
59
- "test:integration:setup": "sh ./test/integration/setup.sh integration",
60
- "test:integration:run": "pnpm --filter @ogcio/o11y run prepare:integration",
61
- "test:integration:assert": "vitest --project integration",
62
- "test:integration:teardown": "sh ./test/integration/teardown.sh integration"
67
+ "peerDependencies": {
68
+ "@sentry/nextjs": "^10.36.0",
69
+ "@sentry/node": "^10.36.0",
70
+ "@sentry/opentelemetry": "^10.36.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@sentry/opentelemetry": {
74
+ "optional": true
75
+ },
76
+ "@sentry/node": {
77
+ "optional": true
78
+ },
79
+ "@sentry/nextjs": {
80
+ "optional": true
81
+ }
63
82
  }
64
- }
83
+ }