@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 ADDED
@@ -0,0 +1,9 @@
1
+ # The MIT License
2
+
3
+ Copyright (c) 2023
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @adonisjs/otel
2
+
3
+ <br />
4
+
5
+ [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url]
6
+
7
+ ## Introduction
8
+
9
+ OpenTelemetry integration for AdonisJS with sensible defaults and zero-config setup. Get distributed tracing, metrics, and automatic instrumentation out of the box.
10
+
11
+ ## Official Documentation
12
+
13
+ The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/digging-deeper/otel)
14
+
15
+ ## Contributing
16
+
17
+ One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework.
18
+
19
+ We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework.
20
+
21
+ ## Code of Conduct
22
+
23
+ In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md).
24
+
25
+ ## License
26
+
27
+ AdonisJS OpenTelemetry is open-sourced software licensed under the [MIT license](LICENSE.md).
28
+
29
+ [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/otel/checks.yml?style=for-the-badge
30
+ [gh-workflow-url]: https://github.com/adonisjs/otel/actions/workflows/checks.yml "Github action"
31
+
32
+ [npm-image]: https://img.shields.io/npm/v/@adonisjs/otel/latest.svg?style=for-the-badge&logo=npm
33
+ [npm-url]: https://www.npmjs.com/package/@adonisjs/otel/v/latest "npm"
34
+
35
+ [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
36
+
37
+ [license-url]: LICENSE.md
38
+ [license-image]: https://img.shields.io/github/license/adonisjs/otel?style=for-the-badge
@@ -0,0 +1,2 @@
1
+ import ConfigureCommand from '@adonisjs/core/commands/configure';
2
+ export declare function configure(command: ConfigureCommand): Promise<void>;
@@ -0,0 +1,69 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Configure hook
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | The configure hook is called when someone runs "node ace configure <package>"
7
+ | command. You are free to perform any operations inside this function to
8
+ | configure the package.
9
+ |
10
+ | To make things easier, you have access to the underlying "ConfigureCommand"
11
+ | instance and you can use codemods to modify the source files.
12
+ |
13
+ */
14
+ import { stubsRoot } from './stubs/main.js';
15
+ export async function configure(command) {
16
+ const codemods = await command.createCodemods();
17
+ /**
18
+ * Publish the configuration file
19
+ */
20
+ await codemods.makeUsingStub(stubsRoot, 'config.stub', {});
21
+ /**
22
+ * Publish the bin/otel.ts file
23
+ */
24
+ await codemods.makeUsingStub(stubsRoot, 'otel.stub', {});
25
+ /**
26
+ * Add import to bin/server.ts as the FIRST import
27
+ * This is critical for auto-instrumentation to work
28
+ */
29
+ const project = await codemods.getTsMorphProject();
30
+ const serverFile = project?.getSourceFile(command.app.makePath('bin/server.ts'));
31
+ if (serverFile) {
32
+ const firstImport = serverFile.getImportDeclarations()[0];
33
+ const insertIndex = firstImport?.getChildIndex() ?? 0;
34
+ serverFile.insertStatements(insertIndex, [
35
+ '/**',
36
+ ' * OpenTelemetry initialization - MUST be the first import',
37
+ ' * @see https://opentelemetry.io/docs/languages/js/getting-started/nodejs/',
38
+ ' */',
39
+ `import './otel.js'`,
40
+ '',
41
+ ]);
42
+ await serverFile.save();
43
+ }
44
+ /**
45
+ * Register the provider
46
+ */
47
+ await codemods.updateRcFile((rcFile) => rcFile.addProvider('@adonisjs/otel/otel_provider'));
48
+ /**
49
+ * Register the middleware
50
+ */
51
+ await codemods.registerMiddleware('router', [
52
+ { path: '@adonisjs/otel/otel_middleware', position: 'before' },
53
+ ]);
54
+ /**
55
+ * Add new environment variables
56
+ */
57
+ await codemods.defineEnvVariables({
58
+ APP_NAME: command.app.appName,
59
+ APP_VERSION: '0.0.1',
60
+ APP_ENV: 'development',
61
+ });
62
+ await codemods.defineEnvValidations({
63
+ variables: {
64
+ APP_NAME: 'Env.schema.string()',
65
+ APP_VERSION: 'Env.schema.string()',
66
+ APP_ENV: "Env.schema.enum(['development', 'staging', 'production'] as const)",
67
+ },
68
+ });
69
+ }
@@ -0,0 +1,11 @@
1
+ export { configure } from './configure.js';
2
+ export { defineConfig } from './src/define_config.js';
3
+ export { OtelManager } from './src/otel.js';
4
+ /**
5
+ * Re-export OTLP exporters so users don't need to install those 100 packages
6
+ * from OpenTelemetry just to get the exporters.
7
+ */
8
+ export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
9
+ export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
10
+ export { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
11
+ export { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
package/build/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Package entrypoint
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Export values from the package entrypoint as you see fit.
7
+ |
8
+ */
9
+ export { configure } from './configure.js';
10
+ export { defineConfig } from './src/define_config.js';
11
+ export { OtelManager } from './src/otel.js';
12
+ /**
13
+ * Re-export OTLP exporters so users don't need to install those 100 packages
14
+ * from OpenTelemetry just to get the exporters.
15
+ */
16
+ export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
17
+ export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
18
+ export { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
19
+ export { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
@@ -0,0 +1,11 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ export default class OtelProvider {
3
+ #private;
4
+ protected app: ApplicationService;
5
+ constructor(app: ApplicationService);
6
+ register(): void;
7
+ /**
8
+ * Gracefully flush pending spans
9
+ */
10
+ shutdown(): Promise<void>;
11
+ }
@@ -0,0 +1,50 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api';
2
+ import { configProvider } from '@adonisjs/core';
3
+ import { ExceptionHandler } from '@adonisjs/core/http';
4
+ import { getCurrentSpan } from '../src/helpers.js';
5
+ import OtelMiddleware from '../src/middleware/otel_middleware.js';
6
+ import { OtelManager } from '../src/otel.js';
7
+ export default class OtelProvider {
8
+ app;
9
+ constructor(app) {
10
+ this.app = app;
11
+ }
12
+ /**
13
+ * Hook into ExceptionHandler to record exceptions in spans
14
+ */
15
+ #registerExceptionHandler() {
16
+ const originalReport = ExceptionHandler.prototype.report;
17
+ ExceptionHandler.macro('report', async function (error, ctx) {
18
+ // @ts-expect-error - protected method
19
+ const httpError = this.toHttpError(error);
20
+ if (!this.shouldReport(httpError))
21
+ return;
22
+ const span = getCurrentSpan();
23
+ if (span && error instanceof Error) {
24
+ span.recordException(error);
25
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
26
+ }
27
+ return originalReport.call(this, error, ctx);
28
+ });
29
+ }
30
+ register() {
31
+ this.#registerExceptionHandler();
32
+ this.#registerMiddleware();
33
+ }
34
+ /**
35
+ * Register the OtelMiddleware as a singleton in the container
36
+ */
37
+ #registerMiddleware() {
38
+ this.app.container.singleton(OtelMiddleware, async () => {
39
+ const otelConfigProvider = this.app.config.get('otel', {});
40
+ const config = await configProvider.resolve(this.app, otelConfigProvider);
41
+ return new OtelMiddleware({ userContext: config?.userContext });
42
+ });
43
+ }
44
+ /**
45
+ * Gracefully flush pending spans
46
+ */
47
+ async shutdown() {
48
+ await OtelManager.getInstance()?.shutdown();
49
+ }
50
+ }
@@ -0,0 +1,68 @@
1
+ import type { SpanOptions } from './types/index.js';
2
+ type Constructor = new (...args: any[]) => any;
3
+ /**
4
+ * Decorator to create a span around a method.
5
+ *
6
+ * Automatically handles:
7
+ * - Creating and closing the span
8
+ * - Capturing exceptions and setting error status
9
+ * - Async/await support
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { span } from '@adonisjs/otel'
14
+ *
15
+ * class UserService {
16
+ * @span()
17
+ * async findById(id: string) {
18
+ * // Span name: "UserService.findById"
19
+ * return db.users.find(id)
20
+ * }
21
+ *
22
+ * @span({ name: 'user.create', attributes: { operation: 'create' } })
23
+ * async create(data: UserData) {
24
+ * return db.users.create(data)
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ export declare function span(options?: SpanOptions): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
30
+ /**
31
+ * Decorator to create spans around all methods of a class.
32
+ *
33
+ * Automatically handles:
34
+ * - Creating and closing spans for each method
35
+ * - Capturing exceptions and setting error status
36
+ * - Async/await support
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { spanAll } from '@adonisjs/otel'
41
+ *
42
+ * @spanAll()
43
+ * class OrderService {
44
+ * async create(data: OrderData) {
45
+ * // Span name: "OrderService.create"
46
+ * return db.orders.create(data)
47
+ * }
48
+ *
49
+ * async findById(id: string) {
50
+ * // Span name: "OrderService.findById"
51
+ * return db.orders.find(id)
52
+ * }
53
+ * }
54
+ *
55
+ * @spanAll({ prefix: 'order' })
56
+ * class OrderService {
57
+ * async create(data: OrderData) {
58
+ * // Span name: "order.create"
59
+ * return db.orders.create(data)
60
+ * }
61
+ * }
62
+ * ```
63
+ */
64
+ export declare function spanAll(options?: {
65
+ prefix?: string;
66
+ attributes?: Record<string, string | number | boolean>;
67
+ }): <T extends Constructor>(constructor: T) => T;
68
+ export {};
@@ -0,0 +1,102 @@
1
+ import { record } from './helpers.js';
2
+ /**
3
+ * Wrap a method to create a span around its execution
4
+ */
5
+ function wrapMethod(target, propertyKey, descriptor, options) {
6
+ const originalMethod = descriptor.value;
7
+ const className = target.constructor.name;
8
+ descriptor.value = function (...args) {
9
+ const spanName = options?.name ?? `${className}.${propertyKey}`;
10
+ return record(spanName, (activeSpan) => {
11
+ if (options?.attributes)
12
+ activeSpan.setAttributes(options.attributes);
13
+ return originalMethod.apply(this, args);
14
+ });
15
+ };
16
+ return descriptor;
17
+ }
18
+ /**
19
+ * Decorator to create a span around a method.
20
+ *
21
+ * Automatically handles:
22
+ * - Creating and closing the span
23
+ * - Capturing exceptions and setting error status
24
+ * - Async/await support
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { span } from '@adonisjs/otel'
29
+ *
30
+ * class UserService {
31
+ * @span()
32
+ * async findById(id: string) {
33
+ * // Span name: "UserService.findById"
34
+ * return db.users.find(id)
35
+ * }
36
+ *
37
+ * @span({ name: 'user.create', attributes: { operation: 'create' } })
38
+ * async create(data: UserData) {
39
+ * return db.users.create(data)
40
+ * }
41
+ * }
42
+ * ```
43
+ */
44
+ export function span(options) {
45
+ return function (target, propertyKey, descriptor) {
46
+ return wrapMethod(target, propertyKey, descriptor, options);
47
+ };
48
+ }
49
+ /**
50
+ * Decorator to create spans around all methods of a class.
51
+ *
52
+ * Automatically handles:
53
+ * - Creating and closing spans for each method
54
+ * - Capturing exceptions and setting error status
55
+ * - Async/await support
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { spanAll } from '@adonisjs/otel'
60
+ *
61
+ * @spanAll()
62
+ * class OrderService {
63
+ * async create(data: OrderData) {
64
+ * // Span name: "OrderService.create"
65
+ * return db.orders.create(data)
66
+ * }
67
+ *
68
+ * async findById(id: string) {
69
+ * // Span name: "OrderService.findById"
70
+ * return db.orders.find(id)
71
+ * }
72
+ * }
73
+ *
74
+ * @spanAll({ prefix: 'order' })
75
+ * class OrderService {
76
+ * async create(data: OrderData) {
77
+ * // Span name: "order.create"
78
+ * return db.orders.create(data)
79
+ * }
80
+ * }
81
+ * ```
82
+ */
83
+ export function spanAll(options) {
84
+ return function (constructor) {
85
+ const prototype = constructor.prototype;
86
+ const propertyNames = Object.getOwnPropertyNames(prototype);
87
+ for (const propertyName of propertyNames) {
88
+ if (propertyName === 'constructor')
89
+ continue;
90
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
91
+ if (!descriptor || typeof descriptor.value !== 'function')
92
+ continue;
93
+ const spanName = options?.prefix ? `${options.prefix}.${propertyName}` : undefined;
94
+ const wrappedDescriptor = wrapMethod(prototype, propertyName, descriptor, {
95
+ name: spanName,
96
+ attributes: options?.attributes,
97
+ });
98
+ Object.defineProperty(prototype, propertyName, wrappedDescriptor);
99
+ }
100
+ return constructor;
101
+ };
102
+ }
@@ -0,0 +1,2 @@
1
+ import type { OtelConfig } from './types/index.js';
2
+ export declare function defineConfig(config: OtelConfig): OtelConfig;
@@ -0,0 +1,3 @@
1
+ export function defineConfig(config) {
2
+ return config;
3
+ }
@@ -0,0 +1,174 @@
1
+ import type { Attributes, Span } from '@opentelemetry/api';
2
+ import { type HeadersCarrier, type OtelLoggingPresetOptions, type UserContextResult } 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 declare function getCurrentSpan(): Span | undefined;
19
+ /**
20
+ * Set attributes on the currently active span.
21
+ *
22
+ * This is a convenience wrapper around `getCurrentSpan()?.setAttributes()`.
23
+ * Does nothing if there is no active span.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { setAttributes } from '@adonisjs/otel'
28
+ *
29
+ * function processOrder(orderId: string) {
30
+ * setAttributes({
31
+ * 'order.id': orderId,
32
+ * 'order.type': 'subscription',
33
+ * })
34
+ * }
35
+ * ```
36
+ */
37
+ export declare function setAttributes(attributes: Attributes): void;
38
+ /**
39
+ * Record a code section as a span in your traces.
40
+ *
41
+ * Automatically handles:
42
+ * - Creating and closing the span
43
+ * - Capturing exceptions and setting error status
44
+ * - Async/await support
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { record } from '@adonisjs/otel'
49
+ *
50
+ * // Sync
51
+ * const result = record('database.query', () => {
52
+ * return db.query('SELECT * FROM users')
53
+ * })
54
+ *
55
+ * // Async
56
+ * const user = await record('user.fetch', async () => {
57
+ * return await userService.findById(id)
58
+ * })
59
+ *
60
+ * // With attributes
61
+ * const order = await record('order.process', async (span) => {
62
+ * span.setAttributes({ 'order.id': orderId })
63
+ * return await processOrder(orderId)
64
+ * })
65
+ * ```
66
+ */
67
+ export declare function record<T>(name: string, callback: (span: Span) => T): T;
68
+ export declare function handleError(span: Span, error: Error): void;
69
+ /**
70
+ * Set user information on the currently active span.
71
+ *
72
+ * Uses OpenTelemetry semantic conventions for user attributes.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { setUser } from '@adonisjs/otel'
77
+ *
78
+ * // In a controller or middleware
79
+ * setUser({
80
+ * id: auth.user.id,
81
+ * email: auth.user.email,
82
+ * role: auth.user.role,
83
+ * })
84
+ * ```
85
+ */
86
+ export declare function setUser(user: UserContextResult): void;
87
+ /**
88
+ * Inject the current trace context into headers for propagation.
89
+ *
90
+ * Use this when making outgoing HTTP requests, dispatching queue jobs,
91
+ * or any cross-service communication where you want to maintain trace continuity.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { injectTraceContext } from '@adonisjs/otel'
96
+ *
97
+ * // HTTP request to another service
98
+ * const headers = {}
99
+ * injectTraceContext(headers)
100
+ * await fetch('https://api.example.com', { headers })
101
+ *
102
+ * // Queue job dispatch
103
+ * const jobHeaders = {}
104
+ * injectTraceContext(jobHeaders)
105
+ * await queue.dispatch('process-order', { orderId }, { headers: jobHeaders })
106
+ * ```
107
+ */
108
+ export declare function injectTraceContext(headers: HeadersCarrier): void;
109
+ /**
110
+ * Extract trace context from incoming headers.
111
+ *
112
+ * Use this in queue workers, background jobs, or any service receiving
113
+ * requests from another traced service to continue the trace.
114
+ *
115
+ * @returns The extracted context that can be used with `record()` or `trace.startActiveSpan()`
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * import { extractTraceContext, record } from '@adonisjs/otel'
120
+ * import { context } from '@opentelemetry/api'
121
+ *
122
+ * // In a queue worker
123
+ * const extractedContext = extractTraceContext(job.headers)
124
+ *
125
+ * context.with(extractedContext, () => {
126
+ * record('process-job', () => {
127
+ * // This span will be a child of the original trace
128
+ * })
129
+ * })
130
+ * ```
131
+ */
132
+ export declare function extractTraceContext(headers: HeadersCarrier): import("@opentelemetry/api").Context;
133
+ /**
134
+ * Record an event on the currently active span.
135
+ *
136
+ * Events are time-stamped annotations that can be attached to spans
137
+ * to record discrete occurrences during a span's lifetime.
138
+ *
139
+ * @see https://opentelemetry.io/docs/concepts/signals/traces/#span-events
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { recordEvent } from '@adonisjs/otel'
144
+ *
145
+ * // Simple event
146
+ * recordEvent('cache.miss')
147
+ *
148
+ * // Event with attributes
149
+ * recordEvent('order.processed', {
150
+ * 'order.id': orderId,
151
+ * 'order.total': 99.99,
152
+ * })
153
+ * ```
154
+ */
155
+ export declare function recordEvent(name: string, attributes?: Attributes): void;
156
+ /**
157
+ * Preset for pino-pretty to hide OpenTelemetry-injected fields ( in development).
158
+ *
159
+ * By default, hides: pid, hostname, trace_id, span_id, trace_flags, route, request_id, x-request-id
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import { otelLoggingPreset } from '@adonisjs/otel'
164
+ *
165
+ * // Hide all OTEL fields
166
+ * targets.pretty(otelLoggingPreset())
167
+ *
168
+ * // Keep trace context visible
169
+ * targets.pretty(otelLoggingPreset({ keep: ['trace_id', 'span_id'] }))
170
+ * ```
171
+ */
172
+ export declare function otelLoggingPreset(options?: OtelLoggingPresetOptions): {
173
+ ignore: string;
174
+ };