@littlebearapps/platform-consumer-sdk 1.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/README.md +306 -0
- package/package.json +53 -0
- package/src/ai-gateway.ts +305 -0
- package/src/constants.ts +147 -0
- package/src/costs.ts +590 -0
- package/src/do-heartbeat.ts +249 -0
- package/src/dynamic-patterns.ts +273 -0
- package/src/errors.ts +285 -0
- package/src/features.ts +149 -0
- package/src/heartbeat.ts +27 -0
- package/src/index.ts +950 -0
- package/src/logging.ts +543 -0
- package/src/middleware.ts +447 -0
- package/src/patterns.ts +156 -0
- package/src/proxy.ts +732 -0
- package/src/retry.ts +19 -0
- package/src/service-client.ts +291 -0
- package/src/telemetry.ts +342 -0
- package/src/timeout.ts +212 -0
- package/src/tracing.ts +403 -0
- package/src/types.ts +465 -0
package/src/timeout.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform SDK Timeout Middleware
|
|
5
|
+
*
|
|
6
|
+
* Provides standardised timeout handling for async operations.
|
|
7
|
+
* Returns 504 Gateway Timeout when operations exceed time limits.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { withTimeout, TimeoutError } from './lib/platform-sdk';
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* const result = await withTimeout(
|
|
15
|
+
* async () => await slowOperation(),
|
|
16
|
+
* 5000, // 5 seconds
|
|
17
|
+
* 'slow_operation'
|
|
18
|
+
* );
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* if (error instanceof TimeoutError) {
|
|
21
|
+
* return new Response('Gateway Timeout', { status: 504 });
|
|
22
|
+
* }
|
|
23
|
+
* throw error;
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { reportErrorExplicit } from './errors';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// TIMEOUT ERROR
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when an operation exceeds its timeout.
|
|
36
|
+
*/
|
|
37
|
+
export class TimeoutError extends Error {
|
|
38
|
+
/** The operation that timed out */
|
|
39
|
+
readonly operation: string;
|
|
40
|
+
/** Timeout duration in milliseconds */
|
|
41
|
+
readonly timeoutMs: number;
|
|
42
|
+
/** Actual duration before timeout (may be slightly less than timeoutMs) */
|
|
43
|
+
readonly actualMs: number;
|
|
44
|
+
|
|
45
|
+
constructor(operation: string, timeoutMs: number, actualMs: number) {
|
|
46
|
+
super(`Operation '${operation}' timed out after ${timeoutMs}ms`);
|
|
47
|
+
this.name = 'TimeoutError';
|
|
48
|
+
this.operation = operation;
|
|
49
|
+
this.timeoutMs = timeoutMs;
|
|
50
|
+
this.actualMs = actualMs;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// TIMEOUT CONFIGURATION
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default timeout values for different operation types.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_TIMEOUTS = {
|
|
62
|
+
/** Short operations (KV reads, simple D1 queries) */
|
|
63
|
+
short: 5000, // 5 seconds
|
|
64
|
+
/** Medium operations (API calls, complex queries) */
|
|
65
|
+
medium: 15000, // 15 seconds
|
|
66
|
+
/** Long operations (batch processing, AI inference) */
|
|
67
|
+
long: 30000, // 30 seconds
|
|
68
|
+
/** Maximum for any operation */
|
|
69
|
+
max: 60000, // 60 seconds
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// TIMEOUT WRAPPER
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute an async function with a timeout.
|
|
78
|
+
* Throws TimeoutError if the operation exceeds the specified duration.
|
|
79
|
+
*
|
|
80
|
+
* @param fn - Async function to execute
|
|
81
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
82
|
+
* @param operation - Operation name for error messages (default: 'operation')
|
|
83
|
+
* @returns The result of the async function
|
|
84
|
+
* @throws TimeoutError if the operation times out
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const data = await withTimeout(
|
|
89
|
+
* () => fetch('https://api.example.com/slow'),
|
|
90
|
+
* 10000,
|
|
91
|
+
* 'fetch_external_api'
|
|
92
|
+
* );
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export async function withTimeout<T>(
|
|
96
|
+
fn: () => Promise<T>,
|
|
97
|
+
timeoutMs: number = DEFAULT_TIMEOUTS.long,
|
|
98
|
+
operation: string = 'operation'
|
|
99
|
+
): Promise<T> {
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
|
|
102
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
const actualMs = Date.now() - startTime;
|
|
105
|
+
reject(new TimeoutError(operation, timeoutMs, actualMs));
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return Promise.race([fn(), timeoutPromise]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Execute an async function with timeout and automatic error reporting.
|
|
114
|
+
* Reports timeout as TIMEOUT category in telemetry.
|
|
115
|
+
*
|
|
116
|
+
* @param env - Tracked environment for error reporting
|
|
117
|
+
* @param fn - Async function to execute
|
|
118
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
119
|
+
* @param operation - Operation name for error messages
|
|
120
|
+
* @returns The result of the async function
|
|
121
|
+
* @throws TimeoutError if the operation times out
|
|
122
|
+
*/
|
|
123
|
+
export async function withTrackedTimeout<T>(
|
|
124
|
+
env: object,
|
|
125
|
+
fn: () => Promise<T>,
|
|
126
|
+
timeoutMs: number = DEFAULT_TIMEOUTS.long,
|
|
127
|
+
operation: string = 'operation'
|
|
128
|
+
): Promise<T> {
|
|
129
|
+
try {
|
|
130
|
+
return await withTimeout(fn, timeoutMs, operation);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof TimeoutError) {
|
|
133
|
+
reportErrorExplicit(env, 'TIMEOUT', `TIMEOUT_${operation.toUpperCase()}`);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// HTTP RESPONSE HELPERS
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a 504 Gateway Timeout response.
|
|
145
|
+
*
|
|
146
|
+
* @param error - Optional TimeoutError for details
|
|
147
|
+
* @returns 504 Response with JSON body
|
|
148
|
+
*/
|
|
149
|
+
export function timeoutResponse(error?: TimeoutError): Response {
|
|
150
|
+
const body = {
|
|
151
|
+
error: 'Gateway Timeout',
|
|
152
|
+
code: 'TIMEOUT',
|
|
153
|
+
operation: error?.operation,
|
|
154
|
+
timeout_ms: error?.timeoutMs,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return new Response(JSON.stringify(body), {
|
|
158
|
+
status: 504,
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if an error is a TimeoutError.
|
|
165
|
+
*/
|
|
166
|
+
export function isTimeoutError(error: unknown): error is TimeoutError {
|
|
167
|
+
return error instanceof TimeoutError;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// REQUEST HANDLER WRAPPER
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wrap a request handler with timeout handling.
|
|
176
|
+
* Returns 504 Gateway Timeout if the handler exceeds the timeout.
|
|
177
|
+
*
|
|
178
|
+
* @param handler - Request handler function
|
|
179
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
180
|
+
* @param operation - Operation name for logging
|
|
181
|
+
* @returns Wrapped handler that returns 504 on timeout
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* export default {
|
|
186
|
+
* fetch: withRequestTimeout(
|
|
187
|
+
* async (request, env, ctx) => {
|
|
188
|
+
* // Handler logic
|
|
189
|
+
* return new Response('OK');
|
|
190
|
+
* },
|
|
191
|
+
* 30000,
|
|
192
|
+
* 'api_handler'
|
|
193
|
+
* )
|
|
194
|
+
* };
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function withRequestTimeout<Env>(
|
|
198
|
+
handler: (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response>,
|
|
199
|
+
timeoutMs: number = DEFAULT_TIMEOUTS.long,
|
|
200
|
+
operation: string = 'request_handler'
|
|
201
|
+
): (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response> {
|
|
202
|
+
return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
|
|
203
|
+
try {
|
|
204
|
+
return await withTimeout(() => handler(request, env, ctx), timeoutMs, operation);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (isTimeoutError(error)) {
|
|
207
|
+
return timeoutResponse(error);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
package/src/tracing.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform SDK Distributed Tracing
|
|
5
|
+
*
|
|
6
|
+
* W3C Trace Context compliant distributed tracing for cross-service correlation.
|
|
7
|
+
* Implements trace propagation via standard headers (traceparent, tracestate).
|
|
8
|
+
*
|
|
9
|
+
* @see https://www.w3.org/TR/trace-context/
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createTraceContext, propagateTraceContext } from './lib/platform-sdk';
|
|
14
|
+
*
|
|
15
|
+
* // Extract from incoming request
|
|
16
|
+
* const trace = createTraceContext(request);
|
|
17
|
+
*
|
|
18
|
+
* // Propagate to outgoing request
|
|
19
|
+
* const headers = propagateTraceContext(trace);
|
|
20
|
+
* await fetch('https://api.example.com', { headers });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// TYPES
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* W3C Trace Context representation.
|
|
30
|
+
*/
|
|
31
|
+
export interface TraceContext {
|
|
32
|
+
/** Trace ID - 16 bytes hex (32 chars) */
|
|
33
|
+
traceId: string;
|
|
34
|
+
/** Parent span ID - 8 bytes hex (16 chars) */
|
|
35
|
+
spanId: string;
|
|
36
|
+
/** Trace flags (sampled = 01) */
|
|
37
|
+
traceFlags: number;
|
|
38
|
+
/** Optional tracestate for vendor-specific data */
|
|
39
|
+
traceState?: string;
|
|
40
|
+
/** Indicates if this is a new trace (not extracted from request) */
|
|
41
|
+
isNewTrace: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Span representation for nested operations.
|
|
46
|
+
*/
|
|
47
|
+
export interface Span {
|
|
48
|
+
/** Span ID - 8 bytes hex (16 chars) */
|
|
49
|
+
spanId: string;
|
|
50
|
+
/** Parent span ID */
|
|
51
|
+
parentSpanId: string;
|
|
52
|
+
/** Trace context */
|
|
53
|
+
traceContext: TraceContext;
|
|
54
|
+
/** Operation name */
|
|
55
|
+
name: string;
|
|
56
|
+
/** Start timestamp (ms) */
|
|
57
|
+
startTime: number;
|
|
58
|
+
/** End timestamp (ms) */
|
|
59
|
+
endTime?: number;
|
|
60
|
+
/** Span status */
|
|
61
|
+
status: 'ok' | 'error' | 'unset';
|
|
62
|
+
/** Span attributes */
|
|
63
|
+
attributes: Record<string, string | number | boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// CONSTANTS
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
/** W3C Trace Context version */
|
|
71
|
+
const TRACE_VERSION = '00';
|
|
72
|
+
|
|
73
|
+
/** W3C Traceparent header name */
|
|
74
|
+
const TRACEPARENT_HEADER = 'traceparent';
|
|
75
|
+
|
|
76
|
+
/** W3C Tracestate header name */
|
|
77
|
+
const TRACESTATE_HEADER = 'tracestate';
|
|
78
|
+
|
|
79
|
+
/** Trace flag: sampled */
|
|
80
|
+
const FLAG_SAMPLED = 0x01;
|
|
81
|
+
|
|
82
|
+
/** Regex for validating traceparent header */
|
|
83
|
+
const TRACEPARENT_REGEX = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// ID GENERATION
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate a random trace ID (16 bytes = 32 hex chars).
|
|
91
|
+
*/
|
|
92
|
+
export function generateTraceId(): string {
|
|
93
|
+
const bytes = new Uint8Array(16);
|
|
94
|
+
crypto.getRandomValues(bytes);
|
|
95
|
+
return Array.from(bytes)
|
|
96
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
97
|
+
.join('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Generate a random span ID (8 bytes = 16 hex chars).
|
|
102
|
+
*/
|
|
103
|
+
export function generateSpanId(): string {
|
|
104
|
+
const bytes = new Uint8Array(8);
|
|
105
|
+
crypto.getRandomValues(bytes);
|
|
106
|
+
return Array.from(bytes)
|
|
107
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
108
|
+
.join('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// TRACE CONTEXT MANAGEMENT
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* WeakMap to store trace context per environment.
|
|
117
|
+
*/
|
|
118
|
+
const traceContexts = new WeakMap<object, TraceContext>();
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get or create a trace context for an environment.
|
|
122
|
+
*/
|
|
123
|
+
export function getTraceContext(env: object): TraceContext {
|
|
124
|
+
let ctx = traceContexts.get(env);
|
|
125
|
+
if (!ctx) {
|
|
126
|
+
ctx = createNewTraceContext();
|
|
127
|
+
traceContexts.set(env, ctx);
|
|
128
|
+
}
|
|
129
|
+
return ctx;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set a trace context for an environment.
|
|
134
|
+
*/
|
|
135
|
+
export function setTraceContext(env: object, ctx: TraceContext): void {
|
|
136
|
+
traceContexts.set(env, ctx);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a new trace context (for new requests).
|
|
141
|
+
*/
|
|
142
|
+
export function createNewTraceContext(): TraceContext {
|
|
143
|
+
return {
|
|
144
|
+
traceId: generateTraceId(),
|
|
145
|
+
spanId: generateSpanId(),
|
|
146
|
+
traceFlags: FLAG_SAMPLED, // Always sampled by default
|
|
147
|
+
isNewTrace: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// PARSING AND SERIALIZATION
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Parse a W3C traceparent header.
|
|
157
|
+
* Format: {version}-{trace-id}-{parent-id}-{trace-flags}
|
|
158
|
+
* Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
|
159
|
+
*/
|
|
160
|
+
export function parseTraceparent(header: string): TraceContext | null {
|
|
161
|
+
const match = header.toLowerCase().match(TRACEPARENT_REGEX);
|
|
162
|
+
if (!match) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const [, version, traceId, spanId, flagsHex] = match;
|
|
167
|
+
|
|
168
|
+
// Validate version
|
|
169
|
+
if (version !== TRACE_VERSION) {
|
|
170
|
+
// Future versions may have different formats, but for now only support 00
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate trace ID is not all zeros
|
|
175
|
+
if (traceId === '00000000000000000000000000000000') {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Validate span ID is not all zeros
|
|
180
|
+
if (spanId === '0000000000000000') {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
traceId,
|
|
186
|
+
spanId,
|
|
187
|
+
traceFlags: parseInt(flagsHex, 16),
|
|
188
|
+
isNewTrace: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Format a TraceContext as a W3C traceparent header value.
|
|
194
|
+
*/
|
|
195
|
+
export function formatTraceparent(ctx: TraceContext): string {
|
|
196
|
+
const flags = ctx.traceFlags.toString(16).padStart(2, '0');
|
|
197
|
+
return `${TRACE_VERSION}-${ctx.traceId}-${ctx.spanId}-${flags}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// =============================================================================
|
|
201
|
+
// REQUEST HANDLING
|
|
202
|
+
// =============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Extract trace context from a request.
|
|
206
|
+
* Creates a new child span for the current operation.
|
|
207
|
+
*/
|
|
208
|
+
export function extractTraceContext(request: Request): TraceContext {
|
|
209
|
+
const traceparent = request.headers.get(TRACEPARENT_HEADER);
|
|
210
|
+
const tracestate = request.headers.get(TRACESTATE_HEADER);
|
|
211
|
+
|
|
212
|
+
if (traceparent) {
|
|
213
|
+
const parsed = parseTraceparent(traceparent);
|
|
214
|
+
if (parsed) {
|
|
215
|
+
// Create a new span ID for this operation, keeping the trace ID
|
|
216
|
+
return {
|
|
217
|
+
traceId: parsed.traceId,
|
|
218
|
+
spanId: generateSpanId(), // New span for this operation
|
|
219
|
+
traceFlags: parsed.traceFlags,
|
|
220
|
+
traceState: tracestate || undefined,
|
|
221
|
+
isNewTrace: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// No valid traceparent, create a new trace
|
|
227
|
+
return createNewTraceContext();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create trace context from a request and attach to environment.
|
|
232
|
+
* This is the main entry point for incoming requests.
|
|
233
|
+
*/
|
|
234
|
+
export function createTraceContext(request: Request, env: object): TraceContext {
|
|
235
|
+
const ctx = extractTraceContext(request);
|
|
236
|
+
setTraceContext(env, ctx);
|
|
237
|
+
return ctx;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// =============================================================================
|
|
241
|
+
// PROPAGATION
|
|
242
|
+
// =============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get headers for propagating trace context to outgoing requests.
|
|
246
|
+
* Creates a new span ID for the outgoing call.
|
|
247
|
+
*/
|
|
248
|
+
export function propagateTraceContext(ctx: TraceContext): Headers {
|
|
249
|
+
const headers = new Headers();
|
|
250
|
+
|
|
251
|
+
// Create a new span ID for the outgoing request (child span)
|
|
252
|
+
const childContext: TraceContext = {
|
|
253
|
+
...ctx,
|
|
254
|
+
spanId: generateSpanId(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
headers.set(TRACEPARENT_HEADER, formatTraceparent(childContext));
|
|
258
|
+
|
|
259
|
+
if (ctx.traceState) {
|
|
260
|
+
headers.set(TRACESTATE_HEADER, ctx.traceState);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return headers;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Merge trace propagation headers into existing headers.
|
|
268
|
+
*/
|
|
269
|
+
export function addTraceHeaders(
|
|
270
|
+
headers: Headers | Record<string, string>,
|
|
271
|
+
ctx: TraceContext
|
|
272
|
+
): Headers {
|
|
273
|
+
const propagationHeaders = propagateTraceContext(ctx);
|
|
274
|
+
const result = headers instanceof Headers ? new Headers(headers) : new Headers(headers);
|
|
275
|
+
|
|
276
|
+
propagationHeaders.forEach((value, key) => {
|
|
277
|
+
result.set(key, value);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create a fetch wrapper that automatically propagates trace context.
|
|
285
|
+
*/
|
|
286
|
+
export function createTracedFetch(
|
|
287
|
+
ctx: TraceContext
|
|
288
|
+
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
|
|
289
|
+
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
290
|
+
const headers = new Headers(init?.headers);
|
|
291
|
+
const propagationHeaders = propagateTraceContext(ctx);
|
|
292
|
+
|
|
293
|
+
propagationHeaders.forEach((value, key) => {
|
|
294
|
+
headers.set(key, value);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return fetch(input, {
|
|
298
|
+
...init,
|
|
299
|
+
headers,
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// SPAN MANAGEMENT
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Start a new span for an operation.
|
|
310
|
+
*/
|
|
311
|
+
export function startSpan(ctx: TraceContext, name: string): Span {
|
|
312
|
+
return {
|
|
313
|
+
spanId: generateSpanId(),
|
|
314
|
+
parentSpanId: ctx.spanId,
|
|
315
|
+
traceContext: ctx,
|
|
316
|
+
name,
|
|
317
|
+
startTime: Date.now(),
|
|
318
|
+
status: 'unset',
|
|
319
|
+
attributes: {},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* End a span with success status.
|
|
325
|
+
*/
|
|
326
|
+
export function endSpan(span: Span): Span {
|
|
327
|
+
return {
|
|
328
|
+
...span,
|
|
329
|
+
endTime: Date.now(),
|
|
330
|
+
status: span.status === 'unset' ? 'ok' : span.status,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* End a span with error status.
|
|
336
|
+
*/
|
|
337
|
+
export function failSpan(span: Span, error?: unknown): Span {
|
|
338
|
+
const result: Span = {
|
|
339
|
+
...span,
|
|
340
|
+
endTime: Date.now(),
|
|
341
|
+
status: 'error',
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if (error instanceof Error) {
|
|
345
|
+
result.attributes['error.type'] = error.name;
|
|
346
|
+
result.attributes['error.message'] = error.message;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Add an attribute to a span.
|
|
354
|
+
*/
|
|
355
|
+
export function setSpanAttribute(span: Span, key: string, value: string | number | boolean): Span {
|
|
356
|
+
return {
|
|
357
|
+
...span,
|
|
358
|
+
attributes: {
|
|
359
|
+
...span.attributes,
|
|
360
|
+
[key]: value,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// =============================================================================
|
|
366
|
+
// UTILITIES
|
|
367
|
+
// =============================================================================
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if a trace context is sampled (should be recorded).
|
|
371
|
+
*/
|
|
372
|
+
export function isSampled(ctx: TraceContext): boolean {
|
|
373
|
+
return (ctx.traceFlags & FLAG_SAMPLED) !== 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get a short trace ID for logging (first 8 chars).
|
|
378
|
+
*/
|
|
379
|
+
export function shortTraceId(ctx: TraceContext): string {
|
|
380
|
+
return ctx.traceId.substring(0, 8);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get a short span ID for logging (first 8 chars).
|
|
385
|
+
*/
|
|
386
|
+
export function shortSpanId(ctx: TraceContext): string {
|
|
387
|
+
return ctx.spanId.substring(0, 8);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Format trace context for log output.
|
|
392
|
+
*/
|
|
393
|
+
export function formatTraceForLog(ctx: TraceContext): {
|
|
394
|
+
trace_id: string;
|
|
395
|
+
span_id: string;
|
|
396
|
+
trace_flags: string;
|
|
397
|
+
} {
|
|
398
|
+
return {
|
|
399
|
+
trace_id: ctx.traceId,
|
|
400
|
+
span_id: ctx.spanId,
|
|
401
|
+
trace_flags: ctx.traceFlags.toString(16).padStart(2, '0'),
|
|
402
|
+
};
|
|
403
|
+
}
|