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,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span processor with flush and tail sampling support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Context } from '@opentelemetry/api';
|
|
6
|
+
import type {
|
|
7
|
+
ReadableSpan,
|
|
8
|
+
Span,
|
|
9
|
+
SpanExporter,
|
|
10
|
+
SpanProcessor,
|
|
11
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
12
|
+
import type { PostProcessorFn, TailSampleFn, LocalTrace } from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Span processor that supports flush by trace ID and tail sampling
|
|
16
|
+
*/
|
|
17
|
+
export class SpanProcessorWithFlush implements SpanProcessor {
|
|
18
|
+
private exporter: SpanExporter;
|
|
19
|
+
private postProcessor?: PostProcessorFn;
|
|
20
|
+
private spans: Map<string, ReadableSpan[]> = new Map();
|
|
21
|
+
|
|
22
|
+
constructor(exporter: SpanExporter, postProcessor?: PostProcessorFn) {
|
|
23
|
+
this.exporter = exporter;
|
|
24
|
+
this.postProcessor = postProcessor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onStart(_span: Span, _parentContext: Context): void {
|
|
28
|
+
// No-op for now
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onEnd(span: ReadableSpan): void {
|
|
32
|
+
const traceId = span.spanContext().traceId;
|
|
33
|
+
|
|
34
|
+
if (!this.spans.has(traceId)) {
|
|
35
|
+
this.spans.set(traceId, []);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.spans.get(traceId)!.push(span);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Force flush spans for a specific trace
|
|
43
|
+
*/
|
|
44
|
+
async forceFlush(traceId?: string): Promise<void> {
|
|
45
|
+
if (traceId) {
|
|
46
|
+
const spans = this.spans.get(traceId);
|
|
47
|
+
if (spans && spans.length > 0) {
|
|
48
|
+
await this.exportSpans(spans);
|
|
49
|
+
this.spans.delete(traceId);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Flush all traces
|
|
53
|
+
const promises: Promise<void>[] = [];
|
|
54
|
+
for (const [id, spans] of this.spans.entries()) {
|
|
55
|
+
promises.push(this.exportSpans(spans));
|
|
56
|
+
this.spans.delete(id);
|
|
57
|
+
}
|
|
58
|
+
await Promise.all(promises);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async shutdown(): Promise<void> {
|
|
63
|
+
await this.forceFlush();
|
|
64
|
+
if (this.exporter) {
|
|
65
|
+
await this.exporter.shutdown();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Export spans with post-processing
|
|
71
|
+
* Errors are caught and logged but don't throw to prevent worker instability
|
|
72
|
+
*/
|
|
73
|
+
private async exportSpans(spans: ReadableSpan[]): Promise<void> {
|
|
74
|
+
if (spans.length === 0) return;
|
|
75
|
+
if (!this.exporter) return; // No exporter configured (e.g., in tests)
|
|
76
|
+
|
|
77
|
+
let processedSpans = spans;
|
|
78
|
+
|
|
79
|
+
if (this.postProcessor) {
|
|
80
|
+
try {
|
|
81
|
+
processedSpans = this.postProcessor(spans);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Post-processor errors should not prevent export
|
|
84
|
+
console.error('[autotel-edge] Post-processor error:', error);
|
|
85
|
+
// Continue with original spans
|
|
86
|
+
processedSpans = spans;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
this.exporter.export(processedSpans, (result) => {
|
|
92
|
+
if (result.code === 0) {
|
|
93
|
+
// SUCCESS
|
|
94
|
+
resolve();
|
|
95
|
+
} else {
|
|
96
|
+
// Log but don't reject - exporter failures shouldn't crash the worker
|
|
97
|
+
console.error(
|
|
98
|
+
'[autotel-edge] Exporter error:',
|
|
99
|
+
result.error?.message || 'Unknown error',
|
|
100
|
+
);
|
|
101
|
+
resolve(); // Resolve instead of reject to prevent unhandled promise rejection
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Span processor that supports tail sampling decisions
|
|
110
|
+
*/
|
|
111
|
+
export class TailSamplingSpanProcessor implements SpanProcessor {
|
|
112
|
+
private wrapped: SpanProcessorWithFlush;
|
|
113
|
+
private tailSampler?: TailSampleFn;
|
|
114
|
+
private traces: Map<string, LocalTrace> = new Map();
|
|
115
|
+
|
|
116
|
+
constructor(
|
|
117
|
+
exporter: SpanExporter,
|
|
118
|
+
postProcessor?: PostProcessorFn,
|
|
119
|
+
tailSampler?: TailSampleFn,
|
|
120
|
+
) {
|
|
121
|
+
this.wrapped = new SpanProcessorWithFlush(exporter, postProcessor);
|
|
122
|
+
this.tailSampler = tailSampler;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onStart(span: Span, parentContext: Context): void {
|
|
126
|
+
this.wrapped.onStart(span, parentContext);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onEnd(span: ReadableSpan): void {
|
|
130
|
+
const traceId = span.spanContext().traceId;
|
|
131
|
+
const spanId = span.spanContext().spanId;
|
|
132
|
+
const parentSpanId = 'parentSpanId' in span ? span.parentSpanId : undefined;
|
|
133
|
+
|
|
134
|
+
// Initialize trace if not exists
|
|
135
|
+
if (!this.traces.has(traceId)) {
|
|
136
|
+
this.traces.set(traceId, {
|
|
137
|
+
traceId,
|
|
138
|
+
spans: [],
|
|
139
|
+
localRootSpan: undefined as any, // Will be set when we identify the local root
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const trace = this.traces.get(traceId)!;
|
|
144
|
+
|
|
145
|
+
// Determine if this span is a local root by checking if its parent is in buffered spans
|
|
146
|
+
// A span is a local root if:
|
|
147
|
+
// 1. It has no parentSpanId (definitive root)
|
|
148
|
+
// 2. Its parentSpanId doesn't match any already-buffered span (remote parent = distributed trace entry)
|
|
149
|
+
const hasLocalParent = parentSpanId &&
|
|
150
|
+
trace.spans.some(s => s.spanContext().spanId === parentSpanId);
|
|
151
|
+
|
|
152
|
+
// Set localRootSpan if this is the local root (no local parent found in buffer)
|
|
153
|
+
if (!hasLocalParent) {
|
|
154
|
+
trace.localRootSpan = span;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
trace.spans.push(span); // Buffer the span AFTER checking parent relationships
|
|
158
|
+
|
|
159
|
+
// Auto-flush decision: only auto-flush for normal traces (no parentSpanId at all)
|
|
160
|
+
// For distributed traces (parentSpanId present), we rely on explicit forceFlush() from instrument.ts
|
|
161
|
+
// This ensures we don't trigger before all spans have been buffered
|
|
162
|
+
const isDefinitiveRoot = !parentSpanId;
|
|
163
|
+
const shouldAutoFlush = isDefinitiveRoot && trace.localRootSpan &&
|
|
164
|
+
trace.localRootSpan.spanContext().spanId === spanId;
|
|
165
|
+
|
|
166
|
+
if (shouldAutoFlush) {
|
|
167
|
+
if (this.tailSampler) {
|
|
168
|
+
const shouldKeep = this.tailSampler(trace);
|
|
169
|
+
|
|
170
|
+
if (shouldKeep) {
|
|
171
|
+
// Export ALL buffered spans in the trace
|
|
172
|
+
for (const bufferedSpan of trace.spans) {
|
|
173
|
+
this.wrapped.onEnd(bufferedSpan);
|
|
174
|
+
}
|
|
175
|
+
// Force flush to actually export the spans
|
|
176
|
+
void this.wrapped.forceFlush(traceId);
|
|
177
|
+
}
|
|
178
|
+
// If not keeping, just drop all spans (don't export)
|
|
179
|
+
} else {
|
|
180
|
+
// No tail sampler, export all buffered spans
|
|
181
|
+
for (const bufferedSpan of trace.spans) {
|
|
182
|
+
this.wrapped.onEnd(bufferedSpan);
|
|
183
|
+
}
|
|
184
|
+
// Force flush to actually export the spans
|
|
185
|
+
void this.wrapped.forceFlush(traceId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Clean up trace after decision
|
|
189
|
+
this.traces.delete(traceId);
|
|
190
|
+
}
|
|
191
|
+
// If not local root span, just buffer it - don't export yet
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async forceFlush(traceId?: string): Promise<void> {
|
|
195
|
+
if (traceId) {
|
|
196
|
+
// Make tail sampling decision for this specific trace before flushing
|
|
197
|
+
const trace = this.traces.get(traceId);
|
|
198
|
+
if (trace) {
|
|
199
|
+
// Ensure localRootSpan is set (fallback to first span if not)
|
|
200
|
+
// This handles distributed traces where no span has undefined parentSpanId
|
|
201
|
+
if (!trace.localRootSpan && trace.spans.length > 0) {
|
|
202
|
+
trace.localRootSpan = trace.spans[0];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.tailSampler) {
|
|
206
|
+
const shouldKeep = this.tailSampler(trace);
|
|
207
|
+
|
|
208
|
+
if (shouldKeep) {
|
|
209
|
+
// Export ALL buffered spans in the trace
|
|
210
|
+
for (const bufferedSpan of trace.spans) {
|
|
211
|
+
this.wrapped.onEnd(bufferedSpan);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// No tail sampler, export all buffered spans
|
|
216
|
+
for (const bufferedSpan of trace.spans) {
|
|
217
|
+
this.wrapped.onEnd(bufferedSpan);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clean up trace after decision
|
|
222
|
+
this.traces.delete(traceId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return this.wrapped.forceFlush(traceId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async shutdown(): Promise<void> {
|
|
229
|
+
this.traces.clear();
|
|
230
|
+
return this.wrapped.shutdown();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace context types and utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Span, SpanStatusCode } from '@opentelemetry/api';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WeakMap to store span names for active spans.
|
|
9
|
+
* Enables retrieving span names for correlation helpers.
|
|
10
|
+
*/
|
|
11
|
+
const spanNameMap = new WeakMap<Span, string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base trace context containing trace identifiers
|
|
15
|
+
*/
|
|
16
|
+
export interface TraceContextBase {
|
|
17
|
+
traceId: string;
|
|
18
|
+
spanId: string;
|
|
19
|
+
correlationId: string;
|
|
20
|
+
'code.function'?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Span methods available on trace context
|
|
25
|
+
*/
|
|
26
|
+
export interface SpanMethods {
|
|
27
|
+
setAttribute(key: string, value: string | number | boolean): void;
|
|
28
|
+
setAttributes(attrs: Record<string, string | number | boolean>): void;
|
|
29
|
+
setStatus(status: { code: SpanStatusCode; message?: string }): void;
|
|
30
|
+
recordException(exception: Error): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Complete trace context that merges base context and span methods
|
|
35
|
+
*
|
|
36
|
+
* This is the ctx parameter passed to factory functions in trace().
|
|
37
|
+
* It provides access to trace IDs and span manipulation methods.
|
|
38
|
+
*/
|
|
39
|
+
export type TraceContext = TraceContextBase & SpanMethods;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a TraceContext from an OpenTelemetry Span
|
|
43
|
+
*
|
|
44
|
+
* This utility extracts trace context information from a span
|
|
45
|
+
* and provides span manipulation methods in a consistent format.
|
|
46
|
+
*/
|
|
47
|
+
export function createTraceContext(span: Span): TraceContext {
|
|
48
|
+
const spanContext = span.spanContext();
|
|
49
|
+
return {
|
|
50
|
+
traceId: spanContext.traceId,
|
|
51
|
+
spanId: spanContext.spanId,
|
|
52
|
+
correlationId: spanContext.traceId.slice(0, 16),
|
|
53
|
+
'code.function': spanNameMap.get(span),
|
|
54
|
+
setAttribute: span.setAttribute.bind(span),
|
|
55
|
+
setAttributes: span.setAttributes.bind(span),
|
|
56
|
+
setStatus: span.setStatus.bind(span),
|
|
57
|
+
recordException: span.recordException.bind(span),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Store the span name for later retrieval via trace context helpers.
|
|
63
|
+
*/
|
|
64
|
+
export function setSpanName(span: Span, name: string): void {
|
|
65
|
+
spanNameMap.set(span, name);
|
|
66
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { WorkerTracer } from './tracer';
|
|
3
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
4
|
+
import { SamplingDecision, type SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
|
|
6
|
+
describe('WorkerTracer', () => {
|
|
7
|
+
let tracer: WorkerTracer;
|
|
8
|
+
let mockProcessor: SpanProcessor;
|
|
9
|
+
let mockHeadSampler: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockProcessor = {
|
|
13
|
+
onStart: vi.fn(),
|
|
14
|
+
onEnd: vi.fn(),
|
|
15
|
+
shutdown: vi.fn(async () => {}),
|
|
16
|
+
forceFlush: vi.fn(async () => {}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
mockHeadSampler = {
|
|
20
|
+
shouldSample: vi.fn(() => ({
|
|
21
|
+
decision: SamplingDecision.RECORD_AND_SAMPLED,
|
|
22
|
+
attributes: {},
|
|
23
|
+
traceState: undefined,
|
|
24
|
+
})),
|
|
25
|
+
toString: () => 'MockHeadSampler',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const resource = resourceFromAttributes({});
|
|
29
|
+
tracer = new WorkerTracer([mockProcessor], resource);
|
|
30
|
+
tracer.setHeadSampler(mockHeadSampler);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('Per-span sampler', () => {
|
|
34
|
+
it('should use per-span sampler when provided in options', () => {
|
|
35
|
+
const perSpanSampler = {
|
|
36
|
+
shouldSample: vi.fn(() => ({
|
|
37
|
+
decision: SamplingDecision.NOT_RECORD,
|
|
38
|
+
attributes: {},
|
|
39
|
+
traceState: undefined,
|
|
40
|
+
})),
|
|
41
|
+
toString: () => 'PerSpanSampler',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const options: any = { sampler: perSpanSampler };
|
|
45
|
+
tracer.startSpan('test.span', options);
|
|
46
|
+
|
|
47
|
+
// Per-span sampler should be called, NOT head sampler
|
|
48
|
+
expect(perSpanSampler.shouldSample).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(mockHeadSampler.shouldSample).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should use head sampler when no per-span sampler provided', () => {
|
|
53
|
+
tracer.startSpan('test.span', {});
|
|
54
|
+
|
|
55
|
+
// Head sampler should be called
|
|
56
|
+
expect(mockHeadSampler.shouldSample).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should pass correct arguments to per-span sampler', () => {
|
|
60
|
+
const perSpanSampler = {
|
|
61
|
+
shouldSample: vi.fn(() => ({
|
|
62
|
+
decision: SamplingDecision.RECORD_AND_SAMPLED,
|
|
63
|
+
attributes: {},
|
|
64
|
+
traceState: undefined,
|
|
65
|
+
})),
|
|
66
|
+
toString: () => 'PerSpanSampler',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const options: any = {
|
|
70
|
+
sampler: perSpanSampler,
|
|
71
|
+
attributes: { 'test.attr': 'value' },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
tracer.startSpan('test.span', options);
|
|
75
|
+
|
|
76
|
+
// Verify sampler was called with correct arguments
|
|
77
|
+
expect(perSpanSampler.shouldSample).toHaveBeenCalledWith(
|
|
78
|
+
expect.anything(), // context
|
|
79
|
+
expect.any(String), // traceId
|
|
80
|
+
'test.span', // span name
|
|
81
|
+
expect.any(Number), // spanKind
|
|
82
|
+
expect.objectContaining({ 'test.attr': 'value' }), // attributes
|
|
83
|
+
[], // links
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should respect per-span sampler decision to NOT_RECORD', () => {
|
|
88
|
+
const rejectingSampler = {
|
|
89
|
+
shouldSample: vi.fn(() => ({
|
|
90
|
+
decision: SamplingDecision.NOT_RECORD,
|
|
91
|
+
attributes: {},
|
|
92
|
+
traceState: undefined,
|
|
93
|
+
})),
|
|
94
|
+
toString: () => 'RejectingSampler',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const options: any = { sampler: rejectingSampler };
|
|
98
|
+
const span = tracer.startSpan('test.span', options);
|
|
99
|
+
|
|
100
|
+
// Span should be created but not sampled
|
|
101
|
+
expect(span).toBeDefined();
|
|
102
|
+
expect(span.spanContext().traceFlags & 1).toBe(0); // Not sampled
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should respect per-span sampler decision to RECORD_AND_SAMPLED', () => {
|
|
106
|
+
const acceptingSampler = {
|
|
107
|
+
shouldSample: vi.fn(() => ({
|
|
108
|
+
decision: SamplingDecision.RECORD_AND_SAMPLED,
|
|
109
|
+
attributes: {},
|
|
110
|
+
traceState: undefined,
|
|
111
|
+
})),
|
|
112
|
+
toString: () => 'AcceptingSampler',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const options: any = { sampler: acceptingSampler };
|
|
116
|
+
const span = tracer.startSpan('test.span', options);
|
|
117
|
+
|
|
118
|
+
// Span should be created and sampled
|
|
119
|
+
expect(span).toBeDefined();
|
|
120
|
+
expect(span.spanContext().traceFlags & 1).toBe(1); // Sampled
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight WorkerTracer for edge environments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Attributes,
|
|
7
|
+
Tracer,
|
|
8
|
+
Span,
|
|
9
|
+
SpanKind,
|
|
10
|
+
SpanOptions,
|
|
11
|
+
Context,
|
|
12
|
+
} from '@opentelemetry/api';
|
|
13
|
+
import {
|
|
14
|
+
context as api_context,
|
|
15
|
+
trace,
|
|
16
|
+
type SpanContext,
|
|
17
|
+
} from '@opentelemetry/api';
|
|
18
|
+
import { sanitizeAttributes } from '@opentelemetry/core';
|
|
19
|
+
import type { Resource } from '@opentelemetry/resources';
|
|
20
|
+
import {
|
|
21
|
+
type SpanProcessor,
|
|
22
|
+
RandomIdGenerator,
|
|
23
|
+
type ReadableSpan,
|
|
24
|
+
SamplingDecision,
|
|
25
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
26
|
+
|
|
27
|
+
import { SpanImpl } from './span';
|
|
28
|
+
import type { TraceFlushableSpanProcessor } from '../types';
|
|
29
|
+
|
|
30
|
+
const NewTraceFlags = {
|
|
31
|
+
RANDOM_TRACE_ID_SET: 2,
|
|
32
|
+
RANDOM_TRACE_ID_UNSET: 0,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
type NewTraceFlagValues =
|
|
36
|
+
| typeof NewTraceFlags.RANDOM_TRACE_ID_SET
|
|
37
|
+
| typeof NewTraceFlags.RANDOM_TRACE_ID_UNSET;
|
|
38
|
+
|
|
39
|
+
const idGenerator: RandomIdGenerator = new RandomIdGenerator();
|
|
40
|
+
|
|
41
|
+
let withNextSpanAttributes: Attributes;
|
|
42
|
+
|
|
43
|
+
function getFlagAt(flagSequence: number, position: number): number {
|
|
44
|
+
return ((flagSequence >> (position - 1)) & 1) * position;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* WorkerTracer - Lightweight tracer for edge environments
|
|
49
|
+
*/
|
|
50
|
+
export class WorkerTracer implements Tracer {
|
|
51
|
+
private readonly spanProcessors: TraceFlushableSpanProcessor[];
|
|
52
|
+
private readonly resource: Resource;
|
|
53
|
+
private headSampler: any; // Will be set via setHeadSampler
|
|
54
|
+
|
|
55
|
+
constructor(spanProcessors: SpanProcessor[], resource: Resource) {
|
|
56
|
+
this.spanProcessors = spanProcessors as TraceFlushableSpanProcessor[];
|
|
57
|
+
this.resource = resource;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the head sampler (called from config)
|
|
62
|
+
*/
|
|
63
|
+
setHeadSampler(sampler: any): void {
|
|
64
|
+
this.headSampler = sampler;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Force flush spans for a specific trace
|
|
69
|
+
*/
|
|
70
|
+
async forceFlush(traceId?: string) {
|
|
71
|
+
const promises = this.spanProcessors.map(async (spanProcessor) => {
|
|
72
|
+
await spanProcessor.forceFlush(traceId);
|
|
73
|
+
});
|
|
74
|
+
await Promise.allSettled(promises);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Add extra resource attributes
|
|
79
|
+
*/
|
|
80
|
+
addToResource(extra: Resource) {
|
|
81
|
+
this.resource.merge(extra);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Start a new span
|
|
86
|
+
*/
|
|
87
|
+
startSpan(
|
|
88
|
+
name: string,
|
|
89
|
+
options: SpanOptions = {},
|
|
90
|
+
context = api_context.active(),
|
|
91
|
+
): Span {
|
|
92
|
+
if (options.root) {
|
|
93
|
+
context = trace.deleteSpan(context);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!this.headSampler) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Head sampler not configured. This is a bug in the instrumentation logic',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parentSpanContext = trace.getSpan(context)?.spanContext();
|
|
103
|
+
const { traceId, randomTraceFlag } = getTraceInfo(parentSpanContext);
|
|
104
|
+
|
|
105
|
+
const spanKind = options.kind || (0 as SpanKind); // SpanKind.INTERNAL
|
|
106
|
+
const sanitisedAttrs = sanitizeAttributes(options.attributes);
|
|
107
|
+
|
|
108
|
+
// Use per-span sampler if provided, otherwise use head sampler
|
|
109
|
+
const optionsWithSampler = options as any;
|
|
110
|
+
const sampler = optionsWithSampler.sampler || this.headSampler;
|
|
111
|
+
|
|
112
|
+
const samplingDecision = sampler.shouldSample(
|
|
113
|
+
context,
|
|
114
|
+
traceId,
|
|
115
|
+
name,
|
|
116
|
+
spanKind,
|
|
117
|
+
sanitisedAttrs,
|
|
118
|
+
[],
|
|
119
|
+
);
|
|
120
|
+
const { decision, traceState, attributes: attrs } = samplingDecision;
|
|
121
|
+
|
|
122
|
+
const attributes = Object.assign(
|
|
123
|
+
{},
|
|
124
|
+
options.attributes,
|
|
125
|
+
attrs,
|
|
126
|
+
withNextSpanAttributes,
|
|
127
|
+
);
|
|
128
|
+
withNextSpanAttributes = {};
|
|
129
|
+
|
|
130
|
+
const spanId = idGenerator.generateSpanId();
|
|
131
|
+
const parentSpanId = parentSpanContext?.spanId;
|
|
132
|
+
|
|
133
|
+
const sampleFlag =
|
|
134
|
+
decision === SamplingDecision.RECORD_AND_SAMPLED ? 1 : 0; // TraceFlags.SAMPLED : TraceFlags.NONE
|
|
135
|
+
const traceFlags = sampleFlag + randomTraceFlag;
|
|
136
|
+
const spanContext: SpanContext = { traceId, spanId, traceFlags, traceState };
|
|
137
|
+
|
|
138
|
+
const span = new SpanImpl({
|
|
139
|
+
attributes: sanitizeAttributes(attributes),
|
|
140
|
+
name,
|
|
141
|
+
onEnd: (span) => {
|
|
142
|
+
for (const sp of this.spanProcessors) {
|
|
143
|
+
sp.onEnd(span as unknown as ReadableSpan);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
resource: this.resource,
|
|
147
|
+
spanContext,
|
|
148
|
+
parentSpanContext,
|
|
149
|
+
parentSpanId,
|
|
150
|
+
spanKind,
|
|
151
|
+
startTime: options.startTime,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (const sp of this.spanProcessors) {
|
|
155
|
+
//@ts-ignore - OTel type quirk
|
|
156
|
+
sp.onStart(span, context);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return span;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Start an active span (with automatic context management)
|
|
164
|
+
*/
|
|
165
|
+
startActiveSpan<F extends (span: Span) => ReturnType<F>>(
|
|
166
|
+
name: string,
|
|
167
|
+
fn: F,
|
|
168
|
+
): ReturnType<F>;
|
|
169
|
+
startActiveSpan<F extends (span: Span) => ReturnType<F>>(
|
|
170
|
+
name: string,
|
|
171
|
+
options: SpanOptions,
|
|
172
|
+
fn: F,
|
|
173
|
+
): ReturnType<F>;
|
|
174
|
+
startActiveSpan<F extends (span: Span) => ReturnType<F>>(
|
|
175
|
+
name: string,
|
|
176
|
+
options: SpanOptions,
|
|
177
|
+
context: Context,
|
|
178
|
+
fn: F,
|
|
179
|
+
): ReturnType<F>;
|
|
180
|
+
startActiveSpan<F extends (span: Span) => ReturnType<F>>(
|
|
181
|
+
name: string,
|
|
182
|
+
...args: unknown[]
|
|
183
|
+
): ReturnType<F> {
|
|
184
|
+
const options = args.length > 1 ? (args[0] as SpanOptions) : undefined;
|
|
185
|
+
const parentContext =
|
|
186
|
+
args.length > 2 ? (args[1] as Context) : api_context.active();
|
|
187
|
+
const fn = args.at(-1) as F;
|
|
188
|
+
|
|
189
|
+
const span = this.startSpan(name, options, parentContext);
|
|
190
|
+
const contextWithSpanSet = trace.setSpan(parentContext, span);
|
|
191
|
+
|
|
192
|
+
return api_context.with(contextWithSpanSet, fn, undefined, span);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Set attributes for the next span created
|
|
198
|
+
*/
|
|
199
|
+
export function withNextSpan(attrs: Attributes) {
|
|
200
|
+
withNextSpanAttributes = Object.assign({}, withNextSpanAttributes, attrs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getTraceInfo(parentSpanContext?: SpanContext): {
|
|
204
|
+
traceId: string;
|
|
205
|
+
randomTraceFlag: NewTraceFlagValues;
|
|
206
|
+
} {
|
|
207
|
+
if (parentSpanContext && trace.isSpanContextValid(parentSpanContext)) {
|
|
208
|
+
const { traceId, traceFlags } = parentSpanContext;
|
|
209
|
+
return { traceId, randomTraceFlag: getFlagAt(traceFlags, 2) as NewTraceFlagValues };
|
|
210
|
+
} else {
|
|
211
|
+
return {
|
|
212
|
+
traceId: idGenerator.generateTraceId(),
|
|
213
|
+
randomTraceFlag: NewTraceFlags.RANDOM_TRACE_ID_SET,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|