autotel-edge 3.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/chunk-F32WSLNX.js +309 -0
  4. package/dist/chunk-F32WSLNX.js.map +1 -0
  5. package/dist/events.d.ts +86 -0
  6. package/dist/events.js +157 -0
  7. package/dist/events.js.map +1 -0
  8. package/dist/index.d.ts +326 -0
  9. package/dist/index.js +921 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/logger.d.ts +89 -0
  12. package/dist/logger.js +81 -0
  13. package/dist/logger.js.map +1 -0
  14. package/dist/sampling.d.ts +166 -0
  15. package/dist/sampling.js +108 -0
  16. package/dist/sampling.js.map +1 -0
  17. package/dist/testing.d.ts +2 -0
  18. package/dist/testing.js +3 -0
  19. package/dist/testing.js.map +1 -0
  20. package/dist/types-Dj85cPUj.d.ts +182 -0
  21. package/package.json +88 -0
  22. package/src/api/logger.test.ts +367 -0
  23. package/src/api/logger.ts +197 -0
  24. package/src/compose.ts +243 -0
  25. package/src/core/buffer.ts +16 -0
  26. package/src/core/config.test.ts +388 -0
  27. package/src/core/config.ts +167 -0
  28. package/src/core/context.ts +224 -0
  29. package/src/core/exporter.ts +99 -0
  30. package/src/core/provider.ts +45 -0
  31. package/src/core/span.ts +222 -0
  32. package/src/core/spanprocessor.test.ts +521 -0
  33. package/src/core/spanprocessor.ts +232 -0
  34. package/src/core/trace-context.ts +66 -0
  35. package/src/core/tracer.test.ts +123 -0
  36. package/src/core/tracer.ts +216 -0
  37. package/src/events/index.test.ts +242 -0
  38. package/src/events/index.ts +338 -0
  39. package/src/events.ts +6 -0
  40. package/src/functional.test.ts +702 -0
  41. package/src/functional.ts +846 -0
  42. package/src/index.ts +81 -0
  43. package/src/logger.ts +13 -0
  44. package/src/sampling/index.test.ts +297 -0
  45. package/src/sampling/index.ts +276 -0
  46. package/src/sampling.ts +6 -0
  47. package/src/testing/index.ts +9 -0
  48. package/src/testing.ts +6 -0
  49. package/src/types.ts +267 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * AsyncLocalStorage-based context manager for edge environments
