@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.
@@ -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
+ });