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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/chunk-F32WSLNX.js +309 -0
- package/dist/chunk-F32WSLNX.js.map +1 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +157 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +326 -0
- package/dist/index.js +921 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +89 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/sampling.d.ts +166 -0
- package/dist/sampling.js +108 -0
- package/dist/sampling.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-Dj85cPUj.d.ts +182 -0
- package/package.json +88 -0
- package/src/api/logger.test.ts +367 -0
- package/src/api/logger.ts +197 -0
- package/src/compose.ts +243 -0
- package/src/core/buffer.ts +16 -0
- package/src/core/config.test.ts +388 -0
- package/src/core/config.ts +167 -0
- package/src/core/context.ts +224 -0
- package/src/core/exporter.ts +99 -0
- package/src/core/provider.ts +45 -0
- package/src/core/span.ts +222 -0
- package/src/core/spanprocessor.test.ts +521 -0
- package/src/core/spanprocessor.ts +232 -0
- package/src/core/trace-context.ts +66 -0
- package/src/core/tracer.test.ts +123 -0
- package/src/core/tracer.ts +216 -0
- package/src/events/index.test.ts +242 -0
- package/src/events/index.ts +338 -0
- package/src/events.ts +6 -0
- package/src/functional.test.ts +702 -0
- package/src/functional.ts +846 -0
- package/src/index.ts +81 -0
- package/src/logger.ts +13 -0
- package/src/sampling/index.test.ts +297 -0
- package/src/sampling/index.ts +276 -0
- package/src/sampling.ts +6 -0
- package/src/testing/index.ts +9 -0
- package/src/testing.ts +6 -0
- package/src/types.ts +267 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sampling/index.ts"],"names":[],"mappings":";AA2EO,SAAS,yBAAA,CACd,OAAA,GAAkC,EAAC,EACrB;AACd,EAAA,MAAM,kBAAA,GAAqB,QAAQ,kBAAA,IAAsB,GAAA;AACzD,EAAA,MAAM,eAAA,GAAkB,QAAQ,eAAA,IAAmB,GAAA;AACnD,EAAA,MAAM,kBAAA,GAAqB,QAAQ,kBAAA,IAAsB,IAAA;AACzD,EAAA,MAAM,gBAAA,GAAmB,QAAQ,gBAAA,IAAoB,IAAA;AAErD,EAAA,IAAI,kBAAA,GAAqB,CAAA,IAAK,kBAAA,GAAqB,CAAA,EAAG;AACpD,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,EAChE;AAGA,EAAA,MAAM,iBAAA,uBAAwB,GAAA,EAAqB;AAEnD,EAAA,OAAO,CAAC,SAAA,KAAmC;AACzC,IAAA,MAAM,EAAE,OAAA,EAAS,aAAA,EAAc,GAAI,SAAA;AAGnC,IAAA,IAAI,CAAC,iBAAA,CAAkB,GAAA,CAAI,OAAO,CAAA,EAAG;AACnC,MAAA,iBAAA,CAAkB,GAAA,CAAI,OAAA,EAAS,IAAA,CAAK,MAAA,KAAW,kBAAkB,CAAA;AAAA,IACnE;AACA,IAAA,MAAM,gBAAA,GAAmB,iBAAA,CAAkB,GAAA,CAAI,OAAO,CAAA;AAGtD,IAAA,IAAI,kBAAA,IAAsB,aAAA,CAAc,MAAA,CAAO,IAAA,KAAS,CAAA,EAAG;AACzD,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,gBAAA,EAAkB;AACpB,MAAA,MAAM,QAAA,GAAW,cAAc,aAAa,CAAA;AAC5C,MAAA,IAAI,YAAY,eAAA,EAAiB;AAC/B,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,OAAO,gBAAA;AAAA,EACT,CAAA;AACF;AAYO,SAAS,wBAAwB,UAAA,EAAkC;AACxE,EAAA,IAAI,UAAA,GAAa,CAAA,IAAK,UAAA,GAAa,CAAA,EAAG;AACpC,IAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AAAA,EACvD;AAEA,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAqB;AAE3C,EAAA,OAAO,CAAC,SAAA,KAAmC;AACzC,IAAA,MAAM,EAAE,SAAQ,GAAI,SAAA;AAEpB,IAAA,IAAI,CAAC,SAAA,CAAU,GAAA,CAAI,OAAO,CAAA,EAAG;AAC3B,MAAA,SAAA,CAAU,GAAA,CAAI,OAAA,EAAS,IAAA,CAAK,MAAA,KAAW,UAAU,CAAA;AAAA,IACnD;AAEA,IAAA,OAAO,SAAA,CAAU,IAAI,OAAO,CAAA;AAAA,EAC9B,CAAA;AACF;AAYO,SAAS,0BAAA,GAA2C;AACzD,EAAA,OAAO,CAAC,SAAA,KAAmC;AAEzC,IAAA,OAAO,SAAA,CAAU,aAAA,CAAc,MAAA,CAAO,IAAA,KAAS,CAAA;AAAA,EACjD,CAAA;AACF;AAYO,SAAS,0BAA0B,WAAA,EAAmC;AAC3E,EAAA,OAAO,CAAC,SAAA,KAAmC;AACzC,IAAA,MAAM,QAAA,GAAW,aAAA,CAAc,SAAA,CAAU,aAAa,CAAA;AACtD,IAAA,OAAO,QAAA,IAAY,WAAA;AAAA,EACrB,CAAA;AACF;AAgBO,SAAS,uBAAuB,QAAA,EAAwC;AAC7E,EAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,EACrE;AAEA,EAAA,OAAO,CAAC,SAAA,KAAmC;AACzC,IAAA,OAAO,SAAS,IAAA,CAAK,CAAC,OAAA,KAAY,OAAA,CAAQ,SAAS,CAAC,CAAA;AAAA,EACtD,CAAA;AACF;AAcO,SAAS,wBACd,SAAA,EACc;AACd,EAAA,OAAO,SAAA;AACT;AAKA,SAAS,cAAc,IAAA,EAA4B;AACjD,EAAA,MAAM,KAAA,GAAQ,KAAK,SAAA,CAAU,CAAC,IAAI,GAAA,GAAO,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA,GAAI,GAAA;AAC7D,EAAA,MAAM,GAAA,GAAM,KAAK,OAAA,CAAQ,CAAC,IAAI,GAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,CAAC,CAAA,GAAI,GAAA;AACvD,EAAA,OAAO,GAAA,GAAM,KAAA;AACf;AAKO,IAAM,eAAA,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7B,UAAA,EAAY,MACV,yBAAA,CAA0B;AAAA,IACxB,kBAAA,EAAoB,GAAA;AAAA,IACpB,eAAA,EAAiB;AAAA,GAClB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMH,WAAA,EAAa,MACX,yBAAA,CAA0B;AAAA,IACxB,kBAAA,EAAoB,IAAA;AAAA,IACpB,eAAA,EAAiB;AAAA,GAClB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMH,SAAA,EAAW,MACT,yBAAA,CAA0B;AAAA,IACxB,kBAAA,EAAoB,GAAA;AAAA,IACpB,eAAA,EAAiB;AAAA,GAClB,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMH,WAAA,EAAa,MAAoB,MAAM,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMvC,UAAA,EAAY,MAAoB,0BAAA;AAClC","file":"sampling.js","sourcesContent":["/**\n * Sampling strategies for autotel-edge\n *\n * Provides intelligent sampling to reduce telemetry costs while capturing critical data.\n *\n * Key strategies:\n * - Always trace errors and slow requests (critical for debugging)\n * - Adaptive sampling based on load\n * - Baseline random sampling for normal traffic\n *\n * @example\n * ```typescript\n * import { createAdaptiveTailSampler } from 'autotel-edge/sampling'\n *\n * export default instrument(handler, {\n * sampling: {\n * tailSampler: createAdaptiveTailSampler({\n * baselineSampleRate: 0.1, // 10% of normal requests\n * slowThresholdMs: 1000, // Requests > 1s are \"slow\"\n * })\n * }\n * })\n * ```\n */\n\nimport type { TailSampleFn, LocalTrace } from '../types';\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-base';\n\nexport interface AdaptiveSamplerOptions {\n /**\n * Baseline sample rate for normal (successful, fast) requests\n * @default 0.1 (10%)\n */\n baselineSampleRate?: number;\n\n /**\n * Threshold in milliseconds for \"slow\" requests\n * Requests taking longer than this will always be trace\n * @default 1000ms\n */\n slowThresholdMs?: number;\n\n /**\n * Always trace error spans\n * @default true\n */\n alwaysSampleErrors?: boolean;\n\n /**\n * Always trace slow spans\n * @default true\n */\n alwaysSampleSlow?: boolean;\n}\n\n/**\n * Create an adaptive tail sampler\n *\n * This sampler ensures you never miss critical issues while keeping costs down:\n * - Always traces errors (status code = ERROR)\n * - Always traces slow requests (duration >= slowThresholdMs)\n * - Uses baseline sample rate for successful fast requests\n *\n * **Recommended for production use.**\n *\n * @example\n * ```typescript\n * const tailSampler = createAdaptiveTailSampler({\n * baselineSampleRate: 0.1, // 10% of normal requests\n * slowThresholdMs: 1000, // Requests > 1s are \"slow\"\n * alwaysSampleErrors: true, // Always trace errors\n * alwaysSampleSlow: true // Always trace slow requests\n * })\n * ```\n */\nexport function createAdaptiveTailSampler(\n options: AdaptiveSamplerOptions = {},\n): TailSampleFn {\n const baselineSampleRate = options.baselineSampleRate ?? 0.1;\n const slowThresholdMs = options.slowThresholdMs ?? 1000;\n const alwaysSampleErrors = options.alwaysSampleErrors ?? true;\n const alwaysSampleSlow = options.alwaysSampleSlow ?? true;\n\n if (baselineSampleRate < 0 || baselineSampleRate > 1) {\n throw new Error('Baseline sample rate must be between 0 and 1');\n }\n\n // Store baseline decisions using trace ID\n const baselineDecisions = new Map<string, boolean>();\n\n return (traceInfo: LocalTrace): boolean => {\n const { traceId, localRootSpan } = traceInfo;\n\n // Get or create baseline decision for this trace\n if (!baselineDecisions.has(traceId)) {\n baselineDecisions.set(traceId, Math.random() < baselineSampleRate);\n }\n const baselineDecision = baselineDecisions.get(traceId)!;\n\n // Always keep errors (SpanStatusCode.ERROR = 2)\n if (alwaysSampleErrors && localRootSpan.status.code === 2) {\n return true;\n }\n\n // Always keep slow requests\n if (alwaysSampleSlow) {\n const duration = getDurationMs(localRootSpan);\n if (duration >= slowThresholdMs) {\n return true;\n }\n }\n\n // Otherwise, use baseline decision\n return baselineDecision;\n };\n}\n\n/**\n * Create a simple random tail sampler\n *\n * Samples a fixed percentage of all traces regardless of outcome.\n *\n * @example\n * ```typescript\n * const tailSampler = createRandomTailSampler(0.1) // 10% of all requests\n * ```\n */\nexport function createRandomTailSampler(sampleRate: number): TailSampleFn {\n if (sampleRate < 0 || sampleRate > 1) {\n throw new Error('Sample rate must be between 0 and 1');\n }\n\n const decisions = new Map<string, boolean>();\n\n return (traceInfo: LocalTrace): boolean => {\n const { traceId } = traceInfo;\n\n if (!decisions.has(traceId)) {\n decisions.set(traceId, Math.random() < sampleRate);\n }\n\n return decisions.get(traceId)!;\n };\n}\n\n/**\n * Create a tail sampler that keeps all errors\n *\n * Useful for debugging - captures all failures while dropping successful requests.\n *\n * @example\n * ```typescript\n * const tailSampler = createErrorOnlyTailSampler()\n * ```\n */\nexport function createErrorOnlyTailSampler(): TailSampleFn {\n return (traceInfo: LocalTrace): boolean => {\n // SpanStatusCode.ERROR = 2\n return traceInfo.localRootSpan.status.code === 2;\n };\n}\n\n/**\n * Create a tail sampler that keeps slow requests\n *\n * Useful for performance debugging - captures slow requests while dropping fast ones.\n *\n * @example\n * ```typescript\n * const tailSampler = createSlowOnlyTailSampler(1000) // Keep requests > 1s\n * ```\n */\nexport function createSlowOnlyTailSampler(thresholdMs: number): TailSampleFn {\n return (traceInfo: LocalTrace): boolean => {\n const duration = getDurationMs(traceInfo.localRootSpan);\n return duration >= thresholdMs;\n };\n}\n\n/**\n * Combine multiple tail samplers with OR logic\n *\n * Keeps a trace if ANY sampler returns true.\n *\n * @example\n * ```typescript\n * const tailSampler = combineTailSamplers(\n * createErrorOnlyTailSampler(),\n * createSlowOnlyTailSampler(1000),\n * createRandomTailSampler(0.01) // 1% baseline\n * )\n * ```\n */\nexport function combineTailSamplers(...samplers: TailSampleFn[]): TailSampleFn {\n if (samplers.length === 0) {\n throw new Error('combineTailSamplers requires at least one sampler');\n }\n\n return (traceInfo: LocalTrace): boolean => {\n return samplers.some((sampler) => sampler(traceInfo));\n };\n}\n\n/**\n * Create a tail sampler based on custom predicate\n *\n * @example\n * ```typescript\n * // Keep traces with specific attributes\n * const tailSampler = createCustomTailSampler((trace) => {\n * const attrs = trace.localRootSpan.attributes\n * return attrs['user.id'] === 'vip_123'\n * })\n * ```\n */\nexport function createCustomTailSampler(\n predicate: (traceInfo: LocalTrace) => boolean,\n): TailSampleFn {\n return predicate;\n}\n\n/**\n * Helper: Get span duration in milliseconds\n */\nfunction getDurationMs(span: ReadableSpan): number {\n const start = span.startTime[0] * 1000 + span.startTime[1] / 1_000_000;\n const end = span.endTime[0] * 1000 + span.endTime[1] / 1_000_000;\n return end - start;\n}\n\n/**\n * Common presets for quick setup\n */\nexport const SamplingPresets = {\n /**\n * Production: 10% baseline, all errors, all slow (>1s)\n * Recommended for most production workloads\n */\n production: (): TailSampleFn =>\n createAdaptiveTailSampler({\n baselineSampleRate: 0.1,\n slowThresholdMs: 1000,\n }),\n\n /**\n * High-traffic: 1% baseline, all errors, all slow (>2s)\n * For high-volume services where cost is a concern\n */\n highTraffic: (): TailSampleFn =>\n createAdaptiveTailSampler({\n baselineSampleRate: 0.01,\n slowThresholdMs: 2000,\n }),\n\n /**\n * Debugging: All errors, all slow (>500ms), 50% baseline\n * For active debugging sessions\n */\n debugging: (): TailSampleFn =>\n createAdaptiveTailSampler({\n baselineSampleRate: 0.5,\n slowThresholdMs: 500,\n }),\n\n /**\n * Development: 100% sampling\n * For local development and testing\n */\n development: (): TailSampleFn => () => true,\n\n /**\n * Errors only: Capture all failures, drop all successes\n * For error-focused monitoring\n */\n errorsOnly: (): TailSampleFn => createErrorOnlyTailSampler(),\n};\n"]}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"testing.js"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Span, TextMapPropagator, SpanOptions, Context, Attributes } from '@opentelemetry/api';
|
|
2
|
+
import { ReadableSpan, Sampler, SpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared types for autotel-edge
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Trigger types for edge handlers
|
|
10
|
+
* Can be a Request or any vendor-specific trigger type
|
|
11
|
+
*/
|
|
12
|
+
type Trigger = Request | DOConstructorTrigger | 'do-alarm' | unknown;
|
|
13
|
+
interface DOConstructorTrigger {
|
|
14
|
+
id: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Config types
|
|
19
|
+
*/
|
|
20
|
+
interface OTLPExporterConfig {
|
|
21
|
+
url: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
type ExporterConfig = OTLPExporterConfig | SpanExporter;
|
|
25
|
+
interface ServiceConfig {
|
|
26
|
+
name: string;
|
|
27
|
+
namespace?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
}
|
|
30
|
+
interface ParentRatioSamplingConfig {
|
|
31
|
+
acceptRemote?: boolean;
|
|
32
|
+
ratio: number;
|
|
33
|
+
}
|
|
34
|
+
type HeadSamplerConf = Sampler | ParentRatioSamplingConfig;
|
|
35
|
+
interface SamplingConfig<HS extends HeadSamplerConf = HeadSamplerConf> {
|
|
36
|
+
headSampler?: HS;
|
|
37
|
+
tailSampler?: TailSampleFn;
|
|
38
|
+
}
|
|
39
|
+
interface InstrumentationOptions {
|
|
40
|
+
instrumentGlobalFetch?: boolean;
|
|
41
|
+
instrumentGlobalCache?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Disable instrumentation entirely (useful for local development)
|
|
44
|
+
* When enabled, the handler is returned as-is without any instrumentation
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Utility types
|
|
51
|
+
*/
|
|
52
|
+
type OrPromise<T> = T | Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Adapter event types
|
|
55
|
+
*/
|
|
56
|
+
type FunnelStepStatus = 'started' | 'completed' | 'abandoned' | 'failed' | (string & {});
|
|
57
|
+
type OutcomeStatus = 'success' | 'failure' | 'partial' | (string & {});
|
|
58
|
+
interface EdgeEventBase {
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
service: string;
|
|
61
|
+
timestamp: number;
|
|
62
|
+
attributes: Record<string, unknown>;
|
|
63
|
+
traceId?: string;
|
|
64
|
+
spanId?: string;
|
|
65
|
+
correlationId?: string;
|
|
66
|
+
name: string;
|
|
67
|
+
}
|
|
68
|
+
interface EdgeTrackEvent extends EdgeEventBase {
|
|
69
|
+
type: 'event';
|
|
70
|
+
event: string;
|
|
71
|
+
}
|
|
72
|
+
interface EdgeFunnelStepEvent extends EdgeEventBase {
|
|
73
|
+
type: 'funnel-step';
|
|
74
|
+
funnel: string;
|
|
75
|
+
status: FunnelStepStatus;
|
|
76
|
+
}
|
|
77
|
+
interface EdgeOutcomeEvent extends EdgeEventBase {
|
|
78
|
+
type: 'outcome';
|
|
79
|
+
operation: string;
|
|
80
|
+
outcome: OutcomeStatus;
|
|
81
|
+
}
|
|
82
|
+
interface EdgeValueEvent extends EdgeEventBase {
|
|
83
|
+
type: 'value';
|
|
84
|
+
metric: string;
|
|
85
|
+
value: number;
|
|
86
|
+
}
|
|
87
|
+
type EdgeEvent = EdgeTrackEvent | EdgeFunnelStepEvent | EdgeOutcomeEvent | EdgeValueEvent;
|
|
88
|
+
type EdgeSubscriber = (event: EdgeEvent) => OrPromise<void>;
|
|
89
|
+
interface FetcherConfig {
|
|
90
|
+
includeTraceContext?: boolean | ((request: Request) => boolean);
|
|
91
|
+
}
|
|
92
|
+
interface PostProcessParams {
|
|
93
|
+
/**
|
|
94
|
+
* The request object that was passed to the fetch handler.
|
|
95
|
+
*/
|
|
96
|
+
request: Request;
|
|
97
|
+
/**
|
|
98
|
+
* The generated response object.
|
|
99
|
+
*/
|
|
100
|
+
response: Response;
|
|
101
|
+
/**
|
|
102
|
+
* A readable version of the span object that can be used to access the span's attributes and events.
|
|
103
|
+
*/
|
|
104
|
+
readable: ReadableSpan;
|
|
105
|
+
}
|
|
106
|
+
interface FetchHandlerConfig {
|
|
107
|
+
/**
|
|
108
|
+
* Whether to enable context propagation for incoming requests to `fetch`.
|
|
109
|
+
* This enables or disables distributed tracing from W3C Trace Context headers.
|
|
110
|
+
* @default true
|
|
111
|
+
*/
|
|
112
|
+
acceptTraceContext?: boolean | ((request: Request) => boolean);
|
|
113
|
+
/**
|
|
114
|
+
* Allows further customization of the generated span, based on the request/response data.
|
|
115
|
+
*/
|
|
116
|
+
postProcess?: (span: Span, ctx: PostProcessParams) => void;
|
|
117
|
+
}
|
|
118
|
+
interface HandlerConfig {
|
|
119
|
+
fetch?: FetchHandlerConfig;
|
|
120
|
+
}
|
|
121
|
+
interface EdgeConfigBase {
|
|
122
|
+
service: ServiceConfig;
|
|
123
|
+
handlers?: HandlerConfig;
|
|
124
|
+
fetch?: FetcherConfig;
|
|
125
|
+
postProcessor?: PostProcessorFn;
|
|
126
|
+
sampling?: SamplingConfig;
|
|
127
|
+
propagator?: TextMapPropagator;
|
|
128
|
+
instrumentation?: InstrumentationOptions;
|
|
129
|
+
subscribers?: EdgeSubscriber[];
|
|
130
|
+
}
|
|
131
|
+
interface EdgeConfigExporter extends EdgeConfigBase {
|
|
132
|
+
exporter: ExporterConfig;
|
|
133
|
+
}
|
|
134
|
+
interface EdgeConfigSpanProcessors extends EdgeConfigBase {
|
|
135
|
+
spanProcessors: SpanProcessor | SpanProcessor[];
|
|
136
|
+
}
|
|
137
|
+
type EdgeConfig = EdgeConfigExporter | EdgeConfigSpanProcessors;
|
|
138
|
+
interface ResolvedEdgeConfig extends EdgeConfigBase {
|
|
139
|
+
handlers: Required<HandlerConfig>;
|
|
140
|
+
fetch: Required<FetcherConfig>;
|
|
141
|
+
postProcessor: PostProcessorFn;
|
|
142
|
+
sampling: Required<SamplingConfig<Sampler>>;
|
|
143
|
+
spanProcessors: SpanProcessor[];
|
|
144
|
+
propagator: TextMapPropagator;
|
|
145
|
+
instrumentation: InstrumentationOptions;
|
|
146
|
+
subscribers: EdgeSubscriber[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Function types
|
|
150
|
+
*/
|
|
151
|
+
type ResolveConfigFn<Env = any> = (env: Env, trigger: Trigger) => EdgeConfig;
|
|
152
|
+
type ConfigurationOption = EdgeConfig | ResolveConfigFn;
|
|
153
|
+
type PostProcessorFn = (spans: ReadableSpan[]) => ReadableSpan[];
|
|
154
|
+
type TailSampleFn = (traceInfo: LocalTrace) => boolean;
|
|
155
|
+
interface LocalTrace {
|
|
156
|
+
traceId: string;
|
|
157
|
+
spans: ReadableSpan[];
|
|
158
|
+
localRootSpan: ReadableSpan;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Span processor with flush support
|
|
162
|
+
*/
|
|
163
|
+
type TraceFlushableSpanProcessor = SpanProcessor & {
|
|
164
|
+
forceFlush: (traceId?: string) => Promise<void>;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Handler instrumentation
|
|
168
|
+
*/
|
|
169
|
+
interface InitialSpanInfo {
|
|
170
|
+
name: string;
|
|
171
|
+
options: SpanOptions;
|
|
172
|
+
context?: Context;
|
|
173
|
+
}
|
|
174
|
+
interface HandlerInstrumentation<T extends Trigger, R extends any> {
|
|
175
|
+
getInitialSpanInfo: (trigger: T) => InitialSpanInfo;
|
|
176
|
+
getAttributesFromResult?: (result: Awaited<R>) => Attributes;
|
|
177
|
+
instrumentTrigger?: (trigger: T) => T;
|
|
178
|
+
executionSucces?: (span: Span, trigger: T, result: Awaited<R>) => void;
|
|
179
|
+
executionFailed?: (span: Span, trigger: T, error?: any) => void;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type { ConfigurationOption as C, EdgeEvent as E, FunnelStepStatus as F, HandlerInstrumentation as H, InstrumentationOptions as I, LocalTrace as L, OrPromise as O, PostProcessorFn as P, ResolvedEdgeConfig as R, ServiceConfig as S, TailSampleFn as T, OutcomeStatus as a, EdgeTrackEvent as b, EdgeFunnelStepEvent as c, EdgeOutcomeEvent as d, EdgeValueEvent as e, OTLPExporterConfig as f, EdgeConfig as g, Trigger as h, ExporterConfig as i, SamplingConfig as j, ResolveConfigFn as k, TraceFlushableSpanProcessor as l, InitialSpanInfo as m, EdgeSubscriber as n };
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autotel-edge",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Vendor-agnostic OpenTelemetry for edge runtimes - foundation for Cloudflare, Vercel, Netlify, Deno",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./sampling": {
|
|
15
|
+
"types": "./dist/sampling.d.ts",
|
|
16
|
+
"import": "./dist/sampling.js"
|
|
17
|
+
},
|
|
18
|
+
"./events": {
|
|
19
|
+
"types": "./dist/events.d.ts",
|
|
20
|
+
"import": "./dist/events.js"
|
|
21
|
+
},
|
|
22
|
+
"./logger": {
|
|
23
|
+
"types": "./dist/logger.d.ts",
|
|
24
|
+
"import": "./dist/logger.js"
|
|
25
|
+
},
|
|
26
|
+
"./testing": {
|
|
27
|
+
"types": "./dist/testing.d.ts",
|
|
28
|
+
"import": "./dist/testing.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"opentelemetry",
|
|
38
|
+
"otel",
|
|
39
|
+
"edge",
|
|
40
|
+
"serverless",
|
|
41
|
+
"observability",
|
|
42
|
+
"tracing",
|
|
43
|
+
"vendor-agnostic",
|
|
44
|
+
"cloudflare",
|
|
45
|
+
"vercel",
|
|
46
|
+
"netlify",
|
|
47
|
+
"deno"
|
|
48
|
+
],
|
|
49
|
+
"author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@opentelemetry/api": "^1.9.0",
|
|
53
|
+
"@opentelemetry/core": "^2.2.0",
|
|
54
|
+
"@opentelemetry/otlp-exporter-base": "^0.208.0",
|
|
55
|
+
"@opentelemetry/otlp-transformer": "^0.208.0",
|
|
56
|
+
"@opentelemetry/resources": "^2.2.0",
|
|
57
|
+
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
58
|
+
"@opentelemetry/semantic-conventions": "^1.38.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@opentelemetry/context-async-hooks": "^2.2.0",
|
|
62
|
+
"@types/node": "^24.10.1",
|
|
63
|
+
"rimraf": "^6.1.2",
|
|
64
|
+
"tsup": "^8.5.1",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"vitest": "^4.0.13",
|
|
67
|
+
"vitest-mock-extended": "^3.1.0"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/jagreehal/autotel",
|
|
72
|
+
"directory": "packages/autotel-edge"
|
|
73
|
+
},
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/jagreehal/autotel/issues"
|
|
76
|
+
},
|
|
77
|
+
"homepage": "https://github.com/jagreehal/autotel/tree/main/packages/autotel-edge#readme",
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsup",
|
|
80
|
+
"dev": "tsup --watch",
|
|
81
|
+
"lint": "npx eslint src/**/*.ts",
|
|
82
|
+
"lint:fix": "npx eslint src/**/*.ts --fix",
|
|
83
|
+
"type-check": "tsc --noEmit",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:watch": "vitest",
|
|
86
|
+
"clean": "rimraf dist"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { createEdgeLogger, runWithLogLevel, getActiveLogLevel } from './logger';
|
|
3
|
+
import { trace } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
describe('Edge Logger', () => {
|
|
6
|
+
let consoleLogSpy: any;
|
|
7
|
+
let consoleErrorSpy: any;
|
|
8
|
+
let consoleWarnSpy: any;
|
|
9
|
+
let consoleInfoSpy: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
13
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
14
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
15
|
+
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
consoleLogSpy.mockRestore();
|
|
20
|
+
consoleErrorSpy.mockRestore();
|
|
21
|
+
consoleWarnSpy.mockRestore();
|
|
22
|
+
consoleInfoSpy.mockRestore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createEdgeLogger', () => {
|
|
26
|
+
it('should create a logger with service name', () => {
|
|
27
|
+
const logger = createEdgeLogger('test-service');
|
|
28
|
+
expect(logger).toBeDefined();
|
|
29
|
+
expect(logger.info).toBeDefined();
|
|
30
|
+
expect(logger.error).toBeDefined();
|
|
31
|
+
expect(logger.warn).toBeDefined();
|
|
32
|
+
expect(logger.debug).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should log info messages with service name', () => {
|
|
36
|
+
const logger = createEdgeLogger('test-service');
|
|
37
|
+
logger.info('test message', { key: 'value' });
|
|
38
|
+
|
|
39
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
40
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
41
|
+
|
|
42
|
+
expect(logOutput).toMatchObject({
|
|
43
|
+
level: 'info',
|
|
44
|
+
service: 'test-service',
|
|
45
|
+
msg: 'test message',
|
|
46
|
+
key: 'value',
|
|
47
|
+
});
|
|
48
|
+
expect(logOutput.timestamp).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should log error messages', () => {
|
|
52
|
+
const logger = createEdgeLogger('test-service');
|
|
53
|
+
const error = new Error('test error');
|
|
54
|
+
logger.error('error occurred', error);
|
|
55
|
+
|
|
56
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
57
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
58
|
+
|
|
59
|
+
expect(logOutput).toMatchObject({
|
|
60
|
+
level: 'error',
|
|
61
|
+
service: 'test-service',
|
|
62
|
+
msg: 'error occurred',
|
|
63
|
+
error: 'test error',
|
|
64
|
+
});
|
|
65
|
+
expect(logOutput.stack).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should log warning messages', () => {
|
|
69
|
+
const logger = createEdgeLogger('test-service');
|
|
70
|
+
logger.warn('warning message', { reason: 'test' });
|
|
71
|
+
|
|
72
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
73
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
74
|
+
|
|
75
|
+
expect(logOutput).toMatchObject({
|
|
76
|
+
level: 'warn',
|
|
77
|
+
service: 'test-service',
|
|
78
|
+
msg: 'warning message',
|
|
79
|
+
reason: 'test',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should log debug messages when level is set to debug', () => {
|
|
84
|
+
const logger = createEdgeLogger('test-service', { level: 'debug' });
|
|
85
|
+
logger.debug('debug message', { detail: 'verbose' });
|
|
86
|
+
|
|
87
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
88
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
89
|
+
|
|
90
|
+
expect(logOutput).toMatchObject({
|
|
91
|
+
level: 'debug',
|
|
92
|
+
service: 'test-service',
|
|
93
|
+
msg: 'debug message',
|
|
94
|
+
detail: 'verbose',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should not log debug messages when level is info (default)', () => {
|
|
99
|
+
const logger = createEdgeLogger('test-service');
|
|
100
|
+
logger.debug('debug message', { detail: 'verbose' });
|
|
101
|
+
|
|
102
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should include trace context when available', () => {
|
|
106
|
+
const logger = createEdgeLogger('test-service');
|
|
107
|
+
|
|
108
|
+
// Create a mock tracer and span
|
|
109
|
+
const mockSpan = {
|
|
110
|
+
spanContext: () => ({
|
|
111
|
+
traceId: 'test-trace-id-16chars',
|
|
112
|
+
spanId: 'test-span-id',
|
|
113
|
+
traceFlags: 1,
|
|
114
|
+
}),
|
|
115
|
+
setAttribute: vi.fn(),
|
|
116
|
+
setStatus: vi.fn(),
|
|
117
|
+
recordException: vi.fn(),
|
|
118
|
+
end: vi.fn(),
|
|
119
|
+
isRecording: () => true,
|
|
120
|
+
updateName: vi.fn(),
|
|
121
|
+
addEvent: vi.fn(),
|
|
122
|
+
setAttributes: vi.fn(),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Mock trace.getActiveSpan to return our span
|
|
126
|
+
const getActiveSpanSpy = vi.spyOn(trace, 'getActiveSpan').mockReturnValue(mockSpan as any);
|
|
127
|
+
|
|
128
|
+
logger.info('message with trace');
|
|
129
|
+
|
|
130
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
131
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
132
|
+
|
|
133
|
+
expect(logOutput).toMatchObject({
|
|
134
|
+
level: 'info',
|
|
135
|
+
service: 'test-service',
|
|
136
|
+
msg: 'message with trace',
|
|
137
|
+
traceId: 'test-trace-id-16chars',
|
|
138
|
+
spanId: 'test-span-id',
|
|
139
|
+
correlationId: 'test-trace-id-16',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
getActiveSpanSpy.mockRestore();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle non-Error objects', () => {
|
|
146
|
+
const logger = createEdgeLogger('test-service');
|
|
147
|
+
const errorObject = { message: 'simple error' };
|
|
148
|
+
logger.error('error occurred', errorObject);
|
|
149
|
+
|
|
150
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
151
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
152
|
+
|
|
153
|
+
expect(logOutput).toMatchObject({
|
|
154
|
+
level: 'error',
|
|
155
|
+
service: 'test-service',
|
|
156
|
+
msg: 'error occurred',
|
|
157
|
+
error: '[object Object]',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle null and undefined context gracefully', () => {
|
|
162
|
+
const logger = createEdgeLogger('test-service');
|
|
163
|
+
logger.info('message with null', null as any);
|
|
164
|
+
logger.info('message with undefined', undefined as any);
|
|
165
|
+
|
|
166
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
167
|
+
|
|
168
|
+
const log1 = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
169
|
+
const log2 = JSON.parse(consoleLogSpy.mock.calls[1][0]);
|
|
170
|
+
|
|
171
|
+
expect(log1.msg).toBe('message with null');
|
|
172
|
+
expect(log2.msg).toBe('message with undefined');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should accept a single context object', () => {
|
|
176
|
+
const logger = createEdgeLogger('test-service');
|
|
177
|
+
logger.info('message', { a: 1, b: 2, c: 3 });
|
|
178
|
+
|
|
179
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
180
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
181
|
+
|
|
182
|
+
expect(logOutput).toMatchObject({
|
|
183
|
+
level: 'info',
|
|
184
|
+
msg: 'message',
|
|
185
|
+
a: 1,
|
|
186
|
+
b: 2,
|
|
187
|
+
c: 3,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should preserve timestamp format', () => {
|
|
192
|
+
const logger = createEdgeLogger('test-service');
|
|
193
|
+
logger.info('test');
|
|
194
|
+
|
|
195
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
196
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
197
|
+
|
|
198
|
+
expect(logOutput.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('Pretty mode', () => {
|
|
203
|
+
it('should format logs in pretty mode', () => {
|
|
204
|
+
const logger = createEdgeLogger('test-service', { pretty: true });
|
|
205
|
+
logger.info('pretty message', { key: 'value' });
|
|
206
|
+
|
|
207
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
208
|
+
const logOutput = consoleLogSpy.mock.calls[0][0];
|
|
209
|
+
|
|
210
|
+
// In pretty mode, output is a formatted string, not JSON
|
|
211
|
+
expect(typeof logOutput).toBe('string');
|
|
212
|
+
expect(logOutput).toContain('INFO');
|
|
213
|
+
expect(logOutput).toContain('test-service');
|
|
214
|
+
expect(logOutput).toContain('pretty message');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should format errors in pretty mode', () => {
|
|
218
|
+
const logger = createEdgeLogger('test-service', { pretty: true });
|
|
219
|
+
const error = new Error('test error');
|
|
220
|
+
logger.error('error occurred', error);
|
|
221
|
+
|
|
222
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
223
|
+
const logOutput = consoleLogSpy.mock.calls[0][0];
|
|
224
|
+
|
|
225
|
+
expect(typeof logOutput).toBe('string');
|
|
226
|
+
expect(logOutput).toContain('ERROR');
|
|
227
|
+
expect(logOutput).toContain('test-service');
|
|
228
|
+
expect(logOutput).toContain('error occurred');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('Edge cases', () => {
|
|
233
|
+
it('should throw on circular references in context', () => {
|
|
234
|
+
const logger = createEdgeLogger('test-service');
|
|
235
|
+
const circular: any = { a: 1 };
|
|
236
|
+
circular.self = circular;
|
|
237
|
+
|
|
238
|
+
// JSON.stringify will throw on circular references
|
|
239
|
+
expect(() => {
|
|
240
|
+
logger.info('circular', circular);
|
|
241
|
+
}).toThrow(/circular/i);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should handle very long messages', () => {
|
|
245
|
+
const logger = createEdgeLogger('test-service');
|
|
246
|
+
const longMessage = 'a'.repeat(10_000);
|
|
247
|
+
|
|
248
|
+
logger.info(longMessage);
|
|
249
|
+
|
|
250
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
251
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
252
|
+
expect(logOutput.msg).toBe(longMessage);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle special characters in messages', () => {
|
|
256
|
+
const logger = createEdgeLogger('test-service');
|
|
257
|
+
const specialMessage = 'Hello\nWorld\t"quoted"\r\nNew Line';
|
|
258
|
+
|
|
259
|
+
logger.info(specialMessage);
|
|
260
|
+
|
|
261
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
262
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
263
|
+
expect(logOutput.msg).toBe(specialMessage);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('Dynamic log level control', () => {
|
|
268
|
+
it('should override log level via runWithLogLevel', () => {
|
|
269
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
270
|
+
|
|
271
|
+
// Normal behavior: debug filtered out
|
|
272
|
+
logger.debug('outside context');
|
|
273
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
274
|
+
|
|
275
|
+
// Inside debug context: debug logged
|
|
276
|
+
runWithLogLevel('debug', () => {
|
|
277
|
+
logger.debug('inside debug context');
|
|
278
|
+
});
|
|
279
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
280
|
+
|
|
281
|
+
// Back to normal: debug filtered out again
|
|
282
|
+
logger.debug('outside context again');
|
|
283
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should support none level to temporarily disable logging', () => {
|
|
287
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
288
|
+
|
|
289
|
+
logger.info('before none');
|
|
290
|
+
const callsBeforeNone = consoleLogSpy.mock.calls.length;
|
|
291
|
+
|
|
292
|
+
runWithLogLevel('none', () => {
|
|
293
|
+
logger.info('inside none context');
|
|
294
|
+
logger.error('even errors suppressed');
|
|
295
|
+
});
|
|
296
|
+
expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeNone); // No new logs
|
|
297
|
+
|
|
298
|
+
logger.info('after none');
|
|
299
|
+
expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeNone + 1);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return value from callback', () => {
|
|
303
|
+
const result = runWithLogLevel('debug', () => {
|
|
304
|
+
return 42;
|
|
305
|
+
});
|
|
306
|
+
expect(result).toBe(42);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should propagate async results', async () => {
|
|
310
|
+
const result = await runWithLogLevel('debug', async () => {
|
|
311
|
+
return 'async value';
|
|
312
|
+
});
|
|
313
|
+
expect(result).toBe('async value');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should allow raising log level temporarily', () => {
|
|
317
|
+
const logger = createEdgeLogger('test-service', { level: 'error' });
|
|
318
|
+
|
|
319
|
+
logger.info('normal info');
|
|
320
|
+
logger.warn('normal warn');
|
|
321
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
322
|
+
|
|
323
|
+
runWithLogLevel('info', () => {
|
|
324
|
+
logger.info('temporary info');
|
|
325
|
+
logger.warn('temporary warn');
|
|
326
|
+
});
|
|
327
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should isolate log level changes to context', () => {
|
|
331
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
332
|
+
|
|
333
|
+
runWithLogLevel('debug', () => {
|
|
334
|
+
logger.debug('context 1 - debug');
|
|
335
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
runWithLogLevel('error', () => {
|
|
339
|
+
logger.info('context 2 - info should be filtered');
|
|
340
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce(); // No new logs
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('getActiveLogLevel', () => {
|
|
346
|
+
it('should return undefined when no active level', () => {
|
|
347
|
+
expect(getActiveLogLevel()).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should return active level inside runWithLogLevel', () => {
|
|
351
|
+
runWithLogLevel('debug', () => {
|
|
352
|
+
expect(getActiveLogLevel()).toBe('debug');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
runWithLogLevel('error', () => {
|
|
356
|
+
expect(getActiveLogLevel()).toBe('error');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should reset after runWithLogLevel completes', () => {
|
|
361
|
+
runWithLogLevel('debug', () => {
|
|
362
|
+
expect(getActiveLogLevel()).toBe('debug');
|
|
363
|
+
});
|
|
364
|
+
expect(getActiveLogLevel()).toBeUndefined();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|