@uploadista/observability 0.0.18-beta.8 → 0.0.18
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/dist/index.cjs +1 -1
- package/dist/index.d.cts +1048 -74
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1096 -122
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
- package/src/core/exporters.ts +475 -0
- package/src/core/full-observability.ts +313 -0
- package/src/core/index.ts +5 -0
- package/src/core/logs-sdk.ts +441 -0
- package/src/core/metrics-sdk.ts +279 -0
- package/src/core/tracing.ts +417 -1
- package/src/core/types.ts +44 -0
- package/src/flow/layers.ts +9 -1
- package/src/flow/metrics.ts +22 -10
- package/src/flow/tracing.ts +74 -2
- package/src/index.ts +0 -2
- package/tests/core/exporters.test.ts +235 -0
package/src/core/tracing.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { NodeSdk, WebSdk } from "@effect/opentelemetry";
|
|
2
|
+
import { trace } from "@opentelemetry/api";
|
|
2
3
|
import {
|
|
3
4
|
BatchSpanProcessor,
|
|
4
5
|
ConsoleSpanExporter,
|
|
6
|
+
type SpanProcessor,
|
|
5
7
|
} from "@opentelemetry/sdk-trace-base";
|
|
6
|
-
import { Context, Effect, Layer } from "effect";
|
|
8
|
+
import { Context, Effect, Layer, Option, Tracer } from "effect";
|
|
9
|
+
import {
|
|
10
|
+
createOtlpTraceExporter,
|
|
11
|
+
getServiceName,
|
|
12
|
+
isOtlpExportEnabled,
|
|
13
|
+
parseResourceAttributes,
|
|
14
|
+
} from "./exporters.js";
|
|
15
|
+
import type { TraceContext } from "./types.js";
|
|
7
16
|
|
|
8
17
|
// ============================================================================
|
|
9
18
|
// Universal Tracing (Environment-agnostic)
|
|
@@ -65,3 +74,410 @@ export const WorkersSdkLive = WebSdk.layer(() => ({
|
|
|
65
74
|
// Export span data to the console in Workers environment
|
|
66
75
|
spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
|
|
67
76
|
}));
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// OTLP Export Layers (Production)
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Configuration options for OTLP SDK layers.
|
|
84
|
+
*/
|
|
85
|
+
export interface OtlpSdkConfig {
|
|
86
|
+
/** Service name for traces. Defaults to OTEL_SERVICE_NAME or "uploadista" */
|
|
87
|
+
serviceName?: string;
|
|
88
|
+
/** Additional resource attributes to include in all spans */
|
|
89
|
+
resourceAttributes?: Record<string, string>;
|
|
90
|
+
/** Maximum queue size for batch processor. Defaults to 512 */
|
|
91
|
+
maxQueueSize?: number;
|
|
92
|
+
/** Maximum export batch size. Defaults to 512 */
|
|
93
|
+
maxExportBatchSize?: number;
|
|
94
|
+
/** Schedule delay in milliseconds. Defaults to 5000 */
|
|
95
|
+
scheduledDelayMillis?: number;
|
|
96
|
+
/** Export timeout in milliseconds. Defaults to 5000 */
|
|
97
|
+
exportTimeoutMillis?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates a BatchSpanProcessor with OTLP exporter and graceful degradation.
|
|
102
|
+
*
|
|
103
|
+
* The processor is configured with:
|
|
104
|
+
* - Configurable queue limits to prevent memory issues
|
|
105
|
+
* - Export timeouts to prevent blocking
|
|
106
|
+
* - Error handling that drops data rather than failing requests
|
|
107
|
+
*
|
|
108
|
+
* @param config - Optional configuration
|
|
109
|
+
* @returns Configured BatchSpanProcessor
|
|
110
|
+
*/
|
|
111
|
+
function createOtlpSpanProcessor(config: OtlpSdkConfig = {}): SpanProcessor {
|
|
112
|
+
const exporter = createOtlpTraceExporter();
|
|
113
|
+
|
|
114
|
+
return new BatchSpanProcessor(exporter, {
|
|
115
|
+
maxQueueSize: config.maxQueueSize ?? 512,
|
|
116
|
+
maxExportBatchSize: config.maxExportBatchSize ?? 512,
|
|
117
|
+
scheduledDelayMillis: config.scheduledDelayMillis ?? 5000,
|
|
118
|
+
// Default to 30 seconds to accommodate cloud endpoints like Grafana Cloud
|
|
119
|
+
exportTimeoutMillis: config.exportTimeoutMillis ?? 30000,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Creates resource configuration from environment and config.
|
|
125
|
+
*/
|
|
126
|
+
function createResourceConfig(config: OtlpSdkConfig = {}): {
|
|
127
|
+
serviceName: string;
|
|
128
|
+
[key: string]: string;
|
|
129
|
+
} {
|
|
130
|
+
const serviceName = config.serviceName ?? getServiceName("uploadista");
|
|
131
|
+
const envAttributes = parseResourceAttributes();
|
|
132
|
+
const configAttributes = config.resourceAttributes ?? {};
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
serviceName,
|
|
136
|
+
...envAttributes,
|
|
137
|
+
...configAttributes,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Node.js OTLP SDK Layer for production use.
|
|
143
|
+
*
|
|
144
|
+
* Exports traces to an OTLP-compatible endpoint configured via environment variables:
|
|
145
|
+
* - OTEL_EXPORTER_OTLP_ENDPOINT: Base endpoint (default: http://localhost:4318)
|
|
146
|
+
* - OTEL_EXPORTER_OTLP_HEADERS: Authentication headers
|
|
147
|
+
* - OTEL_SERVICE_NAME: Service name (default: uploadista)
|
|
148
|
+
* - OTEL_RESOURCE_ATTRIBUTES: Additional resource attributes
|
|
149
|
+
* - UPLOADISTA_OBSERVABILITY_ENABLED: Set to "false" to disable (default: true)
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* import { OtlpNodeSdkLive } from "@uploadista/observability";
|
|
154
|
+
* import { Effect } from "effect";
|
|
155
|
+
*
|
|
156
|
+
* // With default environment configuration
|
|
157
|
+
* const program = myEffect.pipe(Effect.provide(OtlpNodeSdkLive));
|
|
158
|
+
*
|
|
159
|
+
* // Run with:
|
|
160
|
+
* // OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
161
|
+
* // OTEL_SERVICE_NAME=my-upload-service
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export const OtlpNodeSdkLive = NodeSdk.layer(() => {
|
|
165
|
+
// Check if observability is disabled
|
|
166
|
+
if (!isOtlpExportEnabled()) {
|
|
167
|
+
// Return no-op configuration (no span processor means no export)
|
|
168
|
+
return {
|
|
169
|
+
resource: createResourceConfig(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
resource: createResourceConfig(),
|
|
175
|
+
spanProcessor: createOtlpSpanProcessor(),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Creates a customized OTLP Node.js SDK Layer.
|
|
181
|
+
*
|
|
182
|
+
* Use this when you need to customize the SDK configuration beyond
|
|
183
|
+
* what environment variables provide.
|
|
184
|
+
*
|
|
185
|
+
* @param config - Custom configuration options
|
|
186
|
+
* @returns Configured Effect Layer
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* const customSdk = createOtlpNodeSdkLayer({
|
|
191
|
+
* serviceName: "my-custom-service",
|
|
192
|
+
* resourceAttributes: {
|
|
193
|
+
* "tenant.id": "abc123",
|
|
194
|
+
* "deployment.environment": "production"
|
|
195
|
+
* },
|
|
196
|
+
* maxQueueSize: 1024,
|
|
197
|
+
* });
|
|
198
|
+
*
|
|
199
|
+
* const program = myEffect.pipe(Effect.provide(customSdk));
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function createOtlpNodeSdkLayer(config: OtlpSdkConfig = {}) {
|
|
203
|
+
return NodeSdk.layer(() => {
|
|
204
|
+
if (!isOtlpExportEnabled()) {
|
|
205
|
+
return {
|
|
206
|
+
resource: createResourceConfig(config),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
resource: createResourceConfig(config),
|
|
212
|
+
spanProcessor: createOtlpSpanProcessor(config),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Browser OTLP SDK Layer for production use.
|
|
219
|
+
*
|
|
220
|
+
* Similar to OtlpNodeSdkLive but uses fetch API for browser compatibility.
|
|
221
|
+
* Note: Browser environments may have CORS restrictions.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* import { OtlpWebSdkLive } from "@uploadista/observability";
|
|
226
|
+
*
|
|
227
|
+
* const program = myEffect.pipe(Effect.provide(OtlpWebSdkLive));
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export const OtlpWebSdkLive = WebSdk.layer(() => {
|
|
231
|
+
if (!isOtlpExportEnabled()) {
|
|
232
|
+
return {
|
|
233
|
+
resource: createResourceConfig(),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
resource: createResourceConfig(),
|
|
239
|
+
spanProcessor: createOtlpSpanProcessor(),
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Creates a customized OTLP Web SDK Layer.
|
|
245
|
+
*
|
|
246
|
+
* @param config - Custom configuration options
|
|
247
|
+
* @returns Configured Effect Layer for browser environments
|
|
248
|
+
*/
|
|
249
|
+
export function createOtlpWebSdkLayer(config: OtlpSdkConfig = {}) {
|
|
250
|
+
return WebSdk.layer(() => {
|
|
251
|
+
if (!isOtlpExportEnabled()) {
|
|
252
|
+
return {
|
|
253
|
+
resource: createResourceConfig(config),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
resource: createResourceConfig(config),
|
|
259
|
+
spanProcessor: createOtlpSpanProcessor(config),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Cloudflare Workers OTLP SDK Layer for production use.
|
|
266
|
+
*
|
|
267
|
+
* Uses the Web SDK under the hood with fetch-based export.
|
|
268
|
+
* Suitable for edge computing environments.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* import { OtlpWorkersSdkLive } from "@uploadista/observability";
|
|
273
|
+
*
|
|
274
|
+
* export default {
|
|
275
|
+
* async fetch(request, env) {
|
|
276
|
+
* const program = handleRequest(request).pipe(
|
|
277
|
+
* Effect.provide(OtlpWorkersSdkLive)
|
|
278
|
+
* );
|
|
279
|
+
* return Effect.runPromise(program);
|
|
280
|
+
* }
|
|
281
|
+
* };
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export const OtlpWorkersSdkLive = WebSdk.layer(() => {
|
|
285
|
+
const config: OtlpSdkConfig = {
|
|
286
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (!isOtlpExportEnabled()) {
|
|
290
|
+
return {
|
|
291
|
+
resource: createResourceConfig(config),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
resource: createResourceConfig(config),
|
|
297
|
+
spanProcessor: createOtlpSpanProcessor(config),
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Creates a customized OTLP Workers SDK Layer.
|
|
303
|
+
*
|
|
304
|
+
* @param config - Custom configuration options
|
|
305
|
+
* @returns Configured Effect Layer for Cloudflare Workers
|
|
306
|
+
*/
|
|
307
|
+
export function createOtlpWorkersSdkLayer(config: OtlpSdkConfig = {}) {
|
|
308
|
+
return WebSdk.layer(() => {
|
|
309
|
+
const effectiveConfig = {
|
|
310
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
311
|
+
...config,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (!isOtlpExportEnabled()) {
|
|
315
|
+
return {
|
|
316
|
+
resource: createResourceConfig(effectiveConfig),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
resource: createResourceConfig(effectiveConfig),
|
|
322
|
+
spanProcessor: createOtlpSpanProcessor(effectiveConfig),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ============================================================================
|
|
328
|
+
// Distributed Tracing Context Utilities
|
|
329
|
+
// ============================================================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @deprecated Use `captureTraceContextEffect` instead. This synchronous function
|
|
333
|
+
* uses OpenTelemetry's `trace.getActiveSpan()` which may not be synchronized
|
|
334
|
+
* with Effect's span context when using @effect/opentelemetry.
|
|
335
|
+
*
|
|
336
|
+
* @returns TraceContext if there's an active OpenTelemetry span, undefined otherwise
|
|
337
|
+
*/
|
|
338
|
+
export function captureTraceContext(): TraceContext | undefined {
|
|
339
|
+
const currentSpan = trace.getActiveSpan();
|
|
340
|
+
if (!currentSpan) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const spanContext = currentSpan.spanContext();
|
|
345
|
+
return {
|
|
346
|
+
traceId: spanContext.traceId,
|
|
347
|
+
spanId: spanContext.spanId,
|
|
348
|
+
traceFlags: spanContext.traceFlags,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Captures the current Effect trace context for distributed tracing.
|
|
354
|
+
*
|
|
355
|
+
* Uses Effect's `currentSpan` to get the active span, which is more reliable
|
|
356
|
+
* than OpenTelemetry's `trace.getActiveSpan()` when using @effect/opentelemetry
|
|
357
|
+
* because Effect manages its own span context that may not be synchronized
|
|
358
|
+
* with OpenTelemetry's global context.
|
|
359
|
+
*
|
|
360
|
+
* Use this to save the trace context (traceId, spanId, traceFlags) for later
|
|
361
|
+
* use in distributed tracing. The captured context can be stored alongside
|
|
362
|
+
* data (e.g., in KV store with upload metadata) and restored later using
|
|
363
|
+
* `createExternalSpan` and passing it to `Effect.withSpan`'s `parent` option.
|
|
364
|
+
*
|
|
365
|
+
* @returns Effect yielding TraceContext if there's an active span, undefined otherwise
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* // Capture context during upload creation
|
|
370
|
+
* const createUpload = Effect.gen(function* () {
|
|
371
|
+
* const traceContext = yield* captureTraceContextEffect;
|
|
372
|
+
*
|
|
373
|
+
* const file: UploadFile = {
|
|
374
|
+
* id: uploadId,
|
|
375
|
+
* traceContext, // Store for later
|
|
376
|
+
* // ...
|
|
377
|
+
* };
|
|
378
|
+
* yield* kvStore.set(uploadId, file);
|
|
379
|
+
* }).pipe(Effect.withSpan("upload-create", { ... }));
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
export const captureTraceContextEffect: Effect.Effect<
|
|
383
|
+
TraceContext | undefined
|
|
384
|
+
> = Effect.gen(function* () {
|
|
385
|
+
const spanOption = yield* Effect.currentSpan.pipe(Effect.option);
|
|
386
|
+
return Option.match(spanOption, {
|
|
387
|
+
onNone: () => undefined,
|
|
388
|
+
onSome: (span) => ({
|
|
389
|
+
traceId: span.traceId,
|
|
390
|
+
spanId: span.spanId,
|
|
391
|
+
traceFlags: span.sampled ? 1 : 0,
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Creates an ExternalSpan from a stored trace context.
|
|
398
|
+
*
|
|
399
|
+
* Use this to create a parent span reference that can be passed to
|
|
400
|
+
* `Effect.withSpan`'s `parent` option for distributed tracing.
|
|
401
|
+
*
|
|
402
|
+
* **Important:** The parent must be passed directly to `Effect.withSpan`'s
|
|
403
|
+
* options, not provided as a service afterward.
|
|
404
|
+
*
|
|
405
|
+
* @param traceContext - Previously captured trace context
|
|
406
|
+
* @returns ExternalSpan that can be used as a parent in Effect.withSpan
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```typescript
|
|
410
|
+
* // Create parent span from stored trace context
|
|
411
|
+
* const parentSpan = file.traceContext
|
|
412
|
+
* ? createExternalSpan(file.traceContext)
|
|
413
|
+
* : undefined;
|
|
414
|
+
*
|
|
415
|
+
* // Pass parent directly to withSpan
|
|
416
|
+
* const chunkEffect = Effect.gen(function* () {
|
|
417
|
+
* // ... chunk upload logic
|
|
418
|
+
* }).pipe(
|
|
419
|
+
* Effect.withSpan("upload-chunk", {
|
|
420
|
+
* attributes: { ... },
|
|
421
|
+
* parent: parentSpan, // Link to original trace
|
|
422
|
+
* })
|
|
423
|
+
* );
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
export function createExternalSpan(traceContext: TraceContext) {
|
|
427
|
+
return Tracer.externalSpan({
|
|
428
|
+
traceId: traceContext.traceId,
|
|
429
|
+
spanId: traceContext.spanId,
|
|
430
|
+
sampled: traceContext.traceFlags === 1,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @deprecated Use `createExternalSpan` instead and pass the result to
|
|
436
|
+
* `Effect.withSpan`'s `parent` option directly. This function doesn't
|
|
437
|
+
* work correctly because Effect.withSpan reads the parent at construction
|
|
438
|
+
* time, not from the provided service.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* // Instead of:
|
|
443
|
+
* withParentContext(traceContext)(effect.pipe(Effect.withSpan(...)))
|
|
444
|
+
*
|
|
445
|
+
* // Do this:
|
|
446
|
+
* const parent = createExternalSpan(traceContext);
|
|
447
|
+
* effect.pipe(Effect.withSpan("name", { parent }))
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
export function withParentContext(traceContext: TraceContext) {
|
|
451
|
+
return <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => {
|
|
452
|
+
const externalSpan = Tracer.externalSpan({
|
|
453
|
+
traceId: traceContext.traceId,
|
|
454
|
+
spanId: traceContext.spanId,
|
|
455
|
+
sampled: traceContext.traceFlags === 1,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return effect.pipe(Effect.provideService(Tracer.ParentSpan, externalSpan));
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Checks if there's an active trace context.
|
|
464
|
+
*
|
|
465
|
+
* Useful for conditional logic based on whether tracing is active.
|
|
466
|
+
*
|
|
467
|
+
* @returns true if there's an active span with valid trace context
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* if (hasActiveTraceContext()) {
|
|
472
|
+
* console.log("Tracing is active");
|
|
473
|
+
* }
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
export function hasActiveTraceContext(): boolean {
|
|
477
|
+
const span = trace.getActiveSpan();
|
|
478
|
+
if (!span) return false;
|
|
479
|
+
|
|
480
|
+
const ctx = span.spanContext();
|
|
481
|
+
// Check if the trace ID is valid (not all zeros)
|
|
482
|
+
return ctx.traceId !== "00000000000000000000000000000000";
|
|
483
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry trace context types for distributed tracing.
|
|
3
|
+
*
|
|
4
|
+
* These types enable trace context propagation across HTTP requests,
|
|
5
|
+
* allowing spans from multiple requests to be grouped under a single trace.
|
|
6
|
+
*
|
|
7
|
+
* @module observability/core/types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trace context for distributed tracing.
|
|
12
|
+
*
|
|
13
|
+
* This structure holds the essential OpenTelemetry trace context that needs
|
|
14
|
+
* to be persisted and propagated across requests to maintain trace continuity.
|
|
15
|
+
*
|
|
16
|
+
* @property traceId - 128-bit unique identifier for the entire trace (32 hex chars)
|
|
17
|
+
* @property spanId - 64-bit unique identifier for the parent span (16 hex chars)
|
|
18
|
+
* @property traceFlags - Sampling decision (1 = sampled, 0 = not sampled)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Store trace context with upload metadata
|
|
23
|
+
* const traceContext: TraceContext = {
|
|
24
|
+
* traceId: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
|
|
25
|
+
* spanId: "a1b2c3d4e5f6a1b2",
|
|
26
|
+
* traceFlags: 1
|
|
27
|
+
* };
|
|
28
|
+
*
|
|
29
|
+
* // Later, restore context to link spans
|
|
30
|
+
* if (uploadFile.traceContext) {
|
|
31
|
+
* yield* withParentContext(uploadFile.traceContext)(
|
|
32
|
+
* Effect.withSpan("upload-chunk", { ... })(chunkEffect)
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export type TraceContext = {
|
|
38
|
+
/** 128-bit trace identifier (32 hex characters) */
|
|
39
|
+
traceId: string;
|
|
40
|
+
/** 64-bit span identifier (16 hex characters) */
|
|
41
|
+
spanId: string;
|
|
42
|
+
/** Trace flags (1 = sampled) */
|
|
43
|
+
traceFlags: number;
|
|
44
|
+
};
|
package/src/flow/layers.ts
CHANGED
|
@@ -66,13 +66,20 @@ export const withFlowDuration = <A, E, R>(
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Helper to track node duration
|
|
69
|
+
* @param nodeId - Unique node identifier
|
|
70
|
+
* @param nodeType - Generic node type (e.g., "optimize", "resize")
|
|
71
|
+
* @param effect - The effect to track
|
|
72
|
+
* @param nodeTypeId - Optional specific node type ID (e.g., "optimize-image", "resize-video")
|
|
69
73
|
*/
|
|
70
74
|
export const withNodeDuration = <A, E, R>(
|
|
71
75
|
nodeId: string,
|
|
72
76
|
nodeType: string,
|
|
73
77
|
effect: Effect.Effect<A, E, R>,
|
|
78
|
+
nodeTypeId?: string,
|
|
74
79
|
): Effect.Effect<A, E, R> => {
|
|
75
80
|
const metrics = createFlowMetrics();
|
|
81
|
+
// Use nodeTypeId for span name if available, fallback to nodeType
|
|
82
|
+
const spanName = nodeTypeId ?? nodeType;
|
|
76
83
|
return Effect.gen(function* () {
|
|
77
84
|
const startTime = Date.now();
|
|
78
85
|
const result = yield* effect;
|
|
@@ -81,10 +88,11 @@ export const withNodeDuration = <A, E, R>(
|
|
|
81
88
|
yield* Metric.update(metrics.nodeLatencySummary, duration);
|
|
82
89
|
return result;
|
|
83
90
|
}).pipe(
|
|
84
|
-
Effect.withSpan(`node-${
|
|
91
|
+
Effect.withSpan(`node-${spanName}`, {
|
|
85
92
|
attributes: {
|
|
86
93
|
"node.id": nodeId,
|
|
87
94
|
"node.type": nodeType,
|
|
95
|
+
"node.type_id": nodeTypeId ?? nodeType,
|
|
88
96
|
},
|
|
89
97
|
}),
|
|
90
98
|
);
|
package/src/flow/metrics.ts
CHANGED
|
@@ -124,32 +124,44 @@ export const createFlowMetrics = () => ({
|
|
|
124
124
|
|
|
125
125
|
/** Total number of times circuit breakers opened */
|
|
126
126
|
circuitBreakerOpenTotal: Metric.counter("circuit_breaker_open_total", {
|
|
127
|
-
description:
|
|
127
|
+
description:
|
|
128
|
+
"Total number of times circuit breakers transitioned to open state",
|
|
128
129
|
}),
|
|
129
130
|
|
|
130
131
|
/** Total number of times circuit breakers closed */
|
|
131
132
|
circuitBreakerCloseTotal: Metric.counter("circuit_breaker_close_total", {
|
|
132
|
-
description:
|
|
133
|
+
description:
|
|
134
|
+
"Total number of times circuit breakers transitioned to closed state",
|
|
133
135
|
}),
|
|
134
136
|
|
|
135
137
|
/** Total number of requests rejected by open circuit breakers */
|
|
136
|
-
circuitBreakerRejectedTotal: Metric.counter(
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
circuitBreakerRejectedTotal: Metric.counter(
|
|
139
|
+
"circuit_breaker_rejected_total",
|
|
140
|
+
{
|
|
141
|
+
description:
|
|
142
|
+
"Total number of requests rejected because circuit breaker is open",
|
|
143
|
+
},
|
|
144
|
+
),
|
|
139
145
|
|
|
140
146
|
/** Total number of times circuit breakers transitioned to half-open */
|
|
141
|
-
circuitBreakerHalfOpenTotal: Metric.counter(
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
circuitBreakerHalfOpenTotal: Metric.counter(
|
|
148
|
+
"circuit_breaker_half_open_total",
|
|
149
|
+
{
|
|
150
|
+
description:
|
|
151
|
+
"Total number of times circuit breakers transitioned to half-open state",
|
|
152
|
+
},
|
|
153
|
+
),
|
|
144
154
|
|
|
145
155
|
/** Current state of circuit breakers (0=closed, 1=open, 2=half-open) */
|
|
146
156
|
circuitBreakerStateGauge: Metric.gauge("circuit_breaker_state", {
|
|
147
|
-
description:
|
|
157
|
+
description:
|
|
158
|
+
"Current circuit breaker state (0=closed, 1=open, 2=half-open)",
|
|
148
159
|
}),
|
|
149
160
|
|
|
150
161
|
/** Number of failures in circuit breaker sliding window */
|
|
151
162
|
circuitBreakerFailuresGauge: Metric.gauge("circuit_breaker_failures", {
|
|
152
|
-
description:
|
|
163
|
+
description:
|
|
164
|
+
"Number of failures currently in the circuit breaker sliding window",
|
|
153
165
|
}),
|
|
154
166
|
});
|
|
155
167
|
|
package/src/flow/tracing.ts
CHANGED
|
@@ -71,6 +71,76 @@ export const withExecutionContext = (context: {
|
|
|
71
71
|
"execution.parallel_count": context.parallelCount?.toString() ?? "0",
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Plugin Operation Tracing
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Operation domains for plugin-level tracing
|
|
80
|
+
*/
|
|
81
|
+
export type OperationDomain =
|
|
82
|
+
| "image"
|
|
83
|
+
| "video"
|
|
84
|
+
| "document"
|
|
85
|
+
| "ai"
|
|
86
|
+
| "virus-scan"
|
|
87
|
+
| "zip";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Wrap an Effect with a plugin operation span
|
|
91
|
+
*
|
|
92
|
+
* @param domain - The operation domain (e.g., "image", "video", "document")
|
|
93
|
+
* @param operation - The specific operation (e.g., "optimize", "transcode", "extract-text")
|
|
94
|
+
* @param attributes - Optional span attributes with operation-specific details
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* // Image optimization span
|
|
99
|
+
* withOperationSpan("image", "optimize", {
|
|
100
|
+
* "image.format": "webp",
|
|
101
|
+
* "image.quality": 80,
|
|
102
|
+
* })(imageService.optimize(inputBytes, params))
|
|
103
|
+
*
|
|
104
|
+
* // Video transcoding span
|
|
105
|
+
* withOperationSpan("video", "transcode", {
|
|
106
|
+
* "video.format": "mp4",
|
|
107
|
+
* "video.codec": "h264",
|
|
108
|
+
* })(videoService.transcode(inputBytes, params))
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const withOperationSpan =
|
|
112
|
+
<A, E, R>(
|
|
113
|
+
domain: OperationDomain,
|
|
114
|
+
operation: string,
|
|
115
|
+
attributes?: Record<string, unknown>,
|
|
116
|
+
) =>
|
|
117
|
+
(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
118
|
+
effect.pipe(
|
|
119
|
+
Effect.withSpan(`${domain}-${operation}`, {
|
|
120
|
+
attributes: {
|
|
121
|
+
"operation.domain": domain,
|
|
122
|
+
"operation.name": operation,
|
|
123
|
+
...attributes,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Add operation context to the current span
|
|
130
|
+
*/
|
|
131
|
+
export const withOperationContext = (context: {
|
|
132
|
+
domain: OperationDomain;
|
|
133
|
+
operation: string;
|
|
134
|
+
inputSize?: number;
|
|
135
|
+
outputSize?: number;
|
|
136
|
+
}) =>
|
|
137
|
+
Effect.annotateCurrentSpan({
|
|
138
|
+
"operation.domain": context.domain,
|
|
139
|
+
"operation.name": context.operation,
|
|
140
|
+
"operation.input_size": context.inputSize?.toString() ?? "unknown",
|
|
141
|
+
"operation.output_size": context.outputSize?.toString() ?? "unknown",
|
|
142
|
+
});
|
|
143
|
+
|
|
74
144
|
// ============================================================================
|
|
75
145
|
// Circuit Breaker Tracing
|
|
76
146
|
// ============================================================================
|
|
@@ -117,7 +187,8 @@ export const withCircuitBreakerContext = (context: {
|
|
|
117
187
|
"circuit_breaker.failure_count": context.failureCount?.toString() ?? "0",
|
|
118
188
|
"circuit_breaker.failure_threshold":
|
|
119
189
|
context.failureThreshold?.toString() ?? "5",
|
|
120
|
-
"circuit_breaker.reset_timeout":
|
|
190
|
+
"circuit_breaker.reset_timeout":
|
|
191
|
+
context.resetTimeout?.toString() ?? "30000",
|
|
121
192
|
"circuit_breaker.decision": context.decision ?? "unknown",
|
|
122
193
|
});
|
|
123
194
|
|
|
@@ -137,5 +208,6 @@ export const annotateCircuitBreakerStateChange = (event: {
|
|
|
137
208
|
"circuit_breaker.previous_state": event.previousState,
|
|
138
209
|
"circuit_breaker.new_state": event.newState,
|
|
139
210
|
"circuit_breaker.failure_count": event.failureCount?.toString() ?? "0",
|
|
140
|
-
"circuit_breaker.timestamp":
|
|
211
|
+
"circuit_breaker.timestamp":
|
|
212
|
+
event.timestamp?.toString() ?? Date.now().toString(),
|
|
141
213
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// Main observability package exports
|
|
2
2
|
export * from "./core/index.js";
|
|
3
|
-
// Tracing specific exports
|
|
4
|
-
export { NodeSdkLive, WebSdkLive, WorkersSdkLive } from "./core/tracing.js";
|
|
5
3
|
export * from "./flow/index.js";
|
|
6
4
|
export * from "./service/metrics.js";
|
|
7
5
|
export * from "./storage/index.js";
|