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,197 @@
1
+ /**
2
+ * Zero-dependency structured logger for edge environments
3
+ *
4
+ * This logger is ~100 LOC and provides:
5
+ * - Structured JSON logging
6
+ * - Auto trace context injection (traceId, spanId)
7
+ * - Dynamic log level control (per-request via context)
8
+ * - Level support (info, error, warn, debug)
9
+ * - Zero dependencies (console-based)
10
+ *
11
+ * Unlike Pino/Winston (~500KB), this is <1KB minified!
12
+ */
13
+
14
+ import { trace, context as api_context, createContextKey } from '@opentelemetry/api';
15
+
16
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
17
+
18
+ /**
19
+ * Context key for storing active log level (enables per-request log levels)
20
+ */
21
+ const LOG_LEVEL_KEY = createContextKey('autotel-edge-log-level');
22
+
23
+ export interface EdgeLogger {
24
+ info(msg: string, attrs?: Record<string, any>): void;
25
+ error(msg: string, error?: Error | unknown, attrs?: Record<string, any>): void;
26
+ warn(msg: string, attrs?: Record<string, any>): void;
27
+ debug(msg: string, attrs?: Record<string, any>): void;
28
+ }
29
+
30
+ /**
31
+ * Get the active log level from context (if set)
32
+ * Falls back to undefined if no log level is set in context
33
+ */
34
+ export function getActiveLogLevel(): LogLevel | undefined {
35
+ return api_context.active().getValue(LOG_LEVEL_KEY) as LogLevel | undefined;
36
+ }
37
+
38
+ /**
39
+ * Run a function with a specific log level
40
+ * The log level is stored in OpenTelemetry context and applies to all logger calls within the callback
41
+ *
42
+ * This works in edge runtimes (uses OTel context, not Node.js AsyncLocalStorage)
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Enable debug logging for a specific request
47
+ * runWithLogLevel('debug', () => {
48
+ * log.debug('This will be logged')
49
+ * processRequest()
50
+ * })
51
+ *
52
+ * // Disable logging temporarily
53
+ * runWithLogLevel('none', () => {
54
+ * log.info('This will NOT be logged')
55
+ * })
56
+ * ```
57
+ */
58
+ export function runWithLogLevel<T>(level: LogLevel, callback: () => T): T {
59
+ const ctx = api_context.active().setValue(LOG_LEVEL_KEY, level);
60
+ return api_context.with(ctx, callback);
61
+ }
62
+
63
+ /**
64
+ * Get current trace context from active span
65
+ */
66
+ function getTraceContext():
67
+ | { traceId: string; spanId: string; correlationId: string }
68
+ | null {
69
+ const span = trace.getActiveSpan();
70
+ if (!span) return null;
71
+
72
+ const ctx = span.spanContext();
73
+ return {
74
+ traceId: ctx.traceId,
75
+ spanId: ctx.spanId,
76
+ correlationId: ctx.traceId.slice(0, 16), // First 16 chars for grouping
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Create a lightweight structured logger
82
+ *
83
+ * @param service - Service name for logging
84
+ * @param options - Optional configuration
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const log = createEdgeLogger('user-service')
89
+ *
90
+ * log.info('Creating user', { email: 'test@example.com' })
91
+ * // Output: {"level":"info","service":"user-service","msg":"Creating user",
92
+ * // "email":"test@example.com","traceId":"...","spanId":"..."}
93
+ *
94
+ * // Dynamic log level control per-request
95
+ * runWithLogLevel('debug', () => {
96
+ * log.debug('This will be logged even if logger was created with level: "info"')
97
+ * })
98
+ * ```
99
+ */
100
+ export function createEdgeLogger(
101
+ service: string,
102
+ options?: {
103
+ level?: LogLevel;
104
+ pretty?: boolean; // For development
105
+ },
106
+ ): EdgeLogger {
107
+ const defaultLevel = options?.level || 'info';
108
+ const pretty = options?.pretty || false;
109
+
110
+ const levelPriority: Record<LogLevel, number> = {
111
+ none: -1,
112
+ debug: 0,
113
+ info: 1,
114
+ warn: 2,
115
+ error: 3,
116
+ };
117
+
118
+ const shouldLog = (level: LogLevel): boolean => {
119
+ // Priority: context level > options level > 'info' default
120
+ const activeLevel = getActiveLogLevel() ?? defaultLevel;
121
+
122
+ // 'none' means suppress all logging
123
+ if (activeLevel === 'none') return false;
124
+
125
+ return levelPriority[level] >= levelPriority[activeLevel];
126
+ };
127
+
128
+ const log = (
129
+ level: 'info' | 'error' | 'warn' | 'debug',
130
+ msg: string,
131
+ attrs?: Record<string, any>,
132
+ ) => {
133
+ if (!shouldLog(level)) return;
134
+
135
+ const ctx = getTraceContext();
136
+ const logEntry: Record<string, any> = {
137
+ level,
138
+ service,
139
+ msg,
140
+ ...attrs,
141
+ ...ctx, // Auto-inject traceId, spanId, correlationId
142
+ timestamp: new Date().toISOString(),
143
+ };
144
+
145
+ if (pretty) {
146
+ // Pretty print for development
147
+ const traceInfo = ctx
148
+ ? ` [${ctx.traceId.slice(0, 8)}.../${ctx.spanId.slice(0, 8)}...]`
149
+ : '';
150
+ console.log(
151
+ `[${level.toUpperCase()}]${traceInfo} ${service}: ${msg}`,
152
+ attrs || '',
153
+ );
154
+ } else {
155
+ // Structured JSON for production
156
+ console.log(JSON.stringify(logEntry));
157
+ }
158
+ };
159
+
160
+ return {
161
+ info: (msg: string, attrs?: Record<string, any>) => log('info', msg, attrs),
162
+
163
+ error: (msg: string, error?: Error | unknown, attrs?: Record<string, any>) => {
164
+ const errorAttrs = error instanceof Error
165
+ ? {
166
+ error: error.message,
167
+ stack: error.stack,
168
+ name: error.name,
169
+ ...attrs,
170
+ }
171
+ : { error: String(error), ...attrs };
172
+
173
+ log('error', msg, errorAttrs);
174
+ },
175
+
176
+ warn: (msg: string, attrs?: Record<string, any>) => log('warn', msg, attrs),
177
+
178
+ debug: (msg: string, attrs?: Record<string, any>) => log('debug', msg, attrs),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Helper to get trace context (useful for BYOL - Bring Your Own Logger)
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * import bunyan from 'bunyan'
188
+ * import { getEdgeTraceContext } from 'autotel-edge/api/logger'
189
+ *
190
+ * const bunyanLogger = bunyan.createLogger({ name: 'myapp' })
191
+ * const ctx = getEdgeTraceContext()
192
+ * bunyanLogger.info({ ...ctx, email: 'test@example.com' }, 'Creating user')
193
+ * ```
194
+ */
195
+ export function getEdgeTraceContext() {
196
+ return getTraceContext();
197
+ }
package/src/compose.ts ADDED
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Composition utilities for autotel-edge
3
+ *
4
+ * Helper functions for composing instrumentation and middleware.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { compose } from 'autotel-edge/api/compose'
9
+ * import { instrumentGlobalFetch, instrumentGlobalCache } from 'autotel-edge/instrumentation'
10
+ *
11
+ * const setupInstrumentation = compose(
12
+ * instrumentGlobalFetch,
13
+ * instrumentGlobalCache
14
+ * )
15
+ *
16
+ * setupInstrumentation({ enabled: true })
17
+ * ```
18
+ */
19
+
20
+ /**
21
+ * Compose multiple setup functions into one
22
+ *
23
+ * Takes multiple instrumentation functions and returns a single function
24
+ * that calls them all in order with the same config.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const setup = compose(
29
+ * instrumentGlobalFetch,
30
+ * instrumentGlobalCache
31
+ * )
32
+ *
33
+ * setup({ enabled: true })
34
+ * ```
35
+ */
36
+ export function compose<TConfig = unknown>(
37
+ ...fns: Array<(config?: TConfig) => void>
38
+ ): (config?: TConfig) => void {
39
+ return (config?: TConfig) => {
40
+ for (const fn of fns) {
41
+ fn(config);
42
+ }
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Compose multiple async setup functions into one
48
+ *
49
+ * Like `compose` but for async functions.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const setup = composeAsync(
54
+ * async (config) => { await initTracing(config) },
55
+ * async (config) => { await initMetrics(config) }
56
+ * )
57
+ *
58
+ * await setup({ endpoint: 'https://...' })
59
+ * ```
60
+ */
61
+ export function composeAsync<TConfig = unknown>(
62
+ ...fns: Array<(config?: TConfig) => Promise<void>>
63
+ ): (config?: TConfig) => Promise<void> {
64
+ return async (config?: TConfig) => {
65
+ for (const fn of fns) {
66
+ await fn(config);
67
+ }
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Pipe - compose functions from left to right
73
+ *
74
+ * Unlike `compose` which is right-to-left, pipe is left-to-right
75
+ * which matches the execution order visually.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * const setup = pipe(
80
+ * (config) => ({ ...config, tracing: true }),
81
+ * (config) => ({ ...config, metrics: true }),
82
+ * (config) => initObservability(config)
83
+ * )
84
+ *
85
+ * setup({ service: 'my-worker' })
86
+ * ```
87
+ */
88
+ export function pipe<TInput, TOutput>(
89
+ ...fns: Array<(input: any) => any>
90
+ ): (input: TInput) => TOutput {
91
+ return (input: TInput) => {
92
+ return fns.reduce((acc, fn) => fn(acc), input as any) as TOutput;
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Create a conditional instrumentation function
98
+ *
99
+ * Only runs the instrumentation if the predicate returns true.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const setupFetch = when(
104
+ * (env) => env.ENABLE_FETCH_TRACING === 'true',
105
+ * instrumentGlobalFetch
106
+ * )
107
+ *
108
+ * setupFetch(env)
109
+ * ```
110
+ */
111
+ export function when<TConfig = unknown>(
112
+ predicate: (config?: TConfig) => boolean,
113
+ fn: (config?: TConfig) => void
114
+ ): (config?: TConfig) => void {
115
+ return (config?: TConfig) => {
116
+ if (predicate(config)) {
117
+ fn(config);
118
+ }
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Create a conditional async instrumentation function
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const setupCache = whenAsync(
128
+ * async (env) => await featureEnabled('cache-tracing'),
129
+ * instrumentGlobalCache
130
+ * )
131
+ *
132
+ * await setupCache(env)
133
+ * ```
134
+ */
135
+ export function whenAsync<TConfig = unknown>(
136
+ predicate: (config?: TConfig) => Promise<boolean> | boolean,
137
+ fn: (config?: TConfig) => Promise<void>
138
+ ): (config?: TConfig) => Promise<void> {
139
+ return async (config?: TConfig) => {
140
+ if (await predicate(config)) {
141
+ await fn(config);
142
+ }
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Tap - run a side effect and return the original value
148
+ *
149
+ * Useful for logging or debugging in a pipe.
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const setup = pipe(
154
+ * tap((config) => console.log('Initial config:', config)),
155
+ * (config) => ({ ...config, tracing: true }),
156
+ * tap((config) => console.log('After tracing:', config)),
157
+ * (config) => initObservability(config)
158
+ * )
159
+ * ```
160
+ */
161
+ export function tap<T>(fn: (value: T) => void): (value: T) => T {
162
+ return (value: T) => {
163
+ fn(value);
164
+ return value;
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Memoize - cache the result of a function
170
+ *
171
+ * Useful for expensive initialization functions that should only run once.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const setup = memoize(() => {
176
+ * console.log('Setting up (expensive)...')
177
+ * instrumentGlobalFetch()
178
+ * instrumentGlobalCache()
179
+ * })
180
+ *
181
+ * setup() // Logs "Setting up (expensive)..."
182
+ * setup() // Does nothing (cached)
183
+ * setup() // Does nothing (cached)
184
+ * ```
185
+ */
186
+ export function memoize<TArgs extends any[], TReturn>(
187
+ fn: (...args: TArgs) => TReturn
188
+ ): (...args: TArgs) => TReturn {
189
+ const cached: { hasValue: boolean; value: TReturn } = { hasValue: false, value: undefined as any };
190
+
191
+ return (...args: TArgs) => {
192
+ if (!cached.hasValue) {
193
+ cached.value = fn(...args);
194
+ cached.hasValue = true;
195
+ }
196
+ return cached.value;
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Retry - retry a function on failure
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const setupWithRetry = retry(
206
+ * async () => {
207
+ * await fetch('https://api.example.com/init')
208
+ * },
209
+ * { maxAttempts: 3, delayMs: 1000 }
210
+ * )
211
+ *
212
+ * await setupWithRetry()
213
+ * ```
214
+ */
215
+ export function retry<TArgs extends any[], TReturn>(
216
+ fn: (...args: TArgs) => Promise<TReturn>,
217
+ options: {
218
+ maxAttempts?: number;
219
+ delayMs?: number;
220
+ onRetry?: (attempt: number, error: Error) => void;
221
+ } = {}
222
+ ): (...args: TArgs) => Promise<TReturn> {
223
+ const { maxAttempts = 3, delayMs = 1000, onRetry } = options;
224
+
225
+ return async (...args: TArgs) => {
226
+ let lastError: Error | undefined;
227
+
228
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
229
+ try {
230
+ return await fn(...args);
231
+ } catch (error) {
232
+ lastError = error instanceof Error ? error : new Error(String(error));
233
+
234
+ if (attempt < maxAttempts) {
235
+ onRetry?.(attempt, lastError);
236
+ await new Promise(resolve => setTimeout(resolve, delayMs));
237
+ }
238
+ }
239
+ }
240
+
241
+ throw lastError;
242
+ };
243
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Buffer polyfill for edge environments
3
+ *
4
+ * Cloudflare Workers and other edge runtimes need the Buffer global
5
+ * for OpenTelemetry OTLP serialization.
6
+ */
7
+
8
+ //@ts-ignore - node:buffer available in CF Workers with nodejs_compat
9
+ import { Buffer } from 'node:buffer';
10
+
11
+ //@ts-ignore
12
+ globalThis.Buffer = Buffer;
13
+
14
+
15
+
16
+ export {Buffer} from 'node:buffer';