@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.
- package/LICENSE.md +9 -0
- package/README.md +38 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +69 -0
- package/build/index.d.ts +11 -0
- package/build/index.js +19 -0
- package/build/providers/otel_provider.d.ts +11 -0
- package/build/providers/otel_provider.js +50 -0
- package/build/src/decorators.d.ts +68 -0
- package/build/src/decorators.js +102 -0
- package/build/src/define_config.d.ts +2 -0
- package/build/src/define_config.js +3 -0
- package/build/src/helpers.d.ts +174 -0
- package/build/src/helpers.js +236 -0
- package/build/src/middleware/otel_middleware.d.ts +20 -0
- package/build/src/middleware/otel_middleware.js +64 -0
- package/build/src/otel.d.ts +65 -0
- package/build/src/otel.js +329 -0
- package/build/src/start.d.ts +18 -0
- package/build/src/start.js +37 -0
- package/build/src/types/decorators.d.ts +13 -0
- package/build/src/types/decorators.js +1 -0
- package/build/src/types/index.d.ts +155 -0
- package/build/src/types/index.js +1 -0
- package/build/src/types/instrumentations.d.ts +79 -0
- package/build/src/types/instrumentations.js +1 -0
- package/build/src/types/logging.d.ts +12 -0
- package/build/src/types/logging.js +13 -0
- package/build/stubs/config.stub +12 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/main.js +7 -0
- package/build/stubs/otel.stub +12 -0
- package/package.json +134 -0
|
@@ -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
|
+
}
|