@adonisjs/otel 1.0.0-next.0 → 1.0.0-next.2

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.
@@ -19,7 +19,7 @@ export async function configure(command) {
19
19
  */
20
20
  await codemods.makeUsingStub(stubsRoot, 'config.stub', {});
21
21
  /**
22
- * Publish the bin/otel.ts file
22
+ * Publish the otel.ts file
23
23
  */
24
24
  await codemods.makeUsingStub(stubsRoot, 'otel.stub', {});
25
25
  /**
@@ -36,7 +36,7 @@ export async function configure(command) {
36
36
  ' * OpenTelemetry initialization - MUST be the first import',
37
37
  ' * @see https://opentelemetry.io/docs/languages/js/getting-started/nodejs/',
38
38
  ' */',
39
- `import './otel.js'`,
39
+ `import '../otel.js'`,
40
40
  '',
41
41
  ]);
42
42
  await serverFile.save();
@@ -15,7 +15,6 @@ export default class OtelProvider {
15
15
  #registerExceptionHandler() {
16
16
  const originalReport = ExceptionHandler.prototype.report;
17
17
  ExceptionHandler.macro('report', async function (error, ctx) {
18
- // @ts-expect-error - protected method
19
18
  const httpError = this.toHttpError(error);
20
19
  if (!this.shouldReport(httpError))
21
20
  return;
@@ -0,0 +1,2 @@
1
+ export declare const E_OTEL_CONFIG: new (args: [configPath: string], options?: ErrorOptions) => import("@poppinss/utils").Exception;
2
+ export declare const E_OTEL_CONFIG_INVALID: new (args: [configPath: string], options?: ErrorOptions) => import("@poppinss/utils").Exception;
@@ -0,0 +1,3 @@
1
+ import { createError } from '@poppinss/utils';
2
+ export const E_OTEL_CONFIG = createError('Failed to load OpenTelemetry config at "%s". Make sure the file exists and has no syntax errors.', 'E_OTEL_CONFIG');
3
+ export const E_OTEL_CONFIG_INVALID = createError('OpenTelemetry config at "%s" must export a configuration object.', 'E_OTEL_CONFIG_INVALID');
@@ -131,12 +131,6 @@ export function setUser(user) {
131
131
  attributes[ATTR_USER_EMAIL] = user.email;
132
132
  if (user.role)
133
133
  attributes[ATTR_USER_ROLES] = [user.role];
134
- // Add any extra custom attributes
135
- for (const [key, value] of Object.entries(user)) {
136
- if (!['id', 'email', 'role'].includes(key) && value !== undefined) {
137
- attributes[`user.${key}`] = String(value);
138
- }
139
- }
140
134
  span.setAttributes(attributes);
141
135
  }
142
136
  /**
@@ -0,0 +1,52 @@
1
+ import type { Span } from '@opentelemetry/api';
2
+ import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
3
+ import type { ContainerMakeTracingData } from '@adonisjs/core/types/container';
4
+ import { InstrumentationBase } from '@opentelemetry/instrumentation';
5
+ import { TracingChannelSubscribers } from 'node:diagnostics_channel';
6
+ /**
7
+ * OpenTelemetry instrumentation for AdonisJS IoC Container.
8
+ *
9
+ * Creates spans for `container.make()` calls, tracking dependency resolution.
10
+ * Spans are only created when there is an active parent context to avoid
11
+ * orphan traces during application boot.
12
+ *
13
+ * Span name: `container.make {bindingName}`
14
+ * Attributes: `container.binding`
15
+ */
16
+ export declare class ContainerInstrumentation extends InstrumentationBase {
17
+ protected subscribed: boolean;
18
+ protected spans: WeakMap<object, Span>;
19
+ protected handlers?: TracingChannelSubscribers<ContainerMakeTracingData>;
20
+ constructor(config?: InstrumentationConfig);
21
+ /**
22
+ * Required by InstrumentationBase. Returns undefined since we use
23
+ * diagnostics_channel instead of module patching.
24
+ */
25
+ protected init(): undefined;
26
+ /**
27
+ * Extracts a human-readable name from a container binding.
28
+ */
29
+ protected getBindingName(binding: unknown): string;
30
+ /**
31
+ * Called when container.make() starts. Creates a new span if there's a parent context.
32
+ */
33
+ protected handleStart(message: ContainerMakeTracingData): void;
34
+ /**
35
+ * Called when container.make() completes successfully. Ends the span.
36
+ */
37
+ protected handleAsyncEnd(message: ContainerMakeTracingData): void;
38
+ /**
39
+ * Called when container.make() throws. Records the exception and ends the span.
40
+ */
41
+ protected handleError(message: ContainerMakeTracingData & {
42
+ error: unknown;
43
+ }): void;
44
+ /**
45
+ * Subscribes to the container tracing channel.
46
+ */
47
+ enable(): void;
48
+ /**
49
+ * Unsubscribes from the container tracing channel and cleans up.
50
+ */
51
+ disable(): void;
52
+ }
@@ -0,0 +1,102 @@
1
+ import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { InstrumentationBase } from '@opentelemetry/instrumentation';
3
+ import { tracingChannels } from '@adonisjs/core/container';
4
+ /**
5
+ * OpenTelemetry instrumentation for AdonisJS IoC Container.
6
+ *
7
+ * Creates spans for `container.make()` calls, tracking dependency resolution.
8
+ * Spans are only created when there is an active parent context to avoid
9
+ * orphan traces during application boot.
10
+ *
11
+ * Span name: `container.make {bindingName}`
12
+ * Attributes: `container.binding`
13
+ */
14
+ export class ContainerInstrumentation extends InstrumentationBase {
15
+ subscribed = false;
16
+ spans = new WeakMap();
17
+ handlers;
18
+ constructor(config = {}) {
19
+ super('@adonisjs/instrumentation-container', '1.0.0', config);
20
+ }
21
+ /**
22
+ * Required by InstrumentationBase. Returns undefined since we use
23
+ * diagnostics_channel instead of module patching.
24
+ */
25
+ init() {
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Extracts a human-readable name from a container binding.
30
+ */
31
+ getBindingName(binding) {
32
+ if (typeof binding === 'string')
33
+ return binding;
34
+ if (typeof binding === 'symbol')
35
+ return binding.description || 'symbol';
36
+ if (typeof binding === 'function')
37
+ return binding.name || 'anonymous';
38
+ return 'unknown';
39
+ }
40
+ /**
41
+ * Called when container.make() starts. Creates a new span if there's a parent context.
42
+ */
43
+ handleStart(message) {
44
+ const parentContext = context.active();
45
+ if (!trace.getSpan(parentContext))
46
+ return;
47
+ const bindingName = this.getBindingName(message.binding);
48
+ const span = this.tracer.startSpan(`container.make ${bindingName}`, { kind: SpanKind.INTERNAL, attributes: { 'container.binding': bindingName } }, parentContext);
49
+ this.spans.set(message, span);
50
+ }
51
+ /**
52
+ * Called when container.make() completes successfully. Ends the span.
53
+ */
54
+ handleAsyncEnd(message) {
55
+ const span = this.spans.get(message);
56
+ if (!span)
57
+ return;
58
+ span.end();
59
+ this.spans.delete(message);
60
+ }
61
+ /**
62
+ * Called when container.make() throws. Records the exception and ends the span.
63
+ */
64
+ handleError(message) {
65
+ const span = this.spans.get(message);
66
+ if (!span)
67
+ return;
68
+ if (message.error instanceof Error) {
69
+ span.recordException(message.error);
70
+ span.setStatus({ code: SpanStatusCode.ERROR, message: message.error.message });
71
+ }
72
+ span.end();
73
+ this.spans.delete(message);
74
+ }
75
+ /**
76
+ * Subscribes to the container tracing channel.
77
+ */
78
+ enable() {
79
+ if (this.subscribed)
80
+ return;
81
+ this.subscribed = true;
82
+ this.handlers = {
83
+ start: (message) => this.handleStart(message),
84
+ end: () => { },
85
+ asyncStart: () => { },
86
+ asyncEnd: (message) => this.handleAsyncEnd(message),
87
+ error: (message) => this.handleError(message),
88
+ };
89
+ tracingChannels.containerMake.subscribe(this.handlers);
90
+ }
91
+ /**
92
+ * Unsubscribes from the container tracing channel and cleans up.
93
+ */
94
+ disable() {
95
+ if (!this.subscribed || !this.handlers)
96
+ return;
97
+ tracingChannels.containerMake.unsubscribe(this.handlers);
98
+ this.subscribed = false;
99
+ this.handlers = undefined;
100
+ this.spans = new WeakMap();
101
+ }
102
+ }
@@ -0,0 +1,52 @@
1
+ import type { Span } from '@opentelemetry/api';
2
+ import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
3
+ import type { TracingChannelSubscribers } from 'node:diagnostics_channel';
4
+ import { InstrumentationBase } from '@opentelemetry/instrumentation';
5
+ import { AllowedEventTypes, EventDispatchData } from '@adonisjs/core/types/events';
6
+ /**
7
+ * OpenTelemetry instrumentation for AdonisJS Event Emitter.
8
+ *
9
+ * Creates spans for `emitter.emit()` calls, tracking event dispatch and listeners execution.
10
+ * Spans are only created when there is an active parent context to avoid
11
+ * orphan traces during application boot.
12
+ *
13
+ * Span name: `event.dispatch {eventName}`
14
+ * Attributes: `event.name`
15
+ */
16
+ export declare class EventsInstrumentation extends InstrumentationBase {
17
+ protected subscribed: boolean;
18
+ protected spans: WeakMap<object, Span>;
19
+ protected handlers?: TracingChannelSubscribers<EventDispatchData>;
20
+ constructor(config?: InstrumentationConfig);
21
+ /**
22
+ * Required by InstrumentationBase. Returns undefined since we use
23
+ * diagnostics_channel instead of module patching.
24
+ */
25
+ protected init(): undefined;
26
+ /**
27
+ * Extracts a human-readable name from an event.
28
+ */
29
+ protected getEventName(event: AllowedEventTypes): string;
30
+ /**
31
+ * Called when emitter.emit() starts. Creates a new span if there's a parent context.
32
+ */
33
+ protected handleStart(message: EventDispatchData): void;
34
+ /**
35
+ * Called when emitter.emit() completes successfully. Ends the span.
36
+ */
37
+ protected handleAsyncEnd(message: EventDispatchData): void;
38
+ /**
39
+ * Called when a listener throws. Records the exception and ends the span.
40
+ */
41
+ protected handleError(message: EventDispatchData & {
42
+ error: unknown;
43
+ }): void;
44
+ /**
45
+ * Subscribes to the events tracing channel.
46
+ */
47
+ enable(): void;
48
+ /**
49
+ * Unsubscribes from the events tracing channel and cleans up.
50
+ */
51
+ disable(): void;
52
+ }
@@ -0,0 +1,104 @@
1
+ import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { InstrumentationBase } from '@opentelemetry/instrumentation';
3
+ import { tracingChannels } from '@adonisjs/core/events';
4
+ /**
5
+ * OpenTelemetry instrumentation for AdonisJS Event Emitter.
6
+ *
7
+ * Creates spans for `emitter.emit()` calls, tracking event dispatch and listeners execution.
8
+ * Spans are only created when there is an active parent context to avoid
9
+ * orphan traces during application boot.
10
+ *
11
+ * Span name: `event.dispatch {eventName}`
12
+ * Attributes: `event.name`
13
+ */
14
+ export class EventsInstrumentation extends InstrumentationBase {
15
+ subscribed = false;
16
+ spans = new WeakMap();
17
+ handlers;
18
+ constructor(config = {}) {
19
+ super('@adonisjs/instrumentation-events', '1.0.0', config);
20
+ }
21
+ /**
22
+ * Required by InstrumentationBase. Returns undefined since we use
23
+ * diagnostics_channel instead of module patching.
24
+ */
25
+ init() {
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Extracts a human-readable name from an event.
30
+ */
31
+ getEventName(event) {
32
+ if (typeof event === 'string')
33
+ return event;
34
+ if (typeof event === 'symbol')
35
+ return event.description || 'symbol';
36
+ if (typeof event === 'number')
37
+ return String(event);
38
+ if (typeof event === 'function')
39
+ return event.name || 'anonymous';
40
+ return 'unknown';
41
+ }
42
+ /**
43
+ * Called when emitter.emit() starts. Creates a new span if there's a parent context.
44
+ */
45
+ handleStart(message) {
46
+ const parentContext = context.active();
47
+ if (!trace.getSpan(parentContext))
48
+ return;
49
+ const eventName = this.getEventName(message.event);
50
+ const span = this.tracer.startSpan(`event.dispatch ${eventName}`, { kind: SpanKind.INTERNAL, attributes: { 'event.name': eventName } }, parentContext);
51
+ this.spans.set(message, span);
52
+ }
53
+ /**
54
+ * Called when emitter.emit() completes successfully. Ends the span.
55
+ */
56
+ handleAsyncEnd(message) {
57
+ const span = this.spans.get(message);
58
+ if (!span)
59
+ return;
60
+ span.end();
61
+ this.spans.delete(message);
62
+ }
63
+ /**
64
+ * Called when a listener throws. Records the exception and ends the span.
65
+ */
66
+ handleError(message) {
67
+ const span = this.spans.get(message);
68
+ if (!span)
69
+ return;
70
+ if (message.error instanceof Error) {
71
+ span.recordException(message.error);
72
+ span.setStatus({ code: SpanStatusCode.ERROR, message: message.error.message });
73
+ }
74
+ span.end();
75
+ this.spans.delete(message);
76
+ }
77
+ /**
78
+ * Subscribes to the events tracing channel.
79
+ */
80
+ enable() {
81
+ if (this.subscribed)
82
+ return;
83
+ this.subscribed = true;
84
+ this.handlers = {
85
+ start: (message) => this.handleStart(message),
86
+ end: () => { },
87
+ asyncStart: () => { },
88
+ asyncEnd: (message) => this.handleAsyncEnd(message),
89
+ error: (message) => this.handleError(message),
90
+ };
91
+ tracingChannels.eventDispatch.subscribe(this.handlers);
92
+ }
93
+ /**
94
+ * Unsubscribes from the events tracing channel and cleans up.
95
+ */
96
+ disable() {
97
+ if (!this.subscribed || !this.handlers)
98
+ return;
99
+ tracingChannels.eventDispatch.unsubscribe(this.handlers);
100
+ this.subscribed = false;
101
+ this.handlers = undefined;
102
+ this.spans = new WeakMap();
103
+ }
104
+ }
@@ -5,14 +5,14 @@
5
5
  * to enable auto-instrumentation:
6
6
  *
7
7
  * ```ts
8
- * // bin/otel.ts
8
+ * // otel.ts
9
9
  * import { init } from '@adonisjs/otel/init'
10
10
  * await init(import.meta.dirname)
11
11
  * ```
12
12
  *
13
13
  * Then import it first in bin/server.ts:
14
14
  * ```ts
15
- * import './otel.js'
15
+ * import '../otel.js'
16
16
  * ```
17
17
  */
18
18
  export declare function init(dirname: string): Promise<void>;
@@ -5,29 +5,41 @@
5
5
  * to enable auto-instrumentation:
6
6
  *
7
7
  * ```ts
8
- * // bin/otel.ts
8
+ * // otel.ts
9
9
  * import { init } from '@adonisjs/otel/init'
10
10
  * await init(import.meta.dirname)
11
11
  * ```
12
12
  *
13
13
  * Then import it first in bin/server.ts:
14
14
  * ```ts
15
- * import './otel.js'
15
+ * import '../otel.js'
16
16
  * ```
17
17
  */
18
18
  import { createAddHookMessageChannel } from 'import-in-the-middle';
19
19
  import { register } from 'node:module';
20
20
  import { join } from 'node:path';
21
- export async function init(dirname) {
22
- // Setup import-in-the-middle hooks for auto-instrumentation
21
+ import { E_OTEL_CONFIG, E_OTEL_CONFIG_INVALID } from './errors.js';
22
+ async function loadConfig(path) {
23
+ return await import(path)
24
+ .then((mod) => mod.default || mod)
25
+ .catch((error) => {
26
+ throw new E_OTEL_CONFIG([path], { cause: error });
27
+ });
28
+ }
29
+ function setupHooks() {
23
30
  const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel();
24
31
  register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions);
32
+ return waitForAllMessagesAcknowledged;
33
+ }
34
+ export async function init(dirname) {
35
+ // Setup import-in-the-middle hooks for auto-instrumentation
36
+ const waitForAllMessagesAcknowledged = setupHooks();
25
37
  // Import SDK functions after hooks are registered
26
38
  const { OtelManager } = await import('./otel.js');
27
- const configPath = join(dirname, '../config/otel.ts');
28
- const config = await import(configPath).then((mod) => mod.default || mod);
39
+ const configPath = join(dirname, 'config/otel.js');
40
+ const config = await loadConfig(configPath);
29
41
  if (!config)
30
- throw new Error(`Otel configuration not found at ${configPath}`);
42
+ throw new E_OTEL_CONFIG_INVALID([configPath]);
31
43
  // Check if OTEL is enabled
32
44
  if (!OtelManager.isEnabled(config))
33
45
  return;
@@ -30,13 +30,6 @@ export interface OtelConfig extends Partial<Omit<NodeSDKConfiguration, 'resource
30
30
  * This option is ignored if `sampler` is explicitly provided.
31
31
  *
32
32
  * @default 1.0
33
- *
34
- * @example
35
- * ```ts
36
- * defineConfig({
37
- * samplingRatio: 0.1, // Sample 10% of traces in production
38
- * })
39
- * ```
40
33
  */
41
34
  samplingRatio?: number;
42
35
  /**
@@ -46,13 +39,6 @@ export interface OtelConfig extends Partial<Omit<NodeSDKConfiguration, 'resource
46
39
  * to help with local development and debugging.
47
40
  *
48
41
  * @default false
49
- *
50
- * @example
51
- * ```ts
52
- * defineConfig({
53
- * debug: true, // Print spans to console
54
- * })
55
- * ```
56
42
  */
57
43
  debug?: boolean;
58
44
  /**
@@ -122,7 +108,6 @@ export interface UserContextResult {
122
108
  id: string | number;
123
109
  email?: string;
124
110
  role?: string;
125
- [key: string]: unknown;
126
111
  }
127
112
  /**
128
113
  * Configuration for automatic user context extraction
@@ -147,7 +132,6 @@ export interface UserContext {
147
132
  id: string | number;
148
133
  email?: string;
149
134
  role?: string;
150
- [key: string]: unknown;
151
135
  }
152
136
  /**
153
137
  * Headers carrier type for context propagation
@@ -1,5 +1,5 @@
1
1
  {{{
2
- exports({ to: app.makePath('bin/otel.ts') })
2
+ exports({ to: app.makePath('otel.ts') })
3
3
  }}}
4
4
  /**
5
5
  * OpenTelemetry initialization file.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adonisjs/otel",
3
3
  "description": "OpenTelemetry integration for AdonisJS with sensible defaults and zero-config setup",
4
- "version": "1.0.0-next.0",
4
+ "version": "1.0.0-next.2",
5
5
  "engines": {
6
6
  "node": ">=20.6.0"
7
7
  },
@@ -22,7 +22,9 @@
22
22
  "./helpers": "./build/src/helpers.js",
23
23
  "./decorators": "./build/src/decorators.js",
24
24
  "./otel_provider": "./build/providers/otel_provider.js",
25
- "./otel_middleware": "./build/src/middleware/otel_middleware.js"
25
+ "./otel_middleware": "./build/src/middleware/otel_middleware.js",
26
+ "./instrumentations/container": "./build/src/instrumentations/container.js",
27
+ "./instrumentations/events": "./build/src/instrumentations/events.js"
26
28
  },
27
29
  "scripts": {
28
30
  "clean": "del-cli build",
@@ -64,12 +66,13 @@
64
66
  "@opentelemetry/sdk-node": "^0.208.0",
65
67
  "@opentelemetry/sdk-trace-base": "^2.2.0",
66
68
  "@opentelemetry/semantic-conventions": "^1.38.0",
69
+ "@poppinss/utils": "^6.10.1",
67
70
  "import-in-the-middle": "^2.0.0"
68
71
  },
69
72
  "devDependencies": {
70
73
  "@adonisjs/assembler": "^7.8.2",
71
74
  "@adonisjs/auth": "^9.5.1",
72
- "@adonisjs/core": "^6.19.1",
75
+ "@adonisjs/core": "^7.0.0-next.14",
73
76
  "@adonisjs/eslint-config": "2.1.2",
74
77
  "@adonisjs/prettier-config": "^1.4.5",
75
78
  "@adonisjs/tsconfig": "^1.4.1",
@@ -77,6 +80,7 @@
77
80
  "@japa/runner": "^4.4.0",
78
81
  "@opentelemetry/context-async-hooks": "^2.2.0",
79
82
  "@opentelemetry/core": "^2.2.0",
83
+ "@release-it/conventional-changelog": "^10.0.0",
80
84
  "@sentry/node": "^10.30.0",
81
85
  "@swc/core": "^1.15.3",
82
86
  "@types/node": "^20.19.26",
@@ -84,14 +88,13 @@
84
88
  "copyfiles": "^2.4.1",
85
89
  "del-cli": "^7.0.0",
86
90
  "eslint": "^9.39.1",
87
- "@release-it/conventional-changelog": "^10.0.0",
88
- "release-it": "^19.0.0",
89
91
  "prettier": "^3.7.4",
92
+ "release-it": "^19.0.0",
90
93
  "ts-node-maintained": "^10.9.6",
91
94
  "typescript": "^5.9.3"
92
95
  },
93
96
  "peerDependencies": {
94
- "@adonisjs/core": "^6.2.0"
97
+ "@adonisjs/core": "^6.2.0 || ^7.0.0"
95
98
  },
96
99
  "publishConfig": {
97
100
  "access": "public",