@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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP Logs SDK Layers for Uploadista SDK.
|
|
3
|
+
*
|
|
4
|
+
* This module provides Effect Layers that export logs to OTLP-compatible
|
|
5
|
+
* backends like Grafana Loki, Elasticsearch, and others.
|
|
6
|
+
*
|
|
7
|
+
* Logs are automatically enriched with:
|
|
8
|
+
* - trace_id and span_id for correlation with traces
|
|
9
|
+
* - Service name and resource attributes
|
|
10
|
+
* - Effect log annotations as attributes
|
|
11
|
+
*
|
|
12
|
+
* Configuration is done via standard OpenTelemetry environment variables:
|
|
13
|
+
* - OTEL_EXPORTER_OTLP_ENDPOINT: Base endpoint URL (default: http://localhost:4318)
|
|
14
|
+
* - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: Override endpoint for logs only
|
|
15
|
+
* - OTEL_EXPORTER_OTLP_HEADERS: Headers for authentication
|
|
16
|
+
* - OTEL_SERVICE_NAME: Service name (default: uploadista)
|
|
17
|
+
* - OTEL_LOGS_EXPORT_INTERVAL: Export interval in ms (default: 5000)
|
|
18
|
+
* - UPLOADISTA_OBSERVABILITY_ENABLED: Set to "false" to disable (default: true)
|
|
19
|
+
*
|
|
20
|
+
* @module core/logs-sdk
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { trace } from "@opentelemetry/api";
|
|
24
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
25
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
26
|
+
import type { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
27
|
+
import { Context, Effect, Logger as EffectLogger, Layer } from "effect";
|
|
28
|
+
import {
|
|
29
|
+
createOtlpLoggerProvider,
|
|
30
|
+
getServiceName,
|
|
31
|
+
isOtlpExportEnabled,
|
|
32
|
+
type LogsSdkConfig,
|
|
33
|
+
} from "./exporters.js";
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Logs Service
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* OpenTelemetry Logger service for emitting logs.
|
|
41
|
+
*/
|
|
42
|
+
export interface OtelLoggerService {
|
|
43
|
+
/** The OpenTelemetry Logger instance */
|
|
44
|
+
readonly logger: Logger;
|
|
45
|
+
/** The LoggerProvider for shutdown handling */
|
|
46
|
+
readonly provider: LoggerProvider;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Effect Context tag for the OTEL Logger service.
|
|
51
|
+
*/
|
|
52
|
+
export class OtelLogger extends Context.Tag("OtelLogger")<
|
|
53
|
+
OtelLogger,
|
|
54
|
+
OtelLoggerService
|
|
55
|
+
>() {}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Log Level Mapping
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Maps Effect log levels to OpenTelemetry SeverityNumber.
|
|
63
|
+
*
|
|
64
|
+
* | Effect Level | OTEL Severity |
|
|
65
|
+
* |--------------|---------------|
|
|
66
|
+
* | Debug | 5 (DEBUG) |
|
|
67
|
+
* | Info | 9 (INFO) |
|
|
68
|
+
* | Warning | 13 (WARN) |
|
|
69
|
+
* | Error | 17 (ERROR) |
|
|
70
|
+
* | Fatal | 21 (FATAL) |
|
|
71
|
+
*/
|
|
72
|
+
export function mapLogLevelToSeverity(level: string): SeverityNumber {
|
|
73
|
+
switch (level.toLowerCase()) {
|
|
74
|
+
case "debug":
|
|
75
|
+
case "trace":
|
|
76
|
+
return SeverityNumber.DEBUG;
|
|
77
|
+
case "info":
|
|
78
|
+
return SeverityNumber.INFO;
|
|
79
|
+
case "warning":
|
|
80
|
+
case "warn":
|
|
81
|
+
return SeverityNumber.WARN;
|
|
82
|
+
case "error":
|
|
83
|
+
return SeverityNumber.ERROR;
|
|
84
|
+
case "fatal":
|
|
85
|
+
case "critical":
|
|
86
|
+
return SeverityNumber.FATAL;
|
|
87
|
+
default:
|
|
88
|
+
return SeverityNumber.INFO;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Maps SeverityNumber to human-readable severity text.
|
|
94
|
+
*/
|
|
95
|
+
export function severityToText(severity: SeverityNumber): string {
|
|
96
|
+
if (severity <= SeverityNumber.DEBUG) return "DEBUG";
|
|
97
|
+
if (severity <= SeverityNumber.INFO) return "INFO";
|
|
98
|
+
if (severity <= SeverityNumber.WARN) return "WARN";
|
|
99
|
+
if (severity <= SeverityNumber.ERROR) return "ERROR";
|
|
100
|
+
return "FATAL";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Trace Context Injection
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the current trace context from OpenTelemetry.
|
|
109
|
+
*
|
|
110
|
+
* @returns Object with trace_id and span_id if active, empty object otherwise
|
|
111
|
+
*/
|
|
112
|
+
export function getTraceContext(): Record<string, string> {
|
|
113
|
+
const span = trace.getActiveSpan();
|
|
114
|
+
if (!span) {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ctx = span.spanContext();
|
|
119
|
+
// Only include if valid trace ID
|
|
120
|
+
if (ctx.traceId === "00000000000000000000000000000000") {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
trace_id: ctx.traceId,
|
|
126
|
+
span_id: ctx.spanId,
|
|
127
|
+
trace_flags: String(ctx.traceFlags),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Logs SDK Layers
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extended configuration for logs SDK.
|
|
137
|
+
*/
|
|
138
|
+
export interface LogsLayerConfig extends LogsSdkConfig {
|
|
139
|
+
/** Minimum severity level to export. Logs below this level are filtered. */
|
|
140
|
+
minSeverity?: SeverityNumber;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a Logs SDK layer with the given configuration.
|
|
145
|
+
*
|
|
146
|
+
* @param config - Logs SDK configuration
|
|
147
|
+
* @returns Effect Layer providing OtelLogger service
|
|
148
|
+
*/
|
|
149
|
+
function createLogsSdkLayer(config: LogsLayerConfig = {}) {
|
|
150
|
+
return Layer.scoped(
|
|
151
|
+
OtelLogger,
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
// Check if observability is disabled
|
|
154
|
+
if (!isOtlpExportEnabled()) {
|
|
155
|
+
// Return a no-op logger that doesn't export
|
|
156
|
+
const { LoggerProvider } = yield* Effect.promise(
|
|
157
|
+
() => import("@opentelemetry/sdk-logs"),
|
|
158
|
+
);
|
|
159
|
+
const noopProvider = new LoggerProvider();
|
|
160
|
+
return {
|
|
161
|
+
logger: noopProvider.getLogger(getServiceName("uploadista")),
|
|
162
|
+
provider: noopProvider,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create the OTLP LoggerProvider
|
|
167
|
+
const provider = createOtlpLoggerProvider(config);
|
|
168
|
+
const serviceName = config.serviceName ?? getServiceName("uploadista");
|
|
169
|
+
const logger = provider.getLogger(serviceName);
|
|
170
|
+
|
|
171
|
+
// Register shutdown handler
|
|
172
|
+
yield* Effect.addFinalizer(() =>
|
|
173
|
+
Effect.promise(async () => {
|
|
174
|
+
try {
|
|
175
|
+
await provider.shutdown();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Log but don't throw on shutdown errors
|
|
178
|
+
console.warn("Error shutting down LoggerProvider:", error);
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return { logger, provider };
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Node.js OTLP Logs SDK Layer for production use.
|
|
190
|
+
*
|
|
191
|
+
* Exports logs to an OTLP-compatible endpoint with automatic trace correlation.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* import { OtlpLogsNodeSdkLive, OtelLogger } from "@uploadista/observability";
|
|
196
|
+
* import { Effect } from "effect";
|
|
197
|
+
* import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
198
|
+
*
|
|
199
|
+
* const program = Effect.gen(function* () {
|
|
200
|
+
* const { logger } = yield* OtelLogger;
|
|
201
|
+
* logger.emit({
|
|
202
|
+
* severityNumber: SeverityNumber.INFO,
|
|
203
|
+
* body: "Upload completed",
|
|
204
|
+
* attributes: { uploadId: "123" },
|
|
205
|
+
* });
|
|
206
|
+
* }).pipe(Effect.provide(OtlpLogsNodeSdkLive));
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export const OtlpLogsNodeSdkLive = createLogsSdkLayer();
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates a customized OTLP Logs Node.js SDK Layer.
|
|
213
|
+
*
|
|
214
|
+
* @param config - Custom configuration options
|
|
215
|
+
* @returns Configured Effect Layer
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* const customLogs = createOtlpLogsNodeSdkLayer({
|
|
220
|
+
* serviceName: "my-upload-service",
|
|
221
|
+
* minSeverity: SeverityNumber.WARN, // Only export WARN and above
|
|
222
|
+
* });
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export function createOtlpLogsNodeSdkLayer(config: LogsLayerConfig = {}) {
|
|
226
|
+
return createLogsSdkLayer(config);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Browser OTLP Logs SDK Layer for production use.
|
|
231
|
+
*
|
|
232
|
+
* Uses the same OTLP HTTP exporter, suitable for browser environments.
|
|
233
|
+
* Note: Browser environments may have CORS restrictions.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* import { OtlpLogsWebSdkLive } from "@uploadista/observability";
|
|
238
|
+
*
|
|
239
|
+
* const program = myEffect.pipe(Effect.provide(OtlpLogsWebSdkLive));
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export const OtlpLogsWebSdkLive = createLogsSdkLayer();
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Creates a customized OTLP Logs Web SDK Layer.
|
|
246
|
+
*
|
|
247
|
+
* @param config - Custom configuration options
|
|
248
|
+
* @returns Configured Effect Layer for browser environments
|
|
249
|
+
*/
|
|
250
|
+
export function createOtlpLogsWebSdkLayer(config: LogsLayerConfig = {}) {
|
|
251
|
+
return createLogsSdkLayer(config);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Cloudflare Workers OTLP Logs SDK Layer for production use.
|
|
256
|
+
*
|
|
257
|
+
* Pre-configured with Workers-appropriate defaults.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* import { OtlpLogsWorkersSdkLive } from "@uploadista/observability";
|
|
262
|
+
*
|
|
263
|
+
* export default {
|
|
264
|
+
* async fetch(request, env) {
|
|
265
|
+
* const program = handleRequest(request).pipe(
|
|
266
|
+
* Effect.provide(OtlpLogsWorkersSdkLive)
|
|
267
|
+
* );
|
|
268
|
+
* return Effect.runPromise(program);
|
|
269
|
+
* }
|
|
270
|
+
* };
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export const OtlpLogsWorkersSdkLive = createLogsSdkLayer({
|
|
274
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Creates a customized OTLP Logs Workers SDK Layer.
|
|
279
|
+
*
|
|
280
|
+
* @param config - Custom configuration options
|
|
281
|
+
* @returns Configured Effect Layer for Cloudflare Workers
|
|
282
|
+
*/
|
|
283
|
+
export function createOtlpLogsWorkersSdkLayer(config: LogsLayerConfig = {}) {
|
|
284
|
+
return createLogsSdkLayer({
|
|
285
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
286
|
+
...config,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Effect Logger Integration
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Creates an Effect Logger that forwards logs to OTLP.
|
|
296
|
+
*
|
|
297
|
+
* This logger intercepts Effect.log calls and sends them to the OTLP endpoint
|
|
298
|
+
* with automatic trace correlation and annotation support.
|
|
299
|
+
*
|
|
300
|
+
* @param minSeverity - Minimum severity to export (default: all)
|
|
301
|
+
* @returns Effect Logger that exports to OTLP
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* import { createOtlpEffectLogger, OtlpLogsNodeSdkLive } from "@uploadista/observability";
|
|
306
|
+
*
|
|
307
|
+
* const program = Effect.gen(function* () {
|
|
308
|
+
* yield* Effect.log("This will be exported to OTLP");
|
|
309
|
+
* yield* Effect.logError("Errors too!");
|
|
310
|
+
* }).pipe(
|
|
311
|
+
* Effect.provide(OtlpLogsNodeSdkLive),
|
|
312
|
+
* EffectLogger.withMinimumLogLevel(LogLevel.Debug),
|
|
313
|
+
* );
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export const createOtlpEffectLogger = (minSeverity?: SeverityNumber) =>
|
|
317
|
+
EffectLogger.make<unknown, void>(({ logLevel, message, annotations }) => {
|
|
318
|
+
// This is a synchronous logger - we'll emit to OTEL directly
|
|
319
|
+
// In a real implementation, we'd need to access the OtelLogger from context
|
|
320
|
+
// For now, we'll use the global approach
|
|
321
|
+
|
|
322
|
+
const severity = mapLogLevelToSeverity(logLevel.label);
|
|
323
|
+
|
|
324
|
+
// Filter by minimum severity if specified
|
|
325
|
+
if (minSeverity !== undefined && severity < minSeverity) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Get trace context for correlation
|
|
330
|
+
const traceContext = getTraceContext();
|
|
331
|
+
|
|
332
|
+
// Convert annotations to attributes
|
|
333
|
+
const attributes: Record<string, unknown> = {
|
|
334
|
+
...traceContext,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Add annotations as attributes
|
|
338
|
+
for (const [key, value] of annotations) {
|
|
339
|
+
if (
|
|
340
|
+
typeof value === "string" ||
|
|
341
|
+
typeof value === "number" ||
|
|
342
|
+
typeof value === "boolean"
|
|
343
|
+
) {
|
|
344
|
+
attributes[key] = value;
|
|
345
|
+
} else {
|
|
346
|
+
attributes[key] = String(value);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Log to console as fallback (the actual OTLP export happens in the layer)
|
|
351
|
+
// This ensures logs are not lost even if OTLP export fails
|
|
352
|
+
const logFn =
|
|
353
|
+
severity >= SeverityNumber.ERROR
|
|
354
|
+
? console.error
|
|
355
|
+
: severity >= SeverityNumber.WARN
|
|
356
|
+
? console.warn
|
|
357
|
+
: console.log;
|
|
358
|
+
|
|
359
|
+
logFn(`[${logLevel.label}] ${String(message)}`, attributes);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// Utility Functions
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Emits a log record to OTLP using the OtelLogger from context.
|
|
368
|
+
*
|
|
369
|
+
* @param level - Log level (debug, info, warn, error, fatal)
|
|
370
|
+
* @param message - Log message
|
|
371
|
+
* @param attributes - Optional log attributes
|
|
372
|
+
* @returns Effect that emits the log
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```typescript
|
|
376
|
+
* yield* emitLog("info", "Upload completed", { uploadId: "123" });
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
export const emitLog = (
|
|
380
|
+
level: "debug" | "info" | "warn" | "error" | "fatal",
|
|
381
|
+
message: string,
|
|
382
|
+
attributes?: Record<string, string | number | boolean>,
|
|
383
|
+
) =>
|
|
384
|
+
Effect.gen(function* () {
|
|
385
|
+
const { logger } = yield* OtelLogger;
|
|
386
|
+
const severity = mapLogLevelToSeverity(level);
|
|
387
|
+
const traceContext = getTraceContext();
|
|
388
|
+
|
|
389
|
+
logger.emit({
|
|
390
|
+
severityNumber: severity,
|
|
391
|
+
severityText: severityToText(severity),
|
|
392
|
+
body: message,
|
|
393
|
+
attributes: {
|
|
394
|
+
...traceContext,
|
|
395
|
+
...attributes,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Emits a debug log to OTLP.
|
|
402
|
+
*/
|
|
403
|
+
export const logDebug = (
|
|
404
|
+
message: string,
|
|
405
|
+
attributes?: Record<string, string | number | boolean>,
|
|
406
|
+
) => emitLog("debug", message, attributes);
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Emits an info log to OTLP.
|
|
410
|
+
*/
|
|
411
|
+
export const logInfo = (
|
|
412
|
+
message: string,
|
|
413
|
+
attributes?: Record<string, string | number | boolean>,
|
|
414
|
+
) => emitLog("info", message, attributes);
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Emits a warning log to OTLP.
|
|
418
|
+
*/
|
|
419
|
+
export const logWarn = (
|
|
420
|
+
message: string,
|
|
421
|
+
attributes?: Record<string, string | number | boolean>,
|
|
422
|
+
) => emitLog("warn", message, attributes);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Emits an error log to OTLP.
|
|
426
|
+
*/
|
|
427
|
+
export const logError = (
|
|
428
|
+
message: string,
|
|
429
|
+
attributes?: Record<string, string | number | boolean>,
|
|
430
|
+
) => emitLog("error", message, attributes);
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Emits a fatal log to OTLP.
|
|
434
|
+
*/
|
|
435
|
+
export const logFatal = (
|
|
436
|
+
message: string,
|
|
437
|
+
attributes?: Record<string, string | number | boolean>,
|
|
438
|
+
) => emitLog("fatal", message, attributes);
|
|
439
|
+
|
|
440
|
+
// Re-export SeverityNumber for convenience
|
|
441
|
+
export { SeverityNumber } from "@opentelemetry/api-logs";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OTLP Metrics SDK Layers for Uploadista SDK.
|
|
3
|
+
*
|
|
4
|
+
* This module provides Effect Layers that export metrics to OTLP-compatible
|
|
5
|
+
* backends like Grafana Mimir, Prometheus, Datadog, and others.
|
|
6
|
+
*
|
|
7
|
+
* Configuration is done via standard OpenTelemetry environment variables:
|
|
8
|
+
* - OTEL_EXPORTER_OTLP_ENDPOINT: Base endpoint URL (default: http://localhost:4318)
|
|
9
|
+
* - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Override endpoint for metrics only
|
|
10
|
+
* - OTEL_EXPORTER_OTLP_HEADERS: Headers for authentication
|
|
11
|
+
* - OTEL_SERVICE_NAME: Service name (default: uploadista)
|
|
12
|
+
* - OTEL_METRICS_EXPORT_INTERVAL: Export interval in ms (default: 60000)
|
|
13
|
+
* - UPLOADISTA_OBSERVABILITY_ENABLED: Set to "false" to disable (default: true)
|
|
14
|
+
*
|
|
15
|
+
* @module core/metrics-sdk
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Meter } from "@opentelemetry/api";
|
|
19
|
+
import type { MeterProvider } from "@opentelemetry/sdk-metrics";
|
|
20
|
+
import { Context, Effect, Layer } from "effect";
|
|
21
|
+
import {
|
|
22
|
+
createOtlpMeterProvider,
|
|
23
|
+
getServiceName,
|
|
24
|
+
isOtlpExportEnabled,
|
|
25
|
+
type MetricsSdkConfig,
|
|
26
|
+
} from "./exporters.js";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Metrics Service
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* OpenTelemetry Meter service for recording metrics.
|
|
34
|
+
*/
|
|
35
|
+
export interface OtelMeterService {
|
|
36
|
+
/** The OpenTelemetry Meter instance */
|
|
37
|
+
readonly meter: Meter;
|
|
38
|
+
/** The MeterProvider for shutdown handling */
|
|
39
|
+
readonly provider: MeterProvider;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Effect Context tag for the OTEL Meter service.
|
|
44
|
+
*/
|
|
45
|
+
export class OtelMeter extends Context.Tag("OtelMeter")<
|
|
46
|
+
OtelMeter,
|
|
47
|
+
OtelMeterService
|
|
48
|
+
>() {}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Metrics SDK Layers
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a Metrics SDK layer with the given configuration.
|
|
56
|
+
*
|
|
57
|
+
* @param config - Metrics SDK configuration
|
|
58
|
+
* @returns Effect Layer providing OtelMeter service
|
|
59
|
+
*/
|
|
60
|
+
function createMetricsSdkLayer(config: MetricsSdkConfig = {}) {
|
|
61
|
+
return Layer.scoped(
|
|
62
|
+
OtelMeter,
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
// Check if observability is disabled
|
|
65
|
+
if (!isOtlpExportEnabled()) {
|
|
66
|
+
// Return a no-op meter that doesn't export
|
|
67
|
+
const { MeterProvider } = yield* Effect.promise(
|
|
68
|
+
() => import("@opentelemetry/sdk-metrics"),
|
|
69
|
+
);
|
|
70
|
+
const noopProvider = new MeterProvider();
|
|
71
|
+
return {
|
|
72
|
+
meter: noopProvider.getMeter(getServiceName("uploadista")),
|
|
73
|
+
provider: noopProvider,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create the OTLP MeterProvider
|
|
78
|
+
const provider = createOtlpMeterProvider(config);
|
|
79
|
+
const serviceName = config.serviceName ?? getServiceName("uploadista");
|
|
80
|
+
const meter = provider.getMeter(serviceName);
|
|
81
|
+
|
|
82
|
+
// Register shutdown handler
|
|
83
|
+
yield* Effect.addFinalizer(() =>
|
|
84
|
+
Effect.promise(async () => {
|
|
85
|
+
try {
|
|
86
|
+
await provider.shutdown();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Log but don't throw on shutdown errors
|
|
89
|
+
console.warn("Error shutting down MeterProvider:", error);
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { meter, provider };
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Node.js OTLP Metrics SDK Layer for production use.
|
|
101
|
+
*
|
|
102
|
+
* Exports metrics to an OTLP-compatible endpoint configured via environment variables.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* import { OtlpMetricsNodeSdkLive, OtelMeter } from "@uploadista/observability";
|
|
107
|
+
* import { Effect } from "effect";
|
|
108
|
+
*
|
|
109
|
+
* const program = Effect.gen(function* () {
|
|
110
|
+
* const { meter } = yield* OtelMeter;
|
|
111
|
+
* const counter = meter.createCounter("uploads_total");
|
|
112
|
+
* counter.add(1, { storage: "s3" });
|
|
113
|
+
* }).pipe(Effect.provide(OtlpMetricsNodeSdkLive));
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export const OtlpMetricsNodeSdkLive = createMetricsSdkLayer();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a customized OTLP Metrics Node.js SDK Layer.
|
|
120
|
+
*
|
|
121
|
+
* @param config - Custom configuration options
|
|
122
|
+
* @returns Configured Effect Layer
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const customMetrics = createOtlpMetricsNodeSdkLayer({
|
|
127
|
+
* serviceName: "my-upload-service",
|
|
128
|
+
* exportIntervalMillis: 30000, // Export every 30 seconds
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function createOtlpMetricsNodeSdkLayer(config: MetricsSdkConfig = {}) {
|
|
133
|
+
return createMetricsSdkLayer(config);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Browser OTLP Metrics SDK Layer for production use.
|
|
138
|
+
*
|
|
139
|
+
* Uses the same OTLP HTTP exporter, suitable for browser environments.
|
|
140
|
+
* Note: Browser environments may have CORS restrictions.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* import { OtlpMetricsWebSdkLive } from "@uploadista/observability";
|
|
145
|
+
*
|
|
146
|
+
* const program = myEffect.pipe(Effect.provide(OtlpMetricsWebSdkLive));
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export const OtlpMetricsWebSdkLive = createMetricsSdkLayer();
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates a customized OTLP Metrics Web SDK Layer.
|
|
153
|
+
*
|
|
154
|
+
* @param config - Custom configuration options
|
|
155
|
+
* @returns Configured Effect Layer for browser environments
|
|
156
|
+
*/
|
|
157
|
+
export function createOtlpMetricsWebSdkLayer(config: MetricsSdkConfig = {}) {
|
|
158
|
+
return createMetricsSdkLayer(config);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Cloudflare Workers OTLP Metrics SDK Layer for production use.
|
|
163
|
+
*
|
|
164
|
+
* Pre-configured with Workers-appropriate defaults.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* import { OtlpMetricsWorkersSdkLive } from "@uploadista/observability";
|
|
169
|
+
*
|
|
170
|
+
* export default {
|
|
171
|
+
* async fetch(request, env) {
|
|
172
|
+
* const program = handleRequest(request).pipe(
|
|
173
|
+
* Effect.provide(OtlpMetricsWorkersSdkLive)
|
|
174
|
+
* );
|
|
175
|
+
* return Effect.runPromise(program);
|
|
176
|
+
* }
|
|
177
|
+
* };
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export const OtlpMetricsWorkersSdkLive = createMetricsSdkLayer({
|
|
181
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates a customized OTLP Metrics Workers SDK Layer.
|
|
186
|
+
*
|
|
187
|
+
* @param config - Custom configuration options
|
|
188
|
+
* @returns Configured Effect Layer for Cloudflare Workers
|
|
189
|
+
*/
|
|
190
|
+
export function createOtlpMetricsWorkersSdkLayer(
|
|
191
|
+
config: MetricsSdkConfig = {},
|
|
192
|
+
) {
|
|
193
|
+
return createMetricsSdkLayer({
|
|
194
|
+
serviceName: getServiceName("uploadista-workers"),
|
|
195
|
+
...config,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Utility Functions
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Records a counter metric using the OTEL Meter from context.
|
|
205
|
+
*
|
|
206
|
+
* @param name - Counter name
|
|
207
|
+
* @param value - Value to add (default: 1)
|
|
208
|
+
* @param attributes - Optional metric attributes
|
|
209
|
+
* @returns Effect that records the counter
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* yield* recordCounter("uploads_total", 1, { storage: "s3" });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export const recordCounter = (
|
|
217
|
+
name: string,
|
|
218
|
+
value = 1,
|
|
219
|
+
attributes?: Record<string, string | number | boolean>,
|
|
220
|
+
) =>
|
|
221
|
+
Effect.gen(function* () {
|
|
222
|
+
const { meter } = yield* OtelMeter;
|
|
223
|
+
const counter = meter.createCounter(name);
|
|
224
|
+
counter.add(value, attributes);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Records a histogram metric using the OTEL Meter from context.
|
|
229
|
+
*
|
|
230
|
+
* @param name - Histogram name
|
|
231
|
+
* @param value - Value to record
|
|
232
|
+
* @param attributes - Optional metric attributes
|
|
233
|
+
* @returns Effect that records the histogram
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* yield* recordHistogram("upload_duration_seconds", 1.5, { storage: "s3" });
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export const recordHistogram = (
|
|
241
|
+
name: string,
|
|
242
|
+
value: number,
|
|
243
|
+
attributes?: Record<string, string | number | boolean>,
|
|
244
|
+
) =>
|
|
245
|
+
Effect.gen(function* () {
|
|
246
|
+
const { meter } = yield* OtelMeter;
|
|
247
|
+
const histogram = meter.createHistogram(name);
|
|
248
|
+
histogram.record(value, attributes);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Creates an observable gauge that reports the current value.
|
|
253
|
+
*
|
|
254
|
+
* @param name - Gauge name
|
|
255
|
+
* @param callback - Function that returns the current value
|
|
256
|
+
* @param attributes - Optional metric attributes
|
|
257
|
+
* @returns Effect that registers the gauge
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* let activeUploads = 0;
|
|
262
|
+
* yield* createGauge("active_uploads", () => activeUploads);
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export const createGauge = (
|
|
266
|
+
name: string,
|
|
267
|
+
callback: () => number,
|
|
268
|
+
attributes?: Record<string, string | number | boolean>,
|
|
269
|
+
) =>
|
|
270
|
+
Effect.gen(function* () {
|
|
271
|
+
const { meter } = yield* OtelMeter;
|
|
272
|
+
meter
|
|
273
|
+
.createObservableGauge(name, {
|
|
274
|
+
description: name,
|
|
275
|
+
})
|
|
276
|
+
.addCallback((result) => {
|
|
277
|
+
result.observe(callback(), attributes);
|
|
278
|
+
});
|
|
279
|
+
});
|