3
+ *
4
+ * Copyright The OpenTelemetry Authors
5
+ * Licensed under the Apache License, Version 2.0
6
+ */
7
+
8
+ import type { ContextManager, Context } from '@opentelemetry/api';
9
+ import { ROOT_CONTEXT } from '@opentelemetry/api';
10
+
11
+ //@ts-ignore - node:async_hooks available in CF Workers with nodejs_compat
12
+ import { AsyncLocalStorage } from 'node:async_hooks';
13
+ //@ts-ignore
14
+ import { EventEmitter } from 'node:events';
15
+
16
+ type Func<T> = (...args: unknown[]) => T;
17
+
18
+ /**
19
+ * Store a map for each event of all original listeners and their "patched"
20
+ * version. So when a listener is removed by the user, the corresponding
21
+ * patched function will be also removed.
22
+ */
23
+ interface PatchMap {
24
+ [name: string]: WeakMap<Func<void>, Func<void>>;
25
+ }
26
+
27
+ const ADD_LISTENER_METHODS = [
28
+ 'addListener' as const,
29
+ 'on' as const,
30
+ 'once' as const,
31
+ 'prependListener' as const,
32
+ 'prependOnceListener' as const,
33
+ ];
34
+
35
+ abstract class AbstractAsyncHooksContextManager implements ContextManager {
36
+ abstract active(): Context;
37
+
38
+ abstract with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
39
+ context: Context,
40
+ fn: F,
41
+ thisArg?: ThisParameterType<F>,
42
+ ...args: A
43
+ ): ReturnType<F>;
44
+
45
+ abstract enable(): this;
46
+
47
+ abstract disable(): this;
48
+
49
+ /**
50
+ * Binds a context to the target function or event emitter
51
+ */
52
+ bind<T>(context: Context, target: T): T {
53
+ if (target instanceof EventEmitter) {
54
+ return this._bindEventEmitter(context, target);
55
+ }
56
+
57
+ if (typeof target === 'function') {
58
+ return this._bindFunction(context, target);
59
+ }
60
+ return target;
61
+ }
62
+
63
+ private _bindFunction<T extends Function>(context: Context, target: T): T {
64
+ const manager = this;
65
+ const contextWrapper = function (this: never, ...args: unknown[]) {
66
+ return manager.with(context, () => target.apply(this, args));
67
+ };
68
+ Object.defineProperty(contextWrapper, 'length', {
69
+ enumerable: false,
70
+ configurable: true,
71
+ writable: false,
72
+ value: target.length,
73
+ });
74
+ return contextWrapper as any;
75
+ }
76
+
77
+ /**
78
+ * By default, EventEmitter calls callbacks with their context, which we do
79
+ * not want. Instead we bind a specific context to all callbacks.
80
+ */
81
+ private _bindEventEmitter<T extends EventEmitter>(
82
+ context: Context,
83
+ ee: T,
84
+ ): T {
85
+ const map = this._getPatchMap(ee);
86
+ if (map !== undefined) return ee;
87
+ this._createPatchMap(ee);
88
+
89
+ // Patch methods that add a listener to propagate context
90
+ for (const methodName of ADD_LISTENER_METHODS) {
91
+ if (ee[methodName] === undefined) continue;
92
+ ee[methodName] = this._patchAddListener(ee, ee[methodName], context);
93
+ }
94
+
95
+ // Patch methods that remove a listener
96
+ if (typeof ee.removeListener === 'function') {
97
+ ee.removeListener = this._patchRemoveListener(ee, ee.removeListener);
98
+ }
99
+ if (typeof ee.off === 'function') {
100
+ ee.off = this._patchRemoveListener(ee, ee.off);
101
+ }
102
+
103
+ // Patch method that removes all listeners
104
+ if (typeof ee.removeAllListeners === 'function') {
105
+ ee.removeAllListeners = this._patchRemoveAllListeners(
106
+ ee,
107
+ ee.removeAllListeners,
108
+ );
109
+ }
110
+ return ee;
111
+ }
112
+
113
+ private _patchRemoveListener(ee: EventEmitter, original: Function) {
114
+ const contextManager = this;
115
+ return function (this: never, event: string, listener: Func<void>) {
116
+ const events = contextManager._getPatchMap(ee)?.[event];
117
+ if (events === undefined) {
118
+ return original.call(this, event, listener);
119
+ }
120
+ const patchedListener = events.get(listener);
121
+ return original.call(this, event, patchedListener || listener);
122
+ };
123
+ }
124
+
125
+ private _patchRemoveAllListeners(ee: EventEmitter, original: Function) {
126
+ const contextManager = this;
127
+ return function (this: never, event: string) {
128
+ const map = contextManager._getPatchMap(ee);
129
+ if (map !== undefined) {
130
+ if (arguments.length === 0) {
131
+ contextManager._createPatchMap(ee);
132
+ } else if (map[event] !== undefined) {
133
+ delete map[event];
134
+ }
135
+ }
136
+ return Reflect.apply(original, this, arguments);
137
+ };
138
+ }
139
+
140
+ private _patchAddListener(
141
+ ee: EventEmitter,
142
+ original: Function,
143
+ context: Context,
144
+ ) {
145
+ const contextManager = this;
146
+ return function (this: never, event: string, listener: Func<void>) {
147
+ /**
148
+ * This check prevents double-wrapping the listener.
149
+ * The implementation for ee.once wraps the listener and calls ee.on.
150
+ * Without this check, we would wrap that wrapped listener.
151
+ */
152
+ if (contextManager._wrapped) {
153
+ return original.call(this, event, listener);
154
+ }
155
+ let map = contextManager._getPatchMap(ee);
156
+ if (map === undefined) {
157
+ map = contextManager._createPatchMap(ee);
158
+ }
159
+ let listeners = map[event];
160
+ if (listeners === undefined) {
161
+ listeners = new WeakMap();
162
+ map[event] = listeners;
163
+ }
164
+ const patchedListener = contextManager.bind(context, listener);
165
+ // Store a weak reference of the user listener to ours
166
+ listeners.set(listener, patchedListener);
167
+
168
+ contextManager._wrapped = true;
169
+ try {
170
+ return original.call(this, event, patchedListener);
171
+ } finally {
172
+ contextManager._wrapped = false;
173
+ }
174
+ };
175
+ }
176
+
177
+ private _createPatchMap(ee: EventEmitter): PatchMap {
178
+ const map = Object.create(null);
179
+ (ee as any)[this._kOtListeners] = map;
180
+ return map;
181
+ }
182
+
183
+ private _getPatchMap(ee: EventEmitter): PatchMap | undefined {
184
+ return (ee as never)[this._kOtListeners];
185
+ }
186
+
187
+ private readonly _kOtListeners = Symbol('OtListeners');
188
+ private _wrapped = false;
189
+ }
190
+
191
+ /**
192
+ * AsyncLocalStorage-based context manager for edge runtimes
193
+ */
194
+ export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextManager {
195
+ private _asyncLocalStorage: AsyncLocalStorage<Context>;
196
+
197
+ constructor() {
198
+ super();
199
+ this._asyncLocalStorage = new AsyncLocalStorage();
200
+ }
201
+
202
+ active(): Context {
203
+ return this._asyncLocalStorage.getStore() ?? ROOT_CONTEXT;
204
+ }
205
+
206
+ with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
207
+ context: Context,
208
+ fn: F,
209
+ thisArg?: ThisParameterType<F>,
210
+ ...args: A
211
+ ): ReturnType<F> {
212
+ const cb = thisArg == null ? fn : fn.bind(thisArg);
213
+ return this._asyncLocalStorage.run(context, cb as never, ...args);
214
+ }
215
+
216
+ enable(): this {
217
+ return this;
218
+ }
219
+
220
+ disable(): this {
221
+ this._asyncLocalStorage.disable();
222
+ return this;
223
+ }
224
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Lightweight OTLP exporter for edge environments
3
+ * Ported and adapted from @microlabs/
4
+ *
5
+ * This exporter is much smaller than the standard @opentelemetry/exporter-trace-otlp-http
6
+ * because it uses fetch() directly instead of Node.js http/https modules.
7
+ */
8
+
9
+ import type { ExportResult } from '@opentelemetry/core';
10
+ import { ExportResultCode } from '@opentelemetry/core';
11
+ import { OTLPExporterError } from '@opentelemetry/otlp-exporter-base';
12
+ import { JsonTraceSerializer } from '@opentelemetry/otlp-transformer';
13
+ import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
14
+ import type { OTLPExporterConfig } from '../types';
15
+
16
+ // Version is injected at build time via tsup define
17
+ // This avoids runtime filesystem access which isn't available in edge environments
18
+ const PACKAGE_VERSION = process.env.AUTOLEMETRY_EDGE_VERSION || '0.1.1';
19
+
20
+ const defaultHeaders: Record<string, string> = {
21
+ accept: 'application/json',
22
+ 'content-type': 'application/json',
23
+ 'user-agent': `autotel-edge v${PACKAGE_VERSION}`,
24
+ };
25
+
26
+ /**
27
+ * Minimal OTLP exporter using fetch()
28
+ */
29
+ export class OTLPExporter implements SpanExporter {
30
+ private headers: Record<string, string>;
31
+ private url: string;
32
+
33
+ constructor(config: OTLPExporterConfig) {
34
+ this.url = config.url;
35
+ this.headers = Object.assign({}, defaultHeaders, config.headers);
36
+ }
37
+
38
+ export(items: any[], resultCallback: (result: ExportResult) => void): void {
39
+ this._export(items)
40
+ .then(() => {
41
+ resultCallback({ code: ExportResultCode.SUCCESS });
42
+ })
43
+ .catch((error) => {
44
+ resultCallback({ code: ExportResultCode.FAILED, error });
45
+ });
46
+ }
47
+
48
+ private _export(items: any[]): Promise<unknown> {
49
+ return new Promise<void>((resolve, reject) => {
50
+ try {
51
+ this.send(items, resolve, reject);
52
+ } catch (error) {
53
+ reject(error);
54
+ }
55
+ });
56
+ }
57
+
58
+ send(
59
+ items: any[],
60
+ onSuccess: () => void,
61
+ onError: (error: OTLPExporterError) => void,
62
+ ): void {
63
+ const decoder = new TextDecoder();
64
+ const exportMessage = JsonTraceSerializer.serializeRequest(items);
65
+
66
+ const body = decoder.decode(exportMessage);
67
+ const params: RequestInit = {
68
+ method: 'POST',
69
+ headers: this.headers,
70
+ body,
71
+ };
72
+
73
+ fetch(this.url, params)
74
+ .then((response) => {
75
+ if (response.ok) {
76
+ onSuccess();
77
+ } else {
78
+ onError(
79
+ new OTLPExporterError(
80
+ `Exporter received a statusCode: ${response.status}`,
81
+ ),
82
+ );
83
+ }
84
+ })
85
+ .catch((error) => {
86
+ onError(
87
+ new OTLPExporterError(
88
+ `Exception during export: ${error.toString()}`,
89
+ error.code,
90
+ error.stack,
91
+ ),
92
+ );
93
+ });
94
+ }
95
+
96
+ async shutdown(): Promise<void> {
97
+ // No-op for edge environments
98
+ }
99
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Tracer provider for edge environments
3
+ */
4
+
5
+ import { trace } from '@opentelemetry/api';
6
+ import type { Resource } from '@opentelemetry/resources';
7
+ import type { SpanProcessor, TracerConfig } from '@opentelemetry/sdk-trace-base';
8
+ import { WorkerTracer } from './tracer';
9
+ import { AsyncLocalStorageContextManager } from './context';
10
+
11
+ /**
12
+ * WorkerTracerProvider - Registers tracer globally
13
+ */
14
+ export class WorkerTracerProvider {
15
+ private tracer: WorkerTracer;
16
+ private contextManager: AsyncLocalStorageContextManager;
17
+
18
+ constructor(spanProcessors: SpanProcessor[], resource: Resource) {
19
+ this.tracer = new WorkerTracer(spanProcessors, resource);
20
+ this.contextManager = new AsyncLocalStorageContextManager();
21
+ }
22
+
23
+ /**
24
+ * Get the tracer instance
25
+ */
26
+ getTracer(_name: string, _version?: string, _config?: TracerConfig): WorkerTracer {
27
+ return this.tracer;
28
+ }
29
+
30
+ /**
31
+ * Register this provider as the global tracer
32
+ */
33
+ register(): void {
34
+ // Enable context manager
35
+ this.contextManager.enable();
36
+
37
+ // Set tracer provider
38
+ const provider = {
39
+ getTracer: (_name: string, _version?: string) => this.tracer,
40
+ };
41
+
42
+ // @ts-ignore - OTel types
43
+ trace.setGlobalTracerProvider(provider);
44
+ }
45
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Lightweight Span implementation for edge environments
3
+ */
4
+
5
+ import type {
6
+ Attributes,
7
+ AttributeValue,
8
+ Exception,
9
+ HrTime,
10
+ Link,
11
+ Span,
12
+ SpanContext,
13
+ SpanKind,
14
+ SpanStatus,
15
+ SpanStatusCode,
16
+ TimeInput,
17
+ } from '@opentelemetry/api';
18
+ import {
19
+ hrTimeDuration,
20
+ type InstrumentationScope,
21
+ isAttributeValue,
22
+ isTimeInput,
23
+ sanitizeAttributes,
24
+ } from '@opentelemetry/core';
25
+ import type { Resource } from '@opentelemetry/resources';
26
+ import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base';
27
+ import { SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_EXCEPTION_STACKTRACE, SEMATTRS_EXCEPTION_TYPE } from '@opentelemetry/semantic-conventions';
28
+
29
+ type OnSpanEnd = (span: Span) => void;
30
+
31
+ interface SpanInit {
32
+ attributes: unknown;
33
+ name: string;
34
+ onEnd: OnSpanEnd;
35
+ resource: Resource;
36
+ spanContext: SpanContext;
37
+ parentSpanContext?: SpanContext;
38
+ links?: Link[];
39
+ parentSpanId?: string;
40
+ spanKind?: SpanKind;
41
+ startTime?: TimeInput;
42
+ }
43
+
44
+ function transformExceptionAttributes(exception: Exception): Attributes {
45
+ const attributes: Attributes = {};
46
+ if (typeof exception === 'string') {
47
+ attributes[SEMATTRS_EXCEPTION_MESSAGE] = exception;
48
+ } else {
49
+ if (exception.code) {
50
+ attributes[SEMATTRS_EXCEPTION_TYPE] = exception.code.toString();
51
+ } else if (exception.name) {
52
+ attributes[SEMATTRS_EXCEPTION_TYPE] = exception.name;
53
+ }
54
+ if (exception.message) {
55
+ attributes[SEMATTRS_EXCEPTION_MESSAGE] = exception.message;
56
+ }
57
+ if (exception.stack) {
58
+ attributes[SEMATTRS_EXCEPTION_STACKTRACE] = exception.stack;
59
+ }
60
+ }
61
+ return attributes;
62
+ }
63
+
64
+ function millisToHr(millis: number): HrTime {
65
+ return [Math.trunc(millis / 1000), (millis % 1000) * 1e6];
66
+ }
67
+
68
+ function getHrTime(input?: TimeInput): HrTime {
69
+ const now = Date.now();
70
+ if (!input) {
71
+ return millisToHr(now);
72
+ } else if (input instanceof Date) {
73
+ return millisToHr(input.getTime());
74
+ } else if (typeof input === 'number') {
75
+ return millisToHr(input);
76
+ } else if (Array.isArray(input)) {
77
+ return input;
78
+ }
79
+
80
+ const v: never = input;
81
+ throw new Error(`unreachable value: ${JSON.stringify(v)}`);
82
+ }
83
+
84
+ // Previously exported from OTel, now private
85
+ function isAttributeKey(key: unknown): key is string {
86
+ return typeof key === 'string' && key.length > 0;
87
+ }
88
+
89
+ /**
90
+ * Lightweight Span implementation for edge runtimes
91
+ */
92
+ export class SpanImpl implements Span, ReadableSpan {
93
+ name: string;
94
+ private readonly _spanContext: SpanContext;
95
+ private readonly onEnd: OnSpanEnd;
96
+ readonly parentSpanId?: string;
97
+ readonly parentSpanContext?: SpanContext | undefined;
98
+ readonly kind: SpanKind;
99
+ readonly attributes: Attributes;
100
+ status: SpanStatus = {
101
+ code: 0 as SpanStatusCode, // SpanStatusCode.UNSET
102
+ };
103
+ endTime: HrTime = [0, 0];
104
+ private _duration: HrTime = [0, 0];
105
+ readonly startTime: HrTime;
106
+ readonly events: TimedEvent[] = [];
107
+ readonly links: Link[];
108
+ readonly resource: Resource;
109
+ instrumentationScope: InstrumentationScope = {
110
+ name: 'autotel-edge',
111
+ };
112
+ private _ended: boolean = false;
113
+ private _droppedAttributesCount: number = 0;
114
+ private _droppedEventsCount: number = 0;
115
+ private _droppedLinksCount: number = 0;
116
+
117
+ constructor(init: SpanInit) {
118
+ this.name = init.name;
119
+ this._spanContext = init.spanContext;
120
+ this.parentSpanId = init.parentSpanId;
121
+ this.parentSpanContext = init.parentSpanContext;
122
+ this.kind = init.spanKind || (0 as SpanKind); // SpanKind.INTERNAL
123
+ this.attributes = sanitizeAttributes(init.attributes);
124
+ this.startTime = getHrTime(init.startTime);
125
+ this.links = init.links || [];
126
+ this.resource = init.resource;
127
+ this.onEnd = init.onEnd;
128
+ }
129
+
130
+ addLink(link: Link): this {
131
+ this.links.push(link);
132
+ return this;
133
+ }
134
+
135
+ addLinks(links: Link[]): this {
136
+ this.links.push(...links);
137
+ return this;
138
+ }
139
+
140
+ spanContext(): SpanContext {
141
+ return this._spanContext;
142
+ }
143
+
144
+ setAttribute(key: string, value?: AttributeValue): this {
145
+ if (isAttributeKey(key) && isAttributeValue(value)) {
146
+ this.attributes[key] = value;
147
+ }
148
+ return this;
149
+ }
150
+
151
+ setAttributes(attributes: Attributes): this {
152
+ for (const [key, value] of Object.entries(attributes)) {
153
+ this.setAttribute(key, value);
154
+ }
155
+ return this;
156
+ }
157
+
158
+ addEvent(
159
+ name: string,
160
+ attributesOrStartTime?: Attributes | TimeInput,
161
+ startTime?: TimeInput,
162
+ ): this {
163
+ if (isTimeInput(attributesOrStartTime)) {
164
+ startTime = attributesOrStartTime;
165
+ attributesOrStartTime = undefined;
166
+ }
167
+
168
+ const attributes = sanitizeAttributes(attributesOrStartTime);
169
+ const time = getHrTime(startTime);
170
+ this.events.push({ name, attributes, time });
171
+ return this;
172
+ }
173
+
174
+ setStatus(status: SpanStatus): this {
175
+ this.status = status;
176
+ return this;
177
+ }
178
+
179
+ updateName(name: string): this {
180
+ this.name = name;
181
+ return this;
182
+ }
183
+
184
+ end(endTime?: TimeInput): void {
185
+ if (this._ended) {
186
+ return;
187
+ }
188
+ this._ended = true;
189
+ this.endTime = getHrTime(endTime);
190
+ this._duration = hrTimeDuration(this.startTime, this.endTime);
191
+ this.onEnd(this);
192
+ }
193
+
194
+ isRecording(): boolean {
195
+ return !this._ended;
196
+ }
197
+
198
+ recordException(exception: Exception, time?: TimeInput): void {
199
+ const attributes = transformExceptionAttributes(exception);
200
+ this.addEvent('exception', attributes, time);
201
+ }
202
+
203
+ get duration(): HrTime {
204
+ return this._duration;
205
+ }
206
+
207
+ get ended(): boolean {
208
+ return this._ended;
209
+ }
210
+
211
+ get droppedAttributesCount(): number {
212
+ return this._droppedAttributesCount;
213
+ }
214
+
215
+ get droppedEventsCount(): number {
216
+ return this._droppedEventsCount;
217
+ }
218
+
219
+ get droppedLinksCount(): number {
220
+ return this._droppedLinksCount;
221
+ }
222
+ }