@adonisjs/otel 1.0.0-next.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.
@@ -0,0 +1,236 @@
1
+ import { context, propagation, trace, SpanStatusCode } from '@opentelemetry/api';
2
+ import { hiddenFields, } from './types/index.js';
3
+ /**
4
+ * Get the currently active span from the current context.
5
+ *
6
+ * Returns `undefined` if there is no active span.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { getCurrentSpan } from '@adonisjs/otel'
11
+ *
12
+ * function myUtility() {
13
+ * const span = getCurrentSpan()
14
+ * span?.setAttributes({ 'custom.attribute': 'value' })
15
+ * }
16
+ * ```
17
+ */
18
+ export function getCurrentSpan() {
19
+ return trace.getActiveSpan();
20
+ }
21
+ /**
22
+ * Set attributes on the currently active span.
23
+ *
24
+ * This is a convenience wrapper around `getCurrentSpan()?.setAttributes()`.
25
+ * Does nothing if there is no active span.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { setAttributes } from '@adonisjs/otel'
30
+ *
31
+ * function processOrder(orderId: string) {
32
+ * setAttributes({
33
+ * 'order.id': orderId,
34
+ * 'order.type': 'subscription',
35
+ * })
36
+ * }
37
+ * ```
38
+ */
39
+ export function setAttributes(attributes) {
40
+ getCurrentSpan()?.setAttributes(attributes);
41
+ }
42
+ /**
43
+ * Record a code section as a span in your traces.
44
+ *
45
+ * Automatically handles:
46
+ * - Creating and closing the span
47
+ * - Capturing exceptions and setting error status
48
+ * - Async/await support
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { record } from '@adonisjs/otel'
53
+ *
54
+ * // Sync
55
+ * const result = record('database.query', () => {
56
+ * return db.query('SELECT * FROM users')
57
+ * })
58
+ *
59
+ * // Async
60
+ * const user = await record('user.fetch', async () => {
61
+ * return await userService.findById(id)
62
+ * })
63
+ *
64
+ * // With attributes
65
+ * const order = await record('order.process', async (span) => {
66
+ * span.setAttributes({ 'order.id': orderId })
67
+ * return await processOrder(orderId)
68
+ * })
69
+ * ```
70
+ */
71
+ export function record(name, callback) {
72
+ const tracer = trace.getTracer('@adonisjs/otel');
73
+ return tracer.startActiveSpan(name, (span) => {
74
+ try {
75
+ const result = callback(span);
76
+ if (result instanceof Promise) {
77
+ return result
78
+ .then((value) => {
79
+ span.end();
80
+ return value;
81
+ })
82
+ .catch((error) => handleError(span, error));
83
+ }
84
+ span.end();
85
+ return result;
86
+ }
87
+ catch (error) {
88
+ handleError(span, error);
89
+ return undefined;
90
+ }
91
+ });
92
+ }
93
+ export function handleError(span, error) {
94
+ span.recordException(error);
95
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
96
+ span.end();
97
+ throw error;
98
+ }
99
+ /**
100
+ * User semantic convention attributes
101
+ * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
102
+ */
103
+ const ATTR_USER_ID = 'user.id';
104
+ const ATTR_USER_EMAIL = 'user.email';
105
+ const ATTR_USER_ROLES = 'user.roles';
106
+ /**
107
+ * Set user information on the currently active span.
108
+ *
109
+ * Uses OpenTelemetry semantic conventions for user attributes.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * import { setUser } from '@adonisjs/otel'
114
+ *
115
+ * // In a controller or middleware
116
+ * setUser({
117
+ * id: auth.user.id,
118
+ * email: auth.user.email,
119
+ * role: auth.user.role,
120
+ * })
121
+ * ```
122
+ */
123
+ export function setUser(user) {
124
+ const span = getCurrentSpan();
125
+ if (!span)
126
+ return;
127
+ const attributes = {
128
+ [ATTR_USER_ID]: String(user.id),
129
+ };
130
+ if (user.email)
131
+ attributes[ATTR_USER_EMAIL] = user.email;
132
+ if (user.role)
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
+ span.setAttributes(attributes);
141
+ }
142
+ /**
143
+ * Inject the current trace context into headers for propagation.
144
+ *
145
+ * Use this when making outgoing HTTP requests, dispatching queue jobs,
146
+ * or any cross-service communication where you want to maintain trace continuity.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * import { injectTraceContext } from '@adonisjs/otel'
151
+ *
152
+ * // HTTP request to another service
153
+ * const headers = {}
154
+ * injectTraceContext(headers)
155
+ * await fetch('https://api.example.com', { headers })
156
+ *
157
+ * // Queue job dispatch
158
+ * const jobHeaders = {}
159
+ * injectTraceContext(jobHeaders)
160
+ * await queue.dispatch('process-order', { orderId }, { headers: jobHeaders })
161
+ * ```
162
+ */
163
+ export function injectTraceContext(headers) {
164
+ propagation.inject(context.active(), headers);
165
+ }
166
+ /**
167
+ * Extract trace context from incoming headers.
168
+ *
169
+ * Use this in queue workers, background jobs, or any service receiving
170
+ * requests from another traced service to continue the trace.
171
+ *
172
+ * @returns The extracted context that can be used with `record()` or `trace.startActiveSpan()`
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * import { extractTraceContext, record } from '@adonisjs/otel'
177
+ * import { context } from '@opentelemetry/api'
178
+ *
179
+ * // In a queue worker
180
+ * const extractedContext = extractTraceContext(job.headers)
181
+ *
182
+ * context.with(extractedContext, () => {
183
+ * record('process-job', () => {
184
+ * // This span will be a child of the original trace
185
+ * })
186
+ * })
187
+ * ```
188
+ */
189
+ export function extractTraceContext(headers) {
190
+ return propagation.extract(context.active(), headers);
191
+ }
192
+ /**
193
+ * Record an event on the currently active span.
194
+ *
195
+ * Events are time-stamped annotations that can be attached to spans
196
+ * to record discrete occurrences during a span's lifetime.
197
+ *
198
+ * @see https://opentelemetry.io/docs/concepts/signals/traces/#span-events
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * import { recordEvent } from '@adonisjs/otel'
203
+ *
204
+ * // Simple event
205
+ * recordEvent('cache.miss')
206
+ *
207
+ * // Event with attributes
208
+ * recordEvent('order.processed', {
209
+ * 'order.id': orderId,
210
+ * 'order.total': 99.99,
211
+ * })
212
+ * ```
213
+ */
214
+ export function recordEvent(name, attributes) {
215
+ getCurrentSpan()?.addEvent(name, attributes);
216
+ }
217
+ /**
218
+ * Preset for pino-pretty to hide OpenTelemetry-injected fields ( in development).
219
+ *
220
+ * By default, hides: pid, hostname, trace_id, span_id, trace_flags, route, request_id, x-request-id
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * import { otelLoggingPreset } from '@adonisjs/otel'
225
+ *
226
+ * // Hide all OTEL fields
227
+ * targets.pretty(otelLoggingPreset())
228
+ *
229
+ * // Keep trace context visible
230
+ * targets.pretty(otelLoggingPreset({ keep: ['trace_id', 'span_id'] }))
231
+ * ```
232
+ */
233
+ export function otelLoggingPreset(options) {
234
+ const keep = options?.keep ?? [];
235
+ return { ignore: hiddenFields.filter((field) => !keep.includes(field)).join(',') };
236
+ }
@@ -0,0 +1,20 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { NextFn } from '@adonisjs/core/types/http';
3
+ import type { UserContextConfig } from '../types/index.js';
4
+ /**
5
+ * Middleware that enriches the active OpenTelemetry span with AdonisJS
6
+ * specific attributes like the route pattern, user info, etc.
7
+ *
8
+ * This should be registered as a router middleware to run after the
9
+ * route has been resolved.
10
+ *
11
+ * When Auth module is installed, it will automatically set user
12
+ * attributes on the span if a user is authenticated.
13
+ */
14
+ export default class OtelMiddleware {
15
+ #private;
16
+ constructor(options: {
17
+ userContext?: UserContextConfig | false;
18
+ });
19
+ handle(ctx: HttpContext, next: NextFn): Promise<any>;
20
+ }
@@ -0,0 +1,64 @@
1
+ import { trace } from '@opentelemetry/api';
2
+ import { ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_ROUTE, } from '@opentelemetry/semantic-conventions';
3
+ import { setUser } from '../helpers.js';
4
+ /**
5
+ * Middleware that enriches the active OpenTelemetry span with AdonisJS
6
+ * specific attributes like the route pattern, user info, etc.
7
+ *
8
+ * This should be registered as a router middleware to run after the
9
+ * route has been resolved.
10
+ *
11
+ * When Auth module is installed, it will automatically set user
12
+ * attributes on the span if a user is authenticated.
13
+ */
14
+ export default class OtelMiddleware {
15
+ #userContextConfig;
16
+ constructor(options) {
17
+ this.#userContextConfig = options.userContext ?? {};
18
+ }
19
+ /**
20
+ * Try to extract user from auth and set on span
21
+ *
22
+ * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/user/
23
+ */
24
+ async #setUserFromAuth(ctx) {
25
+ if (this.#userContextConfig === false)
26
+ return;
27
+ if (this.#userContextConfig.enabled === false)
28
+ return;
29
+ // Custom resolver takes precedence
30
+ if (this.#userContextConfig.resolver) {
31
+ const resolved = await this.#userContextConfig.resolver(ctx);
32
+ if (resolved)
33
+ setUser(resolved);
34
+ return;
35
+ }
36
+ // Default: extract from ctx.auth.user
37
+ const user = ctx.auth?.user;
38
+ if (!user)
39
+ return;
40
+ setUser({ id: user.id, email: user.email, role: user.role });
41
+ }
42
+ async handle(ctx, next) {
43
+ const span = trace.getActiveSpan();
44
+ if (!span)
45
+ return next();
46
+ const { request, route } = ctx;
47
+ /**
48
+ * Update span name with HTTP method and route pattern
49
+ * This gives much better trace names like "GET /users/:id" instead of "GET"
50
+ */
51
+ if (route?.pattern) {
52
+ span.updateName(`${request.method()} ${route.pattern}`);
53
+ span.setAttribute(ATTR_HTTP_ROUTE, route.pattern);
54
+ }
55
+ span.setAttributes({ 'adonis.route.name': route?.name ?? 'unknown' });
56
+ /**
57
+ * Automatically set user context from Auth module
58
+ */
59
+ await this.#setUserFromAuth(ctx);
60
+ const output = await next();
61
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, ctx.response.getStatus());
62
+ return output;
63
+ }
64
+ }
@@ -0,0 +1,65 @@
1
+ import { NodeSDK } from '@opentelemetry/sdk-node';
2
+ import type { OtelConfig } from './types/index.js';
3
+ /**
4
+ * OpenTelemetry SDK manager for AdonisJS.
5
+ *
6
+ * Provides sensible defaults and easy configuration for OpenTelemetry
7
+ * while allowing full customization when needed.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { OtelManager } from '@adonisjs/otel'
12
+ * import config from '#config/otel'
13
+ *
14
+ * const manager = OtelManager.create(config)
15
+ * manager?.start()
16
+ *
17
+ * // Later, on shutdown
18
+ * await manager?.shutdown()
19
+ * ```
20
+ */
21
+ export declare class OtelManager {
22
+ #private;
23
+ static readonly DEFAULT_IGNORED_URLS: string[];
24
+ readonly sdk: NodeSDK;
25
+ readonly serviceName: string;
26
+ readonly serviceVersion: string;
27
+ readonly environment: string;
28
+ constructor(config: OtelConfig);
29
+ /**
30
+ * Start the OpenTelemetry SDK
31
+ */
32
+ start(): void;
33
+ /**
34
+ * Gracefully shutdown the OpenTelemetry SDK
35
+ */
36
+ shutdown(): Promise<void>;
37
+ /**
38
+ * Check if OpenTelemetry should be enabled based on config.
39
+ * Defaults to false when NODE_ENV === 'test'.
40
+ */
41
+ static isEnabled(config: OtelConfig): boolean;
42
+ /**
43
+ * Create and configure the OpenTelemetry SDK manager.
44
+ *
45
+ * Returns null if OpenTelemetry is disabled.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { OtelManager } from '@adonisjs/otel'
50
+ * import config from '#config/otel'
51
+ *
52
+ * const manager = OtelManager.create(config)
53
+ * manager?.start()
54
+ *
55
+ * // Later, on shutdown
56
+ * await manager?.shutdown()
57
+ * ```
58
+ */
59
+ static create(config: OtelConfig): OtelManager | null;
60
+ /**
61
+ * Get the global OtelManager instance.
62
+ * Returns null if OpenTelemetry is disabled or not yet initialized.
63
+ */
64
+ static getInstance(): OtelManager | null;
65
+ }