| 1 | /** |
| 2 | * Records and reads measurements from Effect programs. |
| 3 | * |
| 4 | * A `Metric<Input, State>` accepts typed update values and stores an aggregated |
| 5 | * state that can be read directly or included in a snapshot. Metrics are used |
| 6 | * for counters, gauges, frequencies, histograms, summaries, and timers. This |
| 7 | * module includes metric constructors, update and read helpers, attributes, |
| 8 | * histogram boundaries, registry snapshots, text dumps, and controls for |
| 9 | * enabling runtime metrics. |
| 10 | * |
| 11 | * @since 2.0.0 |
| 12 | */ |
| 13 | |
| 14 | import * as Arr from "./Array.ts" |
| 15 | import * as Context from "./Context.ts" |
| 16 | import * as Duration from "./Duration.ts" |
| 17 | import type { Effect } from "./Effect.ts" |
| 18 | import type { Exit } from "./Exit.ts" |
| 19 | import { constUndefined, dual } from "./Function.ts" |
| 20 | import * as InternalEffect from "./internal/effect.ts" |
| 21 | import * as InternalMetric from "./internal/metric.ts" |
| 22 | import * as InternalRecord from "./internal/record.ts" |
| 23 | import * as Layer from "./Layer.ts" |
| 24 | import * as Order from "./Order.ts" |
| 25 | import type { Pipeable } from "./Pipeable.ts" |
| 26 | import { pipeArguments } from "./Pipeable.ts" |
| 27 | import * as Predicate from "./Predicate.ts" |
| 28 | import * as _String from "./String.ts" |
| 29 | import type { Contravariant, Covariant } from "./Types.ts" |
| 30 | |
| 31 | /** |
| 32 | * A `Metric<Input, State>` represents a concurrent metric which accepts update |
| 33 | * values of type `Input` and are aggregated to a value of type `State`. |
| 34 | * |
| 35 | * **Details** |
| 36 | * |
| 37 | * For example, a counter metric would have type `Metric<number, number>`, |
| 38 | * representing the fact that the metric can be updated with numbers (the amount |
| 39 | * to increment or decrement the counter by), and the state of the counter is a |
| 40 | * number. |
| 41 | * |
| 42 | * There are five primitive metric types supported by Effect: |
| 43 | * |
| 44 | * - Counters |
| 45 | * - Frequencies |
| 46 | * - Gauges |
| 47 | * - Histograms |
| 48 | * - Summaries |
| 49 | * |
| 50 | * **Example** (Using multiple metric types) |
| 51 | * |
| 52 | * ```ts import.meta.vitest |
| 53 | * import { Effect, Metric } from "effect" |
| 54 | * |
| 55 | * const program = Effect.gen(function*() { |
| 56 | * // Create different types of metrics |
| 57 | * const requestCounter: Metric.Counter<number> = Metric.counter("requests", { |
| 58 | * description: "Total requests processed" |
| 59 | * }) |
| 60 | * |
| 61 | * const memoryGauge: Metric.Gauge<number> = Metric.gauge("memory_usage", { |
| 62 | * description: "Current memory usage in MB" |
| 63 | * }) |
| 64 | * |
| 65 | * const statusFrequency: Metric.Frequency = Metric.frequency("status_codes", { |
| 66 | * description: "HTTP status code frequency" |
| 67 | * }) |
| 68 | * |
| 69 | * // All metrics share the same interface for updates and reads |
| 70 | * yield* Metric.update(requestCounter, 1) |
| 71 | * yield* Metric.update(memoryGauge, 128) |
| 72 | * yield* Metric.update(statusFrequency, "200") |
| 73 | * |
| 74 | * // All metrics can be read with Metric.value |
| 75 | * const counterState = yield* Metric.value(requestCounter) |
| 76 | * const gaugeState = yield* Metric.value(memoryGauge) |
| 77 | * const frequencyState = yield* Metric.value(statusFrequency) |
| 78 | * |
| 79 | * // Metrics have common properties accessible through the interface: |
| 80 | * // - id: unique identifier |
| 81 | * // - type: metric type ("Counter", "Gauge", "Frequency", etc.) |
| 82 | * // - description: optional human-readable description |
| 83 | * // - attributes: optional key-value attributes for tagging |
| 84 | * |
| 85 | * return { |
| 86 | * counter: { |
| 87 | * id: requestCounter.id, |
| 88 | * type: requestCounter.type, |
| 89 | * state: counterState |
| 90 | * }, |
| 91 | * gauge: { id: memoryGauge.id, type: memoryGauge.type, state: gaugeState }, |
| 92 | * frequency: { |
| 93 | * id: statusFrequency.id, |
| 94 | * type: statusFrequency.type, |
| 95 | * state: frequencyState |
| 96 | * } |
| 97 | * } |
| 98 | * }) |
| 99 | * |
| 100 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 101 | * const values = [ |
| 102 | * result.counter.state.count, |
| 103 | * result.gauge.state.value, |
| 104 | * result.frequency.state.occurrences.get("200") |
| 105 | * ] // => [1, 128, 1] |
| 106 | * ``` |
| 107 | * |
| 108 | * @category models |
| 109 | * @since 2.0.0 |
| 110 | */ |
| 111 | export interface Metric<in Input, out State> extends Pipeable { |
| 112 | readonly [TypeId]: typeof TypeId |
| 113 | readonly Input: Contravariant<Input> |
| 114 | readonly State: Covariant<State> |
| 115 | readonly id: string |
| 116 | readonly type: Metric.Type |
| 117 | readonly description: string | undefined |
| 118 | readonly attributes: Metric.AttributeSet | undefined |
| 119 | readonly valueUnsafe: (context: Context.Context<never>) => State |
| 120 | readonly updateUnsafe: (input: Input, context: Context.Context<never>) => void |
| 121 | readonly modifyUnsafe: (input: Input, context: Context.Context<never>) => void |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * A Counter metric that tracks cumulative values that typically only increase. |
| 126 | * |
| 127 | * **When to use** |
| 128 | * |
| 129 | * Use when counters are useful for tracking monotonically increasing values like request counts, |
| 130 | * bytes processed, errors encountered, or any value that accumulates over time. |
| 131 | * |
| 132 | * **Example** (Using counter metrics) |
| 133 | * |
| 134 | * ```ts import.meta.vitest |
| 135 | * import { Effect, Metric } from "effect" |
| 136 | * |
| 137 | * const program = Effect.gen(function*() { |
| 138 | * // Create different types of counters |
| 139 | * const requestCounter: Metric.Counter<number> = Metric.counter( |
| 140 | * "http_requests", |
| 141 | * { |
| 142 | * description: "Total HTTP requests processed", |
| 143 | * incremental: true // Only allows increments |
| 144 | * } |
| 145 | * ) |
| 146 | * |
| 147 | * const bytesCounter: Metric.Counter<bigint> = Metric.counter( |
| 148 | * "bytes_processed", |
| 149 | * { |
| 150 | * description: "Total bytes processed", |
| 151 | * bigint: true, |
| 152 | * attributes: { service: "data-processor" } |
| 153 | * } |
| 154 | * ) |
| 155 | * |
| 156 | * // Update counters |
| 157 | * yield* Metric.update(requestCounter, 1) // Increment by 1 |
| 158 | * yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6) |
| 159 | * yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes |
| 160 | * |
| 161 | * // Read counter state |
| 162 | * const requestState: Metric.CounterState<number> = yield* Metric.value( |
| 163 | * requestCounter |
| 164 | * ) |
| 165 | * const bytesState: Metric.CounterState<bigint> = yield* Metric.value( |
| 166 | * bytesCounter |
| 167 | * ) |
| 168 | * |
| 169 | * // Counter state contains: |
| 170 | * // - count: current accumulated value |
| 171 | * // - incremental: whether only increments are allowed |
| 172 | * |
| 173 | * return { |
| 174 | * requests: { |
| 175 | * count: requestState.count, |
| 176 | * incremental: requestState.incremental |
| 177 | * }, |
| 178 | * bytes: { count: bytesState.count, incremental: bytesState.incremental } |
| 179 | * } |
| 180 | * }) |
| 181 | * |
| 182 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 183 | * const counts = [result.requests.count, result.bytes.count] // => [6, 1024n] |
| 184 | * ``` |
| 185 | * |
| 186 | * @category models |
| 187 | * @since 2.0.0 |
| 188 | */ |
| 189 | export interface Counter<in Input extends number | bigint> extends Metric<Input, CounterState<Input>> {} |
| 190 | |
| 191 | /** |
| 192 | * State interface for Counter metrics containing the current count and increment mode. |
| 193 | * |
| 194 | * **Example** (Reading counter state) |
| 195 | * |
| 196 | * ```ts import.meta.vitest |
| 197 | * import { Effect, Metric } from "effect" |
| 198 | * |
| 199 | * const program = Effect.gen(function*() { |
| 200 | * // Create different types of counters |
| 201 | * const requestCounter = Metric.counter("http_requests_total") |
| 202 | * const errorCounter = Metric.counter("errors_total", { incremental: true }) |
| 203 | * const byteCounter = Metric.counter("bytes_processed", { bigint: true }) |
| 204 | * |
| 205 | * // Update counters |
| 206 | * yield* Metric.update(requestCounter, 5) // Add 5 requests |
| 207 | * yield* Metric.update(requestCounter, -2) // Subtract 2 (allowed for non-incremental) |
| 208 | * yield* Metric.update(errorCounter, 3) // Add 3 errors |
| 209 | * yield* Metric.update(errorCounter, -1) // Attempt to subtract (ignored for incremental) |
| 210 | * yield* Metric.update(byteCounter, 1024000n) // Add bytes as bigint |
| 211 | * |
| 212 | * // Read counter states |
| 213 | * const requestState: Metric.CounterState<number> = yield* Metric.value( |
| 214 | * requestCounter |
| 215 | * ) |
| 216 | * const errorState: Metric.CounterState<number> = yield* Metric.value( |
| 217 | * errorCounter |
| 218 | * ) |
| 219 | * const byteState: Metric.CounterState<bigint> = yield* Metric.value( |
| 220 | * byteCounter |
| 221 | * ) |
| 222 | * // CounterState contains: |
| 223 | * // - count: current count value (number or bigint based on counter type) |
| 224 | * // - incremental: whether counter only allows increases |
| 225 | * |
| 226 | * return { |
| 227 | * requests: { |
| 228 | * total: requestState.count, // 3 (5 - 2, decrements allowed) |
| 229 | * canDecrease: !requestState.incremental // true |
| 230 | * }, |
| 231 | * errors: { |
| 232 | * total: errorState.count, // 3 (subtract ignored) |
| 233 | * canDecrease: !errorState.incremental // false |
| 234 | * }, |
| 235 | * bytes: { |
| 236 | * total: byteState.count, // 1024000n |
| 237 | * canDecrease: !byteState.incremental // true |
| 238 | * } |
| 239 | * } |
| 240 | * }) |
| 241 | * |
| 242 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 243 | * const counts = [result.requests.total, result.errors.total, result.bytes.total] // => [3, 3, 1024000n] |
| 244 | * ``` |
| 245 | * |
| 246 | * @category models |
| 247 | * @since 4.0.0 |
| 248 | */ |
| 249 | export interface CounterState<in Input extends number | bigint> { |
| 250 | readonly count: Input extends bigint ? bigint : number |
| 251 | readonly incremental: boolean |
| 252 | } |
| 253 | |
| 254 | /** |
| 255 | * A Frequency metric interface that counts occurrences of discrete string values. |
| 256 | * |
| 257 | * **When to use** |
| 258 | * |
| 259 | * Use when frequency metrics are ideal for tracking categorical data where you want to count |
| 260 | * how many times specific string values occur, such as HTTP status codes, user actions, |
| 261 | * error types, or any discrete string-based events. |
| 262 | * |
| 263 | * **Example** (Using frequency metrics) |
| 264 | * |
| 265 | * ```ts import.meta.vitest |
| 266 | * import { Effect, Metric } from "effect" |
| 267 | * |
| 268 | * // Function that accepts any Frequency metric |
| 269 | * const analyzeFrequencyMetric = (freq: Metric.Frequency) => |
| 270 | * Effect.gen(function*() { |
| 271 | * const state = yield* Metric.value(freq) |
| 272 | * |
| 273 | * // Access the frequency state |
| 274 | * const occurrences: ReadonlyMap<string, number> = state.occurrences |
| 275 | * |
| 276 | * // Find most frequent value |
| 277 | * let maxCount = 0 |
| 278 | * let mostFrequent = "" |
| 279 | * for (const [value, count] of occurrences) { |
| 280 | * if (count > maxCount) { |
| 281 | * maxCount = count |
| 282 | * mostFrequent = value |
| 283 | * } |
| 284 | * } |
| 285 | * |
| 286 | * return { mostFrequent, maxCount, totalUniqueValues: occurrences.size } |
| 287 | * }) |
| 288 | * |
| 289 | * const program = Effect.gen(function*() { |
| 290 | * // Create frequency metrics |
| 291 | * const statusCodes: Metric.Frequency = Metric.frequency("http_status", { |
| 292 | * description: "HTTP status code frequency" |
| 293 | * }) |
| 294 | * |
| 295 | * const userActions: Metric.Frequency = Metric.frequency("user_actions", { |
| 296 | * description: "User action frequency" |
| 297 | * }) |
| 298 | * |
| 299 | * // Record some occurrences |
| 300 | * yield* Metric.update(statusCodes, "200") |
| 301 | * yield* Metric.update(statusCodes, "200") |
| 302 | * yield* Metric.update(statusCodes, "404") |
| 303 | * yield* Metric.update(statusCodes, "500") |
| 304 | * yield* Metric.update(statusCodes, "200") |
| 305 | * |
| 306 | * yield* Metric.update(userActions, "login") |
| 307 | * yield* Metric.update(userActions, "view_dashboard") |
| 308 | * yield* Metric.update(userActions, "login") |
| 309 | * |
| 310 | * // Use the function with different frequency metrics |
| 311 | * const statusAnalysis = yield* analyzeFrequencyMetric(statusCodes) |
| 312 | * const actionAnalysis = yield* analyzeFrequencyMetric(userActions) |
| 313 | * return { statusAnalysis, actionAnalysis } |
| 314 | * }) |
| 315 | * |
| 316 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 317 | * const values = [result.statusAnalysis.mostFrequent, result.actionAnalysis.mostFrequent] // => ["200", "login"] |
| 318 | * ``` |
| 319 | * |
| 320 | * @category models |
| 321 | * @since 2.0.0 |
| 322 | */ |
| 323 | export interface Frequency extends Metric<string, FrequencyState> {} |
| 324 | |
| 325 | /** |
| 326 | * State interface for Frequency metrics containing occurrence counts for discrete string values. |
| 327 | * |
| 328 | * **Example** (Reading frequency state) |
| 329 | * |
| 330 | * ```ts import.meta.vitest |
| 331 | * import { Effect, Metric } from "effect" |
| 332 | * |
| 333 | * const program = Effect.gen(function*() { |
| 334 | * // Create frequency metrics for different categories |
| 335 | * const statusCodeFreq = Metric.frequency("http_status_codes", { |
| 336 | * description: "HTTP status code distribution" |
| 337 | * }) |
| 338 | * |
| 339 | * const userActionFreq = Metric.frequency("user_actions", { |
| 340 | * description: "User action frequency" |
| 341 | * }) |
| 342 | * |
| 343 | * // Record occurrences |
| 344 | * yield* Metric.update(statusCodeFreq, "200") // Success |
| 345 | * yield* Metric.update(statusCodeFreq, "200") // Another success |
| 346 | * yield* Metric.update(statusCodeFreq, "404") // Not found |
| 347 | * yield* Metric.update(statusCodeFreq, "500") // Server error |
| 348 | * yield* Metric.update(statusCodeFreq, "200") // Another success |
| 349 | * |
| 350 | * yield* Metric.update(userActionFreq, "login") |
| 351 | * yield* Metric.update(userActionFreq, "click") |
| 352 | * yield* Metric.update(userActionFreq, "login") |
| 353 | * yield* Metric.update(userActionFreq, "scroll") |
| 354 | * yield* Metric.update(userActionFreq, "click") |
| 355 | * yield* Metric.update(userActionFreq, "click") |
| 356 | * |
| 357 | * // Read frequency states |
| 358 | * const statusState: Metric.FrequencyState = yield* Metric.value(statusCodeFreq) |
| 359 | * const actionState: Metric.FrequencyState = yield* Metric.value(userActionFreq) |
| 360 | * |
| 361 | * // FrequencyState contains: |
| 362 | * // - occurrences: ReadonlyMap<string, number> with string values and their counts |
| 363 | * |
| 364 | * // Analyze frequency distributions |
| 365 | * const getMostFrequent = (occurrences: ReadonlyMap<string, number>) => { |
| 366 | * let maxKey = "" |
| 367 | * let maxCount = 0 |
| 368 | * for (const [key, count] of occurrences) { |
| 369 | * if (count > maxCount) { |
| 370 | * maxKey = key |
| 371 | * maxCount = count |
| 372 | * } |
| 373 | * } |
| 374 | * return { key: maxKey, count: maxCount } |
| 375 | * } |
| 376 | * |
| 377 | * const topStatus = getMostFrequent(statusState.occurrences) |
| 378 | * const topAction = getMostFrequent(actionState.occurrences) |
| 379 | * return { |
| 380 | * statusCodes: { |
| 381 | * totalResponses: Array.from(statusState.occurrences.values()).reduce( |
| 382 | * (a, b) => a + b, |
| 383 | * 0 |
| 384 | * ), // 5 |
| 385 | * mostCommon: topStatus, // { key: "200", count: 3 } |
| 386 | * uniqueCodes: statusState.occurrences.size // 3 |
| 387 | * }, |
| 388 | * userActions: { |
| 389 | * totalActions: Array.from(actionState.occurrences.values()).reduce( |
| 390 | * (a, b) => a + b, |
| 391 | * 0 |
| 392 | * ), // 6 |
| 393 | * mostCommon: topAction, // { key: "click", count: 3 } |
| 394 | * uniqueActions: actionState.occurrences.size // 3 |
| 395 | * } |
| 396 | * } |
| 397 | * }) |
| 398 | * |
| 399 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 400 | * const mostCommon = [result.statusCodes.mostCommon, result.userActions.mostCommon] |
| 401 | * mostCommon.map(({ key, count }) => [key, count]) // => [["200", 3], ["click", 3]] |
| 402 | * ``` |
| 403 | * |
| 404 | * @category models |
| 405 | * @since 4.0.0 |
| 406 | */ |
| 407 | export interface FrequencyState { |
| 408 | readonly occurrences: ReadonlyMap<string, number> |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * A Gauge metric that tracks instantaneous values that can go up or down. |
| 413 | * |
| 414 | * **When to use** |
| 415 | * |
| 416 | * Use when gauges are useful for tracking current state values like memory usage, CPU load, |
| 417 | * active connections, queue sizes, or any value that represents a current level. |
| 418 | * |
| 419 | * **Example** (Using gauge metrics) |
| 420 | * |
| 421 | * ```ts import.meta.vitest |
| 422 | * import { Effect, Metric } from "effect" |
| 423 | * |
| 424 | * const program = Effect.gen(function*() { |
| 425 | * // Create different types of gauges |
| 426 | * const memoryGauge: Metric.Gauge<number> = Metric.gauge("memory_usage_mb", { |
| 427 | * description: "Current memory usage in megabytes" |
| 428 | * }) |
| 429 | * |
| 430 | * const diskSpaceGauge: Metric.Gauge<bigint> = Metric.gauge("disk_free_bytes", { |
| 431 | * description: "Available disk space in bytes", |
| 432 | * bigint: true, |
| 433 | * attributes: { mount: "/var" } |
| 434 | * }) |
| 435 | * |
| 436 | * // Set gauge values (absolute values) |
| 437 | * yield* Metric.update(memoryGauge, 512) // Set to 512 MB |
| 438 | * yield* Metric.update(memoryGauge, 640) // Set to 640 MB (replaces 512) |
| 439 | * yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to ~5GB free |
| 440 | * |
| 441 | * // Modify gauge values (relative changes) |
| 442 | * yield* Metric.modify(memoryGauge, 128) // Add 128 MB (total: 768) |
| 443 | * yield* Metric.modify(memoryGauge, -64) // Subtract 64 MB (total: 704) |
| 444 | * |
| 445 | * // Read gauge state |
| 446 | * const memoryState: Metric.GaugeState<number> = yield* Metric.value( |
| 447 | * memoryGauge |
| 448 | * ) |
| 449 | * const diskState: Metric.GaugeState<bigint> = yield* Metric.value( |
| 450 | * diskSpaceGauge |
| 451 | * ) |
| 452 | * // Gauge state contains: |
| 453 | * // - value: current instantaneous value |
| 454 | * |
| 455 | * return { |
| 456 | * memory: { currentValue: memoryState.value }, // 704 |
| 457 | * disk: { currentValue: diskState.value } // 5000000000n |
| 458 | * } |
| 459 | * }) |
| 460 | * |
| 461 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 462 | * const values = [result.memory.currentValue, result.disk.currentValue] // => [704, 5000000000n] |
| 463 | * ``` |
| 464 | * |
| 465 | * @category models |
| 466 | * @since 2.0.0 |
| 467 | */ |
| 468 | export interface Gauge<in Input extends number | bigint> extends Metric<Input, GaugeState<Input>> {} |
| 469 | |
| 470 | /** |
| 471 | * State interface for Gauge metrics containing the current instantaneous value. |
| 472 | * |
| 473 | * **Example** (Reading gauge state) |
| 474 | * |
| 475 | * ```ts import.meta.vitest |
| 476 | * import { Effect, Metric } from "effect" |
| 477 | * |
| 478 | * const program = Effect.gen(function*() { |
| 479 | * // Create different types of gauges |
| 480 | * const temperatureGauge = Metric.gauge("room_temperature_celsius", { |
| 481 | * description: "Current room temperature" |
| 482 | * }) |
| 483 | * |
| 484 | * const diskSpaceGauge = Metric.gauge("disk_usage_bytes", { |
| 485 | * description: "Current disk usage", |
| 486 | * bigint: true |
| 487 | * }) |
| 488 | * |
| 489 | * const queueSizeGauge = Metric.gauge("queue_size", { |
| 490 | * description: "Current queue size" |
| 491 | * }) |
| 492 | * |
| 493 | * // Set gauge values (absolute values) |
| 494 | * yield* Metric.update(temperatureGauge, 22.5) // Set to 22.5°C |
| 495 | * yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to 5GB usage |
| 496 | * yield* Metric.update(queueSizeGauge, 10) // Set to 10 items |
| 497 | * |
| 498 | * // Update gauge values (new absolute values) |
| 499 | * yield* Metric.update(temperatureGauge, 23.1) // Temperature changed |
| 500 | * yield* Metric.update(queueSizeGauge, 15) // Queue grew |
| 501 | * |
| 502 | * // Read gauge states |
| 503 | * const tempState: Metric.GaugeState<number> = yield* Metric.value( |
| 504 | * temperatureGauge |
| 505 | * ) |
| 506 | * const diskState: Metric.GaugeState<bigint> = yield* Metric.value( |
| 507 | * diskSpaceGauge |
| 508 | * ) |
| 509 | * const queueState: Metric.GaugeState<number> = yield* Metric.value( |
| 510 | * queueSizeGauge |
| 511 | * ) |
| 512 | * // GaugeState contains: |
| 513 | * // - value: current instantaneous value (number or bigint based on gauge type) |
| 514 | * |
| 515 | * return { |
| 516 | * environment: { |
| 517 | * temperature: tempState.value, // 23.1 |
| 518 | * temperatureUnit: "°C" |
| 519 | * }, |
| 520 | * system: { |
| 521 | * diskUsage: diskState.value, // 5000000000n |
| 522 | * diskUsageGB: Number(diskState.value) / 1_000_000_000, // 5 |
| 523 | * queueSize: queueState.value // 15 |
| 524 | * } |
| 525 | * } |
| 526 | * }) |
| 527 | * |
| 528 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 529 | * const values = [result.environment.temperature, result.system.diskUsage, result.system.queueSize] |
| 530 | * values // => [23.1, 5000000000n, 15] |
| 531 | * ``` |
| 532 | * |
| 533 | * @category models |
| 534 | * @since 4.0.0 |
| 535 | */ |
| 536 | export interface GaugeState<in Input extends number | bigint> { |
| 537 | readonly value: Input extends bigint ? bigint : number |
| 538 | } |
| 539 | |
| 540 | /** |
| 541 | * A Histogram metric that records observations in configurable buckets to analyze value distributions. |
| 542 | * |
| 543 | * **When to use** |
| 544 | * |
| 545 | * Use when histograms are ideal for measuring request durations, response sizes, and other continuous values |
| 546 | * where you need to understand the distribution of values rather than just aggregates. |
| 547 | * |
| 548 | * **Example** (Using histogram metrics) |
| 549 | * |
| 550 | * ```ts import.meta.vitest |
| 551 | * import { Effect, Metric } from "effect" |
| 552 | * |
| 553 | * const program = Effect.gen(function*() { |
| 554 | * // Create histograms with different boundary strategies |
| 555 | * const responseTimeHistogram: Metric.Histogram<number> = Metric.histogram( |
| 556 | * "http_response_time_ms", |
| 557 | * { |
| 558 | * description: "HTTP response time distribution in milliseconds", |
| 559 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) // 50, 100, ..., 900, Infinity |
| 560 | * } |
| 561 | * ) |
| 562 | * |
| 563 | * const fileSizeHistogram: Metric.Histogram<number> = Metric.histogram( |
| 564 | * "file_size_bytes", |
| 565 | * { |
| 566 | * description: "File size distribution in bytes", |
| 567 | * boundaries: Metric.exponentialBoundaries({ |
| 568 | * start: 1, |
| 569 | * factor: 2, |
| 570 | * count: 10 |
| 571 | * }) // 1, 2, 4, 8, ..., 512 |
| 572 | * } |
| 573 | * ) |
| 574 | * |
| 575 | * // Record observations (values get placed into appropriate buckets) |
| 576 | * yield* Metric.update(responseTimeHistogram, 125) // Goes into 100-150ms bucket |
| 577 | * yield* Metric.update(responseTimeHistogram, 75) // Goes into 50-100ms bucket |
| 578 | * yield* Metric.update(responseTimeHistogram, 200) // Goes into 150-200ms bucket |
| 579 | * yield* Metric.update(responseTimeHistogram, 45) // Goes into 0-50ms bucket |
| 580 | * |
| 581 | * yield* Metric.update(fileSizeHistogram, 3) // Goes into 2-4 bytes bucket |
| 582 | * yield* Metric.update(fileSizeHistogram, 15) // Goes into 8-16 bytes bucket |
| 583 | * yield* Metric.update(fileSizeHistogram, 100) // Goes into 64-128 bytes bucket |
| 584 | * |
| 585 | * // Read histogram state |
| 586 | * const responseTimeState: Metric.HistogramState = yield* Metric.value( |
| 587 | * responseTimeHistogram |
| 588 | * ) |
| 589 | * const fileSizeState: Metric.HistogramState = yield* Metric.value( |
| 590 | * fileSizeHistogram |
| 591 | * ) |
| 592 | * // Histogram state contains: |
| 593 | * // - buckets: Array of [boundary, cumulativeCount] pairs |
| 594 | * // - count: total number of observations |
| 595 | * // - min: smallest observed value |
| 596 | * // - max: largest observed value |
| 597 | * // - sum: sum of all observed values |
| 598 | * |
| 599 | * return { |
| 600 | * responseTime: { |
| 601 | * totalRequests: responseTimeState.count, // 4 |
| 602 | * fastestRequest: responseTimeState.min, // 45 |
| 603 | * slowestRequest: responseTimeState.max, // 200 |
| 604 | * totalTime: responseTimeState.sum, // 445 |
| 605 | * averageTime: responseTimeState.sum / responseTimeState.count // 111.25 |
| 606 | * }, |
| 607 | * fileSize: { |
| 608 | * totalFiles: fileSizeState.count, // 3 |
| 609 | * smallestFile: fileSizeState.min, // 3 |
| 610 | * largestFile: fileSizeState.max, // 100 |
| 611 | * totalBytes: fileSizeState.sum // 118 |
| 612 | * } |
| 613 | * } |
| 614 | * }) |
| 615 | * |
| 616 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 617 | * const values = [result.responseTime.totalRequests, result.responseTime.totalTime, result.fileSize.totalBytes] |
| 618 | * values // => [4, 445, 118] |
| 619 | * ``` |
| 620 | * |
| 621 | * @category models |
| 622 | * @since 2.0.0 |
| 623 | */ |
| 624 | export interface Histogram<Input> extends Metric<Input, HistogramState> {} |
| 625 | |
| 626 | /** |
| 627 | * State interface for Histogram metrics containing bucket distributions and aggregate statistics. |
| 628 | * |
| 629 | * **Example** (Reading histogram state) |
| 630 | * |
| 631 | * ```ts import.meta.vitest |
| 632 | * import { Effect, Metric } from "effect" |
| 633 | * |
| 634 | * const program = Effect.gen(function*() { |
| 635 | * // Create histogram with linear boundaries |
| 636 | * const responseTimeHistogram = Metric.histogram("api_response_time_ms", { |
| 637 | * description: "API response time distribution", |
| 638 | * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) // 100, 200, ..., 800, Infinity |
| 639 | * }) |
| 640 | * |
| 641 | * // Record observations |
| 642 | * yield* Metric.update(responseTimeHistogram, 50) // Fast response |
| 643 | * yield* Metric.update(responseTimeHistogram, 150) // Average response |
| 644 | * yield* Metric.update(responseTimeHistogram, 750) // Slow response |
| 645 | * yield* Metric.update(responseTimeHistogram, 250) // Average response |
| 646 | * yield* Metric.update(responseTimeHistogram, 95) // Fast response |
| 647 | * |
| 648 | * // Read histogram state |
| 649 | * const state: Metric.HistogramState = yield* Metric.value( |
| 650 | * responseTimeHistogram |
| 651 | * ) |
| 652 | * |
| 653 | * // HistogramState contains: |
| 654 | * // - buckets: Array of [boundary, cumulativeCount] pairs showing distribution |
| 655 | * // - count: total number of observations |
| 656 | * // - min: smallest observed value |
| 657 | * // - max: largest observed value |
| 658 | * // - sum: sum of all observed values |
| 659 | * |
| 660 | * // Analyze bucket distribution |
| 661 | * const analyzeBuckets = (buckets: ReadonlyArray<[number, number]>) => { |
| 662 | * const analysis: Array< |
| 663 | * { range: string; count: number; percentage: number } |
| 664 | * > = [] |
| 665 | * let previousCount = 0 |
| 666 | * const totalCount = buckets[buckets.length - 1]?.[1] ?? 0 |
| 667 | * |
| 668 | * for (let i = 0; i < buckets.length; i++) { |
| 669 | * const [boundary, cumulativeCount] = buckets[i] |
| 670 | * const bucketCount = cumulativeCount - previousCount |
| 671 | * const percentage = totalCount > 0 ? (bucketCount / totalCount) * 100 : 0 |
| 672 | * const prevBoundary = i === 0 ? 0 : buckets[i - 1][0] |
| 673 | * |
| 674 | * analysis.push({ |
| 675 | * range: `${prevBoundary}-${boundary}ms`, |
| 676 | * count: bucketCount, |
| 677 | * percentage: Math.round(percentage * 10) / 10 |
| 678 | * }) |
| 679 | * previousCount = cumulativeCount |
| 680 | * } |
| 681 | * return analysis |
| 682 | * } |
| 683 | * |
| 684 | * const bucketAnalysis = analyzeBuckets(state.buckets) |
| 685 | * return { |
| 686 | * responseTime: { |
| 687 | * totalRequests: state.count, // 5 |
| 688 | * fastestResponse: state.min, // 50 |
| 689 | * slowestResponse: state.max, // 750 |
| 690 | * averageResponse: state.sum / state.count, // 268 |
| 691 | * totalTime: state.sum, // 1340 |
| 692 | * distribution: bucketAnalysis |
| 693 | * // Example distribution: |
| 694 | * // [{ range: "0-100ms", count: 2, percentage: 40.0 }, |
| 695 | * // { range: "100-200ms", count: 1, percentage: 20.0 }, |
| 696 | * // { range: "200-300ms", count: 1, percentage: 20.0 }, |
| 697 | * // { range: "700-800ms", count: 1, percentage: 20.0 }] |
| 698 | * } |
| 699 | * } |
| 700 | * }) |
| 701 | * |
| 702 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 703 | * const stats = result.responseTime |
| 704 | * const values = [stats.totalRequests, stats.fastestResponse, stats.slowestResponse, stats.totalTime] |
| 705 | * values // => [5, 50, 750, 1295] |
| 706 | * ``` |
| 707 | * |
| 708 | * @category models |
| 709 | * @since 4.0.0 |
| 710 | */ |
| 711 | export interface HistogramState { |
| 712 | readonly buckets: ReadonlyArray<[number, number]> |
| 713 | readonly count: number |
| 714 | readonly min: number |
| 715 | readonly max: number |
| 716 | readonly sum: number |
| 717 | } |
| 718 | |
| 719 | /** |
| 720 | * A Summary metric that calculates quantiles over a sliding time window of observations. |
| 721 | * |
| 722 | * **When to use** |
| 723 | * |
| 724 | * Use when summaries provide statistical insights into value distributions by tracking specific quantiles |
| 725 | * (percentiles) such as median (50th), 95th percentile, 99th percentile, etc. They're ideal for |
| 726 | * understanding performance characteristics like response time distributions. |
| 727 | * |
| 728 | * **Example** (Using summary metrics) |
| 729 | * |
| 730 | * ```ts import.meta.vitest |
| 731 | * import { Effect, Metric } from "effect" |
| 732 | * |
| 733 | * const program = Effect.gen(function*() { |
| 734 | * // Create summaries with different quantile configurations |
| 735 | * const responseTimeSummary: Metric.Summary<number> = Metric.summary( |
| 736 | * "api_response_time_ms", |
| 737 | * { |
| 738 | * description: "API response time distribution in milliseconds", |
| 739 | * maxAge: "5 minutes", // Keep observations for 5 minutes |
| 740 | * maxSize: 1000, // Keep up to 1000 observations |
| 741 | * quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles |
| 742 | * } |
| 743 | * ) |
| 744 | * |
| 745 | * const requestSizeSummary: Metric.Summary<number> = Metric.summary( |
| 746 | * "request_size_bytes", |
| 747 | * { |
| 748 | * description: "Request payload size distribution", |
| 749 | * maxAge: "10 minutes", |
| 750 | * maxSize: 500, |
| 751 | * quantiles: [0.25, 0.5, 0.75, 0.9] // Track quartiles and 90th percentile |
| 752 | * } |
| 753 | * ) |
| 754 | * |
| 755 | * // Record observations (values are stored in time-based sliding window) |
| 756 | * yield* Metric.update(responseTimeSummary, 120) // Fast response |
| 757 | * yield* Metric.update(responseTimeSummary, 250) // Average response |
| 758 | * yield* Metric.update(responseTimeSummary, 45) // Very fast response |
| 759 | * yield* Metric.update(responseTimeSummary, 890) // Slow response |
| 760 | * yield* Metric.update(responseTimeSummary, 156) // Average response |
| 761 | * |
| 762 | * yield* Metric.update(requestSizeSummary, 1024) // 1KB request |
| 763 | * yield* Metric.update(requestSizeSummary, 512) // 512B request |
| 764 | * yield* Metric.update(requestSizeSummary, 2048) // 2KB request |
| 765 | * |
| 766 | * // Read summary state |
| 767 | * const responseTimeState: Metric.SummaryState = yield* Metric.value( |
| 768 | * responseTimeSummary |
| 769 | * ) |
| 770 | * const requestSizeState: Metric.SummaryState = yield* Metric.value( |
| 771 | * requestSizeSummary |
| 772 | * ) |
| 773 | * |
| 774 | * // Summary state contains: |
| 775 | * // - quantiles: Array of [quantile, optionalValue] pairs |
| 776 | * // - count: total number of observations in window |
| 777 | * // - min: smallest observed value in window |
| 778 | * // - max: largest observed value in window |
| 779 | * // - sum: sum of all observed values in window |
| 780 | * |
| 781 | * // Extract quantile values safely |
| 782 | * const getQuantileValue = ( |
| 783 | * quantiles: ReadonlyArray<readonly [number, number | undefined]>, |
| 784 | * q: number |
| 785 | * ) => quantiles.find(([quantile]) => quantile === q)?.[1] |
| 786 | * |
| 787 | * const median = getQuantileValue(responseTimeState.quantiles, 0.5) |
| 788 | * const p95 = getQuantileValue(responseTimeState.quantiles, 0.95) |
| 789 | * const p99 = getQuantileValue(responseTimeState.quantiles, 0.99) |
| 790 | * return { |
| 791 | * responseTime: { |
| 792 | * totalRequests: responseTimeState.count, // 5 |
| 793 | * fastestResponse: responseTimeState.min, // 45 |
| 794 | * slowestResponse: responseTimeState.max, // 890 |
| 795 | * totalTime: responseTimeState.sum, // 1461 |
| 796 | * averageTime: responseTimeState.sum / responseTimeState.count, // 292.2 |
| 797 | * medianTime: median ?? null, // ~156 |
| 798 | * p95Time: p95 ?? null, // ~890 |
| 799 | * p99Time: p99 ?? null // ~890 |
| 800 | * }, |
| 801 | * requestSize: { |
| 802 | * totalRequests: requestSizeState.count, // 3 |
| 803 | * averageSize: requestSizeState.sum / requestSizeState.count // ~1194.7 |
| 804 | * } |
| 805 | * } |
| 806 | * }) |
| 807 | * |
| 808 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 809 | * const counts = [result.responseTime.totalRequests, result.responseTime.totalTime, result.requestSize.totalRequests] |
| 810 | * counts // => [5, 1461, 3] |
| 811 | * ``` |
| 812 | * |
| 813 | * @category models |
| 814 | * @since 2.0.0 |
| 815 | */ |
| 816 | export interface Summary<Input> extends Metric<Input, SummaryState> {} |
| 817 | |
| 818 | /** |
| 819 | * State interface for Summary metrics containing quantile calculations and aggregate statistics. |
| 820 | * |
| 821 | * **Example** (Reading summary state) |
| 822 | * |
| 823 | * ```ts import.meta.vitest |
| 824 | * import { Effect, Metric } from "effect" |
| 825 | * |
| 826 | * const program = Effect.gen(function*() { |
| 827 | * // Create summary with specific quantiles |
| 828 | * const responseTimeSummary = Metric.summary("api_response_latency", { |
| 829 | * description: "API response time distribution with quantiles", |
| 830 | * maxAge: "5 minutes", |
| 831 | * maxSize: 1000, |
| 832 | * quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles |
| 833 | * }) |
| 834 | * |
| 835 | * // Record observations over time |
| 836 | * yield* Metric.update(responseTimeSummary, 120) // Fast response |
| 837 | * yield* Metric.update(responseTimeSummary, 250) // Average response |
| 838 | * yield* Metric.update(responseTimeSummary, 45) // Very fast response |
| 839 | * yield* Metric.update(responseTimeSummary, 890) // Slow response |
| 840 | * yield* Metric.update(responseTimeSummary, 156) // Average response |
| 841 | * yield* Metric.update(responseTimeSummary, 78) // Fast response |
| 842 | * yield* Metric.update(responseTimeSummary, 340) // Slower response |
| 843 | * |
| 844 | * // Read summary state |
| 845 | * const state: Metric.SummaryState = yield* Metric.value(responseTimeSummary) |
| 846 | * |
| 847 | * // SummaryState contains: |
| 848 | * // - quantiles: Array of [quantile, optionalValue] pairs showing percentile values |
| 849 | * // - count: total number of observations in current window |
| 850 | * // - min: smallest observed value in window |
| 851 | * // - max: largest observed value in window |
| 852 | * // - sum: sum of all observed values in window |
| 853 | * |
| 854 | * // Extract quantile information safely |
| 855 | * const extractQuantiles = ( |
| 856 | * quantiles: ReadonlyArray<readonly [number, number | undefined]> |
| 857 | * ) => { |
| 858 | * const result: Record<string, number | null> = {} |
| 859 | * for (const [quantile, valueOption] of quantiles) { |
| 860 | * const percentile = Math.round(quantile * 100) |
| 861 | * result[`p${percentile}`] = valueOption ?? null |
| 862 | * } |
| 863 | * return result |
| 864 | * } |
| 865 | * |
| 866 | * const quantileValues = extractQuantiles(state.quantiles) |
| 867 | * return { |
| 868 | * latencyAnalysis: { |
| 869 | * totalRequests: state.count, // 7 |
| 870 | * fastestResponse: state.min, // 45 |
| 871 | * slowestResponse: state.max, // 890 |
| 872 | * averageResponse: state.sum / state.count, // ~268.4 |
| 873 | * totalLatency: state.sum, // 1879 |
| 874 | * percentiles: quantileValues, |
| 875 | * // Example percentiles: |
| 876 | * // { p50: 156, p95: 890, p99: 890 } |
| 877 | * performance: { |
| 878 | * fast: quantileValues.p50 !== null && quantileValues.p50 < 200 |
| 879 | * ? "Good" |
| 880 | * : "Needs improvement", |
| 881 | * reliability: quantileValues.p95 !== null && quantileValues.p95 < 500 |
| 882 | * ? "Reliable" |
| 883 | * : "Concerning" |
| 884 | * } |
| 885 | * } |
| 886 | * } |
| 887 | * }) |
| 888 | * |
| 889 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 890 | * const analysis = result.latencyAnalysis |
| 891 | * const values = [analysis.totalRequests, analysis.fastestResponse, analysis.slowestResponse, analysis.totalLatency] |
| 892 | * values // => [7, 45, 890, 1879] |
| 893 | * ``` |
| 894 | * |
| 895 | * @category models |
| 896 | * @since 4.0.0 |
| 897 | */ |
| 898 | export interface SummaryState { |
| 899 | readonly quantiles: ReadonlyArray<readonly [number, number | undefined]> |
| 900 | readonly count: number |
| 901 | readonly min: number |
| 902 | readonly max: number |
| 903 | readonly sum: number |
| 904 | } |
| 905 | |
| 906 | /** |
| 907 | * The `Metric` namespace provides a comprehensive system for collecting, aggregating, and observing |
| 908 | * application metrics in Effect applications. |
| 909 | * |
| 910 | * **Example** (Collecting application metrics) |
| 911 | * |
| 912 | * ```ts import.meta.vitest |
| 913 | * import { Effect, Metric } from "effect" |
| 914 | * |
| 915 | * const program = Effect.gen(function*() { |
| 916 | * // Create different types of metrics |
| 917 | * const requestCounter = Metric.counter("http_requests_total") |
| 918 | * const responseTimeHistogram = Metric.histogram("http_response_time", { |
| 919 | * boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 10 }) |
| 920 | * }) |
| 921 | * const activeConnectionsGauge = Metric.gauge("active_connections") |
| 922 | * const statusFrequency = Metric.frequency("http_status_codes") |
| 923 | * |
| 924 | * // Update metrics |
| 925 | * yield* Metric.update(requestCounter, 1) |
| 926 | * yield* Metric.update(responseTimeHistogram, 45.2) |
| 927 | * yield* Metric.update(activeConnectionsGauge, 12) |
| 928 | * yield* Metric.update(statusFrequency, "200") |
| 929 | * |
| 930 | * // Get metric values |
| 931 | * const counterValue = yield* Metric.value(requestCounter) |
| 932 | * const histogramValue = yield* Metric.value(responseTimeHistogram) |
| 933 | * const gaugeValue = yield* Metric.value(activeConnectionsGauge) |
| 934 | * const frequencyValue = yield* Metric.value(statusFrequency) |
| 935 | * |
| 936 | * return { |
| 937 | * counter: counterValue, |
| 938 | * histogram: histogramValue, |
| 939 | * gauge: gaugeValue, |
| 940 | * frequency: frequencyValue |
| 941 | * } |
| 942 | * }) |
| 943 | * |
| 944 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 945 | * const values = [result.counter.count, result.gauge.value] // => [1, 12] |
| 946 | * ``` |
| 947 | * |
| 948 | * @since 2.0.0 |
| 949 | */ |
| 950 | export declare namespace Metric { |
| 951 | /** |
| 952 | * Union type representing all available metric types in the Effect metrics system. |
| 953 | * |
| 954 | * **Example** (Inspecting metric types) |
| 955 | * |
| 956 | * ```ts import.meta.vitest |
| 957 | * import { Metric } from "effect" |
| 958 | * |
| 959 | * const metrics: ReadonlyArray<Metric.Metric<any, any>> = [ |
| 960 | * Metric.counter("requests_total"), |
| 961 | * Metric.gauge("cpu_usage"), |
| 962 | * Metric.frequency("status_codes"), |
| 963 | * Metric.histogram("response_time", { |
| 964 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) |
| 965 | * }), |
| 966 | * Metric.summary("latency", { |
| 967 | * maxAge: "5 minutes", |
| 968 | * maxSize: 1000, |
| 969 | * quantiles: [0.5, 0.95, 0.99] |
| 970 | * }) |
| 971 | * ] |
| 972 | * |
| 973 | * const types: ReadonlyArray<Metric.Metric.Type> = metrics.map((metric) => metric.type) |
| 974 | * const actual = types // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] |
| 975 | * ``` |
| 976 | * |
| 977 | * @category models |
| 978 | * @since 4.0.0 |
| 979 | */ |
| 980 | export type Type = "Counter" | "Frequency" | "Gauge" | "Histogram" | "Summary" |
| 981 | |
| 982 | /** |
| 983 | * Union type for metric attributes that can be provided as either an object or array of tuples. |
| 984 | * |
| 985 | * **Example** (Providing attributes in different formats) |
| 986 | * |
| 987 | * ```ts import.meta.vitest |
| 988 | * import { Effect, Metric } from "effect" |
| 989 | * |
| 990 | * const program = Effect.gen(function*() { |
| 991 | * // Different ways to specify attributes |
| 992 | * const attributesAsObject = { |
| 993 | * service: "api", |
| 994 | * environment: "production", |
| 995 | * version: "1.2.3" |
| 996 | * } |
| 997 | * |
| 998 | * const attributesAsArray: ReadonlyArray<[string, string]> = [ |
| 999 | * ["service", "api"], |
| 1000 | * ["environment", "production"], |
| 1001 | * ["version", "1.2.3"] |
| 1002 | * ] |
| 1003 | * |
| 1004 | * // Create metrics with different attribute formats |
| 1005 | * const requestCounter1 = Metric.counter("requests", { |
| 1006 | * description: "Total requests", |
| 1007 | * attributes: attributesAsObject // Using object format |
| 1008 | * }) |
| 1009 | * |
| 1010 | * const requestCounter2 = Metric.counter("requests", { |
| 1011 | * description: "Total requests", |
| 1012 | * attributes: attributesAsArray // Using array format |
| 1013 | * }) |
| 1014 | * |
| 1015 | * // Function to normalize attributes to object format |
| 1016 | * const normalizeAttributes = ( |
| 1017 | * attrs: typeof attributesAsObject | ReadonlyArray<[string, string]> |
| 1018 | * ) => { |
| 1019 | * if (Array.isArray(attrs)) { |
| 1020 | * return Object.fromEntries(attrs) |
| 1021 | * } |
| 1022 | * return attrs |
| 1023 | * } |
| 1024 | * |
| 1025 | * // Add runtime attributes using withAttributes |
| 1026 | * const contextualCounter = Metric.withAttributes(requestCounter1, { |
| 1027 | * method: "GET", |
| 1028 | * endpoint: "/api/users" |
| 1029 | * }) |
| 1030 | * |
| 1031 | * // Update metrics with different attribute combinations |
| 1032 | * yield* Metric.update(contextualCounter, 1) |
| 1033 | * |
| 1034 | * // Both formats result in the same internal representation |
| 1035 | * const normalizedObject = normalizeAttributes(attributesAsObject) |
| 1036 | * const normalizedArray = normalizeAttributes(attributesAsArray) |
| 1037 | * return { |
| 1038 | * attributeFormats: { |
| 1039 | * object: normalizedObject, // { service: "api", environment: "production", version: "1.2.3" } |
| 1040 | * array: normalizedArray // { service: "api", environment: "production", version: "1.2.3" } |
| 1041 | * } |
| 1042 | * } |
| 1043 | * }) |
| 1044 | * |
| 1045 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1046 | * const objectAttributes = result.attributeFormats.object |
| 1047 | * const arrayAttributes = result.attributeFormats.array |
| 1048 | * const sameAttributes = [objectAttributes, arrayAttributes] |
| 1049 | * sameAttributes // => [{ service: "api", environment: "production", version: "1.2.3" }, { service: "api", environment: "production", version: "1.2.3" }] |
| 1050 | * ``` |
| 1051 | * |
| 1052 | * @category models |
| 1053 | * @since 4.0.0 |
| 1054 | */ |
| 1055 | export type Attributes = AttributeSet | ReadonlyArray<[string, string]> |
| 1056 | |
| 1057 | /** |
| 1058 | * Type for metric attributes as a readonly record of string key-value pairs. |
| 1059 | * |
| 1060 | * **Example** (Combining metric attribute sets) |
| 1061 | * |
| 1062 | * ```ts import.meta.vitest |
| 1063 | * import { Effect, Metric } from "effect" |
| 1064 | * |
| 1065 | * const program = Effect.gen(function*() { |
| 1066 | * // Define attribute sets for different contexts |
| 1067 | * const serviceAttributes = { |
| 1068 | * service: "user-api", |
| 1069 | * version: "2.1.0", |
| 1070 | * environment: "production" |
| 1071 | * } |
| 1072 | * |
| 1073 | * const operationAttributes = { |
| 1074 | * operation: "create_user", |
| 1075 | * method: "POST", |
| 1076 | * endpoint: "/api/users" |
| 1077 | * } |
| 1078 | * |
| 1079 | * const infrastructureAttributes = { |
| 1080 | * region: "us-east-1", |
| 1081 | * datacenter: "dc1", |
| 1082 | * host: "api-server-01" |
| 1083 | * } |
| 1084 | * |
| 1085 | * // Create metrics with predefined attribute sets |
| 1086 | * const requestCounter = Metric.counter("http_requests_total", { |
| 1087 | * description: "Total HTTP requests", |
| 1088 | * attributes: serviceAttributes |
| 1089 | * }) |
| 1090 | * |
| 1091 | * // Combine attribute sets |
| 1092 | * const combineAttributes = (...attributeSets: Array<Record<string, string>>) => |
| 1093 | * Object.assign({}, ...attributeSets) |
| 1094 | * |
| 1095 | * const fullAttributes = combineAttributes( |
| 1096 | * serviceAttributes, |
| 1097 | * operationAttributes, |
| 1098 | * infrastructureAttributes |
| 1099 | * ) |
| 1100 | * |
| 1101 | * // Create metric with combined attributes |
| 1102 | * const detailedCounter = Metric.withAttributes(requestCounter, fullAttributes) |
| 1103 | * |
| 1104 | * // Helper to validate attribute keys (all must be strings) |
| 1105 | * const validateAttributeSet = (attrs: Record<string, string>): boolean => { |
| 1106 | * return Object.entries(attrs).every(([key, value]) => |
| 1107 | * typeof key === "string" && typeof value === "string" |
| 1108 | * ) |
| 1109 | * } |
| 1110 | * |
| 1111 | * yield* Metric.update(detailedCounter, 1) |
| 1112 | * |
| 1113 | * return { |
| 1114 | * attributes: { |
| 1115 | * service: serviceAttributes, |
| 1116 | * operation: operationAttributes, |
| 1117 | * infrastructure: infrastructureAttributes, |
| 1118 | * combined: fullAttributes, |
| 1119 | * isValid: validateAttributeSet(fullAttributes), // true |
| 1120 | * totalKeys: Object.keys(fullAttributes).length // 9 |
| 1121 | * } |
| 1122 | * } |
| 1123 | * }) |
| 1124 | * |
| 1125 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1126 | * const validation = [result.attributes.isValid, result.attributes.totalKeys] // => [true, 9] |
| 1127 | * ``` |
| 1128 | * |
| 1129 | * @category models |
| 1130 | * @since 4.0.0 |
| 1131 | */ |
| 1132 | export type AttributeSet = Readonly<Record<string, string>> |
| 1133 | |
| 1134 | /** |
| 1135 | * Utility type to extract the Input type from a Metric type. |
| 1136 | * |
| 1137 | * **Example** (Extracting metric input types) |
| 1138 | * |
| 1139 | * ```ts import.meta.vitest |
| 1140 | * import { Metric } from "effect" |
| 1141 | * |
| 1142 | * // Create various metric types |
| 1143 | * const numberCounter = Metric.counter("requests") |
| 1144 | * const bigintCounter = Metric.counter("bytes", { bigint: true }) |
| 1145 | * const stringFrequency = Metric.frequency("status_codes") |
| 1146 | * const numberGauge = Metric.gauge("cpu_usage") |
| 1147 | * const numberHistogram = Metric.histogram("response_time", { |
| 1148 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) |
| 1149 | * }) |
| 1150 | * |
| 1151 | * // The Input utility type extracts the input type from metric types: |
| 1152 | * // - Counter<number>: number |
| 1153 | * // - Counter<bigint>: bigint |
| 1154 | * // - Frequency: string |
| 1155 | * // - Gauge<number>: number |
| 1156 | * // - Histogram<number>: number |
| 1157 | * |
| 1158 | * // Helper function that works with any metric |
| 1159 | * const createMetricInfo = (metric: Metric.Metric<any, any>) => ({ |
| 1160 | * id: metric.id, |
| 1161 | * type: metric.type |
| 1162 | * }) |
| 1163 | * |
| 1164 | * const metrics = [ |
| 1165 | * createMetricInfo(numberCounter), // { id: "requests", type: "Counter" } |
| 1166 | * createMetricInfo(bigintCounter), // { id: "bytes", type: "Counter" } |
| 1167 | * createMetricInfo(stringFrequency), // { id: "status_codes", type: "Frequency" } |
| 1168 | * createMetricInfo(numberGauge), // { id: "cpu_usage", type: "Gauge" } |
| 1169 | * createMetricInfo(numberHistogram) // { id: "response_time", type: "Histogram" } |
| 1170 | * ] |
| 1171 | * |
| 1172 | * // Type safety is enforced at compile time: |
| 1173 | * // Metric.update(numberCounter, 123) // ✓ Valid (number) |
| 1174 | * // Metric.update(numberCounter, "abc") // ✗ Type error |
| 1175 | * // Metric.update(stringFrequency, "ok") // ✓ Valid (string) |
| 1176 | * // Metric.update(stringFrequency, 404) // ✗ Type error |
| 1177 | * const metricIds = metrics.map(({ id, type }) => `${id}:${type}`) |
| 1178 | * metricIds // => ["requests:Counter", "bytes:Counter", "status_codes:Frequency", "cpu_usage:Gauge", "response_time:Histogram"] |
| 1179 | * ``` |
| 1180 | * |
| 1181 | * @category utility types |
| 1182 | * @since 4.0.0 |
| 1183 | */ |
| 1184 | export type Input<A> = A extends Metric<infer _Input, infer _State> ? _Input |
| 1185 | : never |
| 1186 | |
| 1187 | /** |
| 1188 | * Utility type to extract the State type from a Metric type. |
| 1189 | * |
| 1190 | * **Example** (Extracting metric state types) |
| 1191 | * |
| 1192 | * ```ts import.meta.vitest |
| 1193 | * import { Effect, Metric } from "effect" |
| 1194 | * |
| 1195 | * // Create various metric types |
| 1196 | * const requestCounter = Metric.counter("requests") |
| 1197 | * const cpuGauge = Metric.gauge("cpu_usage") |
| 1198 | * const statusFrequency = Metric.frequency("status_codes") |
| 1199 | * const responseHistogram = Metric.histogram("response_time", { |
| 1200 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) |
| 1201 | * }) |
| 1202 | * const latencySummary = Metric.summary("latency", { |
| 1203 | * maxAge: "5 minutes", |
| 1204 | * maxSize: 1000, |
| 1205 | * quantiles: [0.5, 0.95, 0.99] |
| 1206 | * }) |
| 1207 | * |
| 1208 | * // The State utility type extracts the state type from metric types: |
| 1209 | * // - Counter<number>: CounterState<number> |
| 1210 | * // - Gauge<number>: GaugeState<number> |
| 1211 | * // - Frequency: FrequencyState |
| 1212 | * // - Histogram<number>: HistogramState |
| 1213 | * // - Summary<number>: SummaryState |
| 1214 | * |
| 1215 | * // Type-safe state analysis functions |
| 1216 | * const program = Effect.gen(function*() { |
| 1217 | * // Update metrics first |
| 1218 | * yield* Metric.update(requestCounter, 10) |
| 1219 | * yield* Metric.update(cpuGauge, 85.5) |
| 1220 | * yield* Metric.update(statusFrequency, "200") |
| 1221 | * yield* Metric.update(responseHistogram, 150) |
| 1222 | * yield* Metric.update(latencySummary, 120) |
| 1223 | * |
| 1224 | * // Extract states with proper typing |
| 1225 | * const counterState = yield* Metric.value(requestCounter) |
| 1226 | * const gaugeState = yield* Metric.value(cpuGauge) |
| 1227 | * const frequencyState = yield* Metric.value(statusFrequency) |
| 1228 | * const histogramState = yield* Metric.value(responseHistogram) |
| 1229 | * const summaryState = yield* Metric.value(latencySummary) |
| 1230 | * return { |
| 1231 | * counter: { count: counterState.count }, // { count: 10 } |
| 1232 | * gauge: { value: gaugeState.value }, // { value: 85.5 } |
| 1233 | * frequency: { uniqueValues: frequencyState.occurrences.size }, // { uniqueValues: 1 } |
| 1234 | * histogram: { totalObservations: histogramState.count }, // { totalObservations: 1 } |
| 1235 | * summary: { observations: summaryState.count } // { observations: 1 } |
| 1236 | * } |
| 1237 | * }) |
| 1238 | * |
| 1239 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1240 | * const values = [result.counter.count, result.gauge.value, result.frequency.uniqueValues] |
| 1241 | * values // => [10, 85.5, 1] |
| 1242 | * ``` |
| 1243 | * |
| 1244 | * @category utility types |
| 1245 | * @since 4.0.0 |
| 1246 | */ |
| 1247 | export type State<A> = A extends Metric<infer _Input, infer _State> ? _State |
| 1248 | : never |
| 1249 | |
| 1250 | /** |
| 1251 | * Interface defining the core hooks for metric operations: get, update, and modify. |
| 1252 | * |
| 1253 | * **Example** (Using metric hooks) |
| 1254 | * |
| 1255 | * ```ts import.meta.vitest |
| 1256 | * import { Effect, Metric } from "effect" |
| 1257 | * |
| 1258 | * const program = Effect.gen(function*() { |
| 1259 | * // Create a counter metric |
| 1260 | * const requestCounter = Metric.counter("requests_total", { |
| 1261 | * description: "Total number of requests" |
| 1262 | * }) |
| 1263 | * |
| 1264 | * // The Hooks interface provides three core operations for metrics: |
| 1265 | * // 1. get: retrieve current state |
| 1266 | * // 2. update: add/set a value |
| 1267 | * // 3. modify: transform the current state |
| 1268 | * |
| 1269 | * // These are low-level APIs. Most users should use high-level APIs: |
| 1270 | * // - Metric.value() for getting state |
| 1271 | * // - Metric.update() for updating values |
| 1272 | * // - Metric.modify() for modifying values |
| 1273 | * |
| 1274 | * // Example using high-level APIs (recommended) |
| 1275 | * yield* Metric.update(requestCounter, 1) |
| 1276 | * yield* Metric.update(requestCounter, 5) |
| 1277 | * const state = yield* Metric.value(requestCounter) |
| 1278 | * |
| 1279 | * return { |
| 1280 | * currentCount: state.count, // 6 |
| 1281 | * isIncremental: state.incremental // false |
| 1282 | * } |
| 1283 | * }) |
| 1284 | * |
| 1285 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1286 | * const state = result // => { currentCount: 6, isIncremental: false } |
| 1287 | * ``` |
| 1288 | * |
| 1289 | * @category models |
| 1290 | * @since 4.0.0 |
| 1291 | */ |
| 1292 | export interface Hooks<in Input, out State> { |
| 1293 | readonly get: (context: Context.Context<never>) => State |
| 1294 | readonly update: (input: Input, context: Context.Context<never>) => void |
| 1295 | readonly modify: (input: Input, context: Context.Context<never>) => void |
| 1296 | } |
| 1297 | |
| 1298 | /** |
| 1299 | * Interface containing complete metadata information about a metric. |
| 1300 | * |
| 1301 | * **Example** (Inspecting metric metadata) |
| 1302 | * |
| 1303 | * ```ts import.meta.vitest |
| 1304 | * import { Effect, Metric } from "effect" |
| 1305 | * |
| 1306 | * const program = Effect.gen(function*() { |
| 1307 | * // Create metrics with different configurations |
| 1308 | * const requestCounter = Metric.counter("http_requests_total", { |
| 1309 | * description: "Total number of HTTP requests", |
| 1310 | * attributes: { service: "api", version: "1.0" } |
| 1311 | * }) |
| 1312 | * |
| 1313 | * const memoryGauge = Metric.gauge("memory_usage_bytes", { |
| 1314 | * description: "Current memory usage in bytes" |
| 1315 | * }) |
| 1316 | * |
| 1317 | * const statusFrequency = Metric.frequency("http_status_codes") |
| 1318 | * |
| 1319 | * // The Metadata interface contains complete information about a metric: |
| 1320 | * // - id: metric identifier |
| 1321 | * // - type: metric type ("Counter", "Gauge", etc.) |
| 1322 | * // - description: optional description |
| 1323 | * // - attributes: optional key-value attributes |
| 1324 | * // - hooks: low-level operations interface |
| 1325 | * |
| 1326 | * // Each metric has associated metadata that can be inspected |
| 1327 | * yield* Metric.update(requestCounter, 10) |
| 1328 | * yield* Metric.update(memoryGauge, 256000000) |
| 1329 | * yield* Metric.update(statusFrequency, "200") |
| 1330 | * |
| 1331 | * return { |
| 1332 | * counter: { |
| 1333 | * id: requestCounter.id, // "http_requests_total" |
| 1334 | * type: requestCounter.type, // "Counter" |
| 1335 | * description: requestCounter.description // "Total number of HTTP requests" |
| 1336 | * }, |
| 1337 | * gauge: { |
| 1338 | * id: memoryGauge.id, // "memory_usage_bytes" |
| 1339 | * type: memoryGauge.type, // "Gauge" |
| 1340 | * description: memoryGauge.description // "Current memory usage in bytes" |
| 1341 | * }, |
| 1342 | * frequency: { |
| 1343 | * id: statusFrequency.id, // "http_status_codes" |
| 1344 | * type: statusFrequency.type, // "Frequency" |
| 1345 | * description: statusFrequency.description // undefined |
| 1346 | * } |
| 1347 | * } |
| 1348 | * }) |
| 1349 | * |
| 1350 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1351 | * const types = [result.counter.type, result.gauge.type, result.frequency.type] // => ["Counter", "Gauge", "Frequency"] |
| 1352 | * ``` |
| 1353 | * |
| 1354 | * @category models |
| 1355 | * @since 4.0.0 |
| 1356 | */ |
| 1357 | export interface Metadata<in Input, out State> { |
| 1358 | readonly id: string |
| 1359 | readonly type: Type |
| 1360 | readonly description: string | undefined |
| 1361 | readonly attributes: Metric.AttributeSet | undefined |
| 1362 | readonly hooks: Hooks<Input, State> |
| 1363 | } |
| 1364 | |
| 1365 | /** |
| 1366 | * Protocol interface for metric snapshots containing metadata and current state. |
| 1367 | * |
| 1368 | * **Example** (Inspecting metric snapshot protocols) |
| 1369 | * |
| 1370 | * ```ts import.meta.vitest |
| 1371 | * import { Effect, Metric } from "effect" |
| 1372 | * |
| 1373 | * const program = Effect.gen(function*() { |
| 1374 | * // Create and update metrics |
| 1375 | * const requestCounter = Metric.counter("requests", { |
| 1376 | * description: "Request count", |
| 1377 | * attributes: { service: "api" } |
| 1378 | * }) |
| 1379 | * |
| 1380 | * const responseTimeHistogram = Metric.histogram("response_time", { |
| 1381 | * description: "Response time distribution", |
| 1382 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) |
| 1383 | * }) |
| 1384 | * |
| 1385 | * yield* Metric.update(requestCounter, 25) |
| 1386 | * yield* Metric.update(responseTimeHistogram, 150) |
| 1387 | * yield* Metric.update(responseTimeHistogram, 75) |
| 1388 | * |
| 1389 | * // Take snapshot of all metrics |
| 1390 | * const snapshots = yield* Metric.snapshot |
| 1391 | * |
| 1392 | * // Each snapshot follows the SnapshotProto interface: |
| 1393 | * // - id: metric identifier |
| 1394 | * // - type: specific metric type |
| 1395 | * // - description: optional description |
| 1396 | * // - attributes: optional attributes |
| 1397 | * // - state: current metric state |
| 1398 | * |
| 1399 | * const counterSnapshot = snapshots.find((s) => s.id === "requests") |
| 1400 | * const histogramSnapshot = snapshots.find((s) => s.id === "response_time") |
| 1401 | * |
| 1402 | * return { |
| 1403 | * counter: counterSnapshot ? |
| 1404 | * { |
| 1405 | * id: counterSnapshot.id, // "requests" |
| 1406 | * type: counterSnapshot.type, // "Counter" |
| 1407 | * description: counterSnapshot.description, // "Request count" |
| 1408 | * hasAttributes: counterSnapshot.attributes !== undefined, // true |
| 1409 | * count: (counterSnapshot.state as any).count // 25 |
| 1410 | * } : |
| 1411 | * null, |
| 1412 | * histogram: histogramSnapshot ? |
| 1413 | * { |
| 1414 | * id: histogramSnapshot.id, // "response_time" |
| 1415 | * type: histogramSnapshot.type, // "Histogram" |
| 1416 | * observations: (histogramSnapshot.state as any).count // 2 |
| 1417 | * } : |
| 1418 | * null |
| 1419 | * } |
| 1420 | * }) |
| 1421 | * |
| 1422 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1423 | * const counts = [result.counter?.count, result.histogram?.observations] // => [25, 2] |
| 1424 | * ``` |
| 1425 | * |
| 1426 | * @category models |
| 1427 | * @since 4.0.0 |
| 1428 | */ |
| 1429 | export interface SnapshotProto<T extends Type, State> { |
| 1430 | readonly id: string |
| 1431 | readonly type: T |
| 1432 | readonly description: string | undefined |
| 1433 | readonly attributes: Metric.AttributeSet | undefined |
| 1434 | readonly state: State |
| 1435 | } |
| 1436 | |
| 1437 | /** |
| 1438 | * Union type representing all possible metric snapshot types with their corresponding states. |
| 1439 | * |
| 1440 | * **Example** (Analyzing metric snapshots) |
| 1441 | * |
| 1442 | * ```ts import.meta.vitest |
| 1443 | * import { Effect, Metric } from "effect" |
| 1444 | * |
| 1445 | * const program = Effect.gen(function*() { |
| 1446 | * // Create different types of metrics |
| 1447 | * const requestCounter = Metric.counter("requests_total") |
| 1448 | * const cpuGauge = Metric.gauge("cpu_usage_percent") |
| 1449 | * const statusFrequency = Metric.frequency("http_status") |
| 1450 | * const responseHistogram = Metric.histogram("response_time_ms", { |
| 1451 | * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) |
| 1452 | * }) |
| 1453 | * const latencySummary = Metric.summary("request_latency", { |
| 1454 | * maxAge: "1 minute", |
| 1455 | * maxSize: 100, |
| 1456 | * quantiles: [0.5, 0.95, 0.99] |
| 1457 | * }) |
| 1458 | * |
| 1459 | * // Update all metrics |
| 1460 | * yield* Metric.update(requestCounter, 150) |
| 1461 | * yield* Metric.update(cpuGauge, 45.7) |
| 1462 | * yield* Metric.update(statusFrequency, "200") |
| 1463 | * yield* Metric.update(statusFrequency, "404") |
| 1464 | * yield* Metric.update(responseHistogram, 250) |
| 1465 | * yield* Metric.update(latencySummary, 120) |
| 1466 | * |
| 1467 | * // Take snapshot of all metrics |
| 1468 | * const allSnapshots = yield* Metric.snapshot |
| 1469 | * |
| 1470 | * // Type-safe snapshot analysis using discriminated union |
| 1471 | * const analyzeSnapshot = (snapshot: any) => { |
| 1472 | * switch (snapshot.type) { |
| 1473 | * case "Counter": |
| 1474 | * return { type: "Counter", count: snapshot.state.count } |
| 1475 | * case "Gauge": |
| 1476 | * return { type: "Gauge", value: snapshot.state.value } |
| 1477 | * case "Frequency": |
| 1478 | * return { |
| 1479 | * type: "Frequency", |
| 1480 | * uniqueValues: snapshot.state.occurrences.size |
| 1481 | * } |
| 1482 | * case "Histogram": |
| 1483 | * return { type: "Histogram", observations: snapshot.state.count } |
| 1484 | * case "Summary": |
| 1485 | * return { type: "Summary", observations: snapshot.state.count } |
| 1486 | * } |
| 1487 | * } |
| 1488 | * |
| 1489 | * const analysis = allSnapshots.map(analyzeSnapshot) |
| 1490 | * |
| 1491 | * return { |
| 1492 | * totalMetrics: allSnapshots.length, // 5 |
| 1493 | * metricTypes: allSnapshots.map((s) => s.type), // ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] |
| 1494 | * analysis |
| 1495 | * } |
| 1496 | * }) |
| 1497 | * |
| 1498 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1499 | * const types = result.metricTypes // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] |
| 1500 | * ``` |
| 1501 | * |
| 1502 | * @category models |
| 1503 | * @since 4.0.0 |
| 1504 | */ |
| 1505 | export type Snapshot = |
| 1506 | | SnapshotProto<"Counter", CounterState<number | bigint>> |
| 1507 | | SnapshotProto<"Gauge", GaugeState<number | bigint>> |
| 1508 | | SnapshotProto<"Frequency", FrequencyState> |
| 1509 | | SnapshotProto<"Histogram", HistogramState> |
| 1510 | | SnapshotProto<"Summary", SummaryState> |
| 1511 | } |
| 1512 | |
| 1513 | /** |
| 1514 | * Service key for the current metric attributes context. |
| 1515 | * |
| 1516 | * **Example** (Accessing the current metric attributes key) |
| 1517 | * |
| 1518 | * ```ts import.meta.vitest |
| 1519 | * import { Effect, Metric } from "effect" |
| 1520 | * |
| 1521 | * const program = Effect.gen(function*() { |
| 1522 | * // The key is used internally by the Effect runtime to manage metric attributes |
| 1523 | * const key = Metric.CurrentMetricAttributesKey |
| 1524 | * |
| 1525 | * // Create metrics with base attributes |
| 1526 | * const requestCounter = Metric.counter("requests_total", { |
| 1527 | * description: "Total HTTP requests" |
| 1528 | * }) |
| 1529 | * |
| 1530 | * // The CurrentMetricAttributes service provides default attributes |
| 1531 | * // that get applied to all metrics in the current context |
| 1532 | * const baseAttributes = { service: "api", version: "1.0" } |
| 1533 | * |
| 1534 | * // Use withAttributes to apply attributes to metrics |
| 1535 | * const taggedCounter1 = Metric.withAttributes(requestCounter, baseAttributes) |
| 1536 | * const program1 = Metric.update(taggedCounter1, 1) |
| 1537 | * |
| 1538 | * const taggedCounter2 = Metric.withAttributes(requestCounter, { |
| 1539 | * ...baseAttributes, |
| 1540 | * endpoint: "/users" |
| 1541 | * }) |
| 1542 | * const program2 = Metric.update(taggedCounter2, 5) |
| 1543 | * |
| 1544 | * yield* program1 |
| 1545 | * yield* program2 |
| 1546 | * |
| 1547 | * return { |
| 1548 | * keyValue: key, // "effect/Metric/CurrentMetricAttributes" |
| 1549 | * keyType: typeof key, // "string" |
| 1550 | * isConstant: key === "effect/Metric/CurrentMetricAttributes" // true |
| 1551 | * } |
| 1552 | * }) |
| 1553 | * |
| 1554 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 1555 | * const key = result // => { keyValue: "effect/Metric/CurrentMetricAttributes", keyType: "string", isConstant: true } |
| 1556 | * ``` |
| 1557 | * |
| 1558 | * @category constants |
| 1559 | * @since 4.0.0 |
| 1560 | */ |
| 1561 | export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes" as const |
| 1562 | |
| 1563 | /** |
| 1564 | * Context reference for metric attributes applied from the current Effect |
| 1565 | * context. |
| 1566 | * |
| 1567 | * **When to use** |
| 1568 | * |
| 1569 | * Use to provide default attributes that should be merged into metric updates |
| 1570 | * and reads in a scoped part of a program. |
| 1571 | * |
| 1572 | * **Details** |
| 1573 | * |
| 1574 | * The default value is an empty attribute set. Metric reads and updates merge |
| 1575 | * these contextual attributes with the metric's own attributes to select the |
| 1576 | * metric series being accessed. |
| 1577 | * |
| 1578 | * **Example** (Providing current metric attributes) |
| 1579 | * |
| 1580 | * ```ts import.meta.vitest |
| 1581 | * import { Effect, Metric } from "effect" |
| 1582 | * |
| 1583 | * const program = Effect.gen(function*() { |
| 1584 | * // Access current metric attributes |
| 1585 | * yield* Metric.CurrentMetricAttributes |
| 1586 | * |
| 1587 | * // Set new attributes context |
| 1588 | * const newAttributes = { service: "api", version: "1.0" } |
| 1589 | * const result = yield* Effect.provideService( |
| 1590 | * Effect.gen(function*() { |
| 1591 | * const updatedAttributes = yield* Metric.CurrentMetricAttributes |
| 1592 | * return updatedAttributes |
| 1593 | * }), |
| 1594 | * Metric.CurrentMetricAttributes, |
| 1595 | * newAttributes |
| 1596 | * ) |
| 1597 | * |
| 1598 | * return result |
| 1599 | * }) |
| 1600 | * |
| 1601 | * const attributes = await Effect.runPromise(program) |
| 1602 | * const actual = attributes // => { service: "api", version: "1.0" } |
| 1603 | * ``` |
| 1604 | * |
| 1605 | * @category services |
| 1606 | * @since 4.0.0 |
| 1607 | */ |
| 1608 | export const CurrentMetricAttributes = Context.Reference<Metric.AttributeSet>(CurrentMetricAttributesKey, { |
| 1609 | defaultValue: () => ({}) |
| 1610 | }) |
| 1611 | |
| 1612 | const MetricRegistryKey = "effect/Metric/MetricRegistry" |
| 1613 | |
| 1614 | /** |
| 1615 | * The registry used to store metric metadata and hooks. |
| 1616 | * |
| 1617 | * @category services |
| 1618 | * @since 4.0.0 |
| 1619 | */ |
| 1620 | export type MetricRegistry = Map<string, Metric.Metadata<any, any>> |
| 1621 | |
| 1622 | /** |
| 1623 | * Context reference for the metric registry in the current context. |
| 1624 | * |
| 1625 | * **When to use** |
| 1626 | * |
| 1627 | * Use when you need a custom metric registry for an isolated program or test |
| 1628 | * instead of the default registry. |
| 1629 | * |
| 1630 | * **Details** |
| 1631 | * |
| 1632 | * By default, the reference creates an empty `Map` the first time it is |
| 1633 | * resolved. Metrics register their metadata and hooks lazily in this map when |
| 1634 | * they are read or updated. |
| 1635 | * |
| 1636 | * **Gotchas** |
| 1637 | * |
| 1638 | * Because `Context.Reference` caches default values, the default `Map` is |
| 1639 | * shared by contexts that do not provide an override. Provide `MetricRegistry` |
| 1640 | * with a fresh `Map` when isolation matters. |
| 1641 | * |
| 1642 | * @see {@link snapshot} for reading all registered metrics from the current `Effect` context |
| 1643 | * @see {@link snapshotUnsafe} for reading all registered metrics from an explicit `Context` |
| 1644 | * |
| 1645 | * @category services |
| 1646 | * @since 4.0.0 |
| 1647 | */ |
| 1648 | export const MetricRegistry: Context.Reference<MetricRegistry> = Context.Reference<MetricRegistry>( |
| 1649 | MetricRegistryKey, |
| 1650 | { defaultValue: () => new Map() } |
| 1651 | ) |
| 1652 | |
| 1653 | const TypeId = "~effect/Metric" |
| 1654 | |
| 1655 | abstract class Metric$<in Input, out State> implements Metric<Input, State> { |
| 1656 | readonly [TypeId] = TypeId |
| 1657 | |
| 1658 | abstract readonly type: Metric.Type |
| 1659 | |
| 1660 | declare readonly Input: Contravariant<Input> |
| 1661 | declare readonly State: Covariant<State> |
| 1662 | |
| 1663 | #extraAttributes: Metric.AttributeSet | undefined |
| 1664 | #key = "" |
| 1665 | |
| 1666 | readonly id: string |
| 1667 | readonly description: string | undefined |
| 1668 | readonly attributes: Metric.AttributeSet | undefined |
| 1669 | |
| 1670 | constructor( |
| 1671 | id: string, |
| 1672 | description: string | undefined, |
| 1673 | attributes: Metric.AttributeSet | undefined |
| 1674 | ) { |
| 1675 | this.id = id |
| 1676 | this.description = description |
| 1677 | this.attributes = attributes |
| 1678 | } |
| 1679 | |
| 1680 | valueUnsafe(context: Context.Context<never>): State { |
| 1681 | return this.hook(context).get(context) |
| 1682 | } |
| 1683 | |
| 1684 | modifyUnsafe(input: Input, context: Context.Context<never>): void { |
| 1685 | return this.hook(context).modify(input, context) |
| 1686 | } |
| 1687 | |
| 1688 | updateUnsafe(input: Input, context: Context.Context<never>): void { |
| 1689 | return this.hook(context).update(input, context) |
| 1690 | } |
| 1691 | |
| 1692 | abstract createHooks(): Metric.Hooks<Input, State> |
| 1693 | |
| 1694 | hook(context: Context.Context<never>): Metric.Hooks<Input, State> { |
| 1695 | const extraAttributes = Context.get(context, CurrentMetricAttributes) |
| 1696 | if (extraAttributes !== this.#extraAttributes) { |
| 1697 | this.#extraAttributes = extraAttributes |
| 1698 | this.#key = makeKey(this, mergeAttributes(this.attributes, extraAttributes)) |
| 1699 | } |
| 1700 | // The registry owns the series, so it is looked up on every update. |
| 1701 | const registry = Context.get(context, MetricRegistry) |
| 1702 | let metadata = registry.get(this.#key) |
| 1703 | if (Predicate.isUndefined(metadata)) { |
| 1704 | metadata = { |
| 1705 | id: this.id, |
| 1706 | type: this.type, |
| 1707 | description: this.description, |
| 1708 | attributes: Object.keys(extraAttributes).length > 0 |
| 1709 | ? mergeAttributes(this.attributes, extraAttributes) |
| 1710 | : this.attributes, |
| 1711 | hooks: this.createHooks() |
| 1712 | } |
| 1713 | registry.set(this.#key, metadata) |
| 1714 | } |
| 1715 | return metadata.hooks |
| 1716 | } |
| 1717 | |
| 1718 | pipe() { |
| 1719 | return pipeArguments(this, arguments) |
| 1720 | } |
| 1721 | } |
| 1722 | |
| 1723 | const bigint0 = BigInt(0) |
| 1724 | |
| 1725 | class CounterMetric<Input extends number | bigint> extends Metric$<Input, CounterState<Input>> { |
| 1726 | readonly type = "Counter" |
| 1727 | readonly #bigint: boolean |
| 1728 | readonly #incremental: boolean |
| 1729 | |
| 1730 | constructor(id: string, options?: { |
| 1731 | readonly description?: string | undefined |
| 1732 | readonly attributes?: Metric.Attributes | undefined |
| 1733 | readonly bigint?: boolean | undefined |
| 1734 | readonly incremental?: boolean | undefined |
| 1735 | }) { |
| 1736 | super(id, options?.description, attributesToRecord(options?.attributes)) |
| 1737 | this.#bigint = options?.bigint ?? false |
| 1738 | this.#incremental = options?.incremental ?? false |
| 1739 | } |
| 1740 | |
| 1741 | createHooks(): Metric.Hooks<Input, CounterState<Input>> { |
| 1742 | let count = (this.#bigint ? bigint0 : 0) as any |
| 1743 | const canUpdate = this.#incremental |
| 1744 | ? this.#bigint |
| 1745 | ? (value: bigint | number) => value >= bigint0 |
| 1746 | : (value: bigint | number) => value >= 0 |
| 1747 | : (_value: bigint | number) => true |
| 1748 | const update = (value: Input) => { |
| 1749 | if (canUpdate(value)) { |
| 1750 | count = (count as any) + value |
| 1751 | } |
| 1752 | } |
| 1753 | return makeHooks(() => ({ count, incremental: this.#incremental }), update) |
| 1754 | } |
| 1755 | } |
| 1756 | |
| 1757 | class GaugeMetric<Input extends number | bigint> extends Metric$<Input, GaugeState<Input>> { |
| 1758 | readonly type = "Gauge" |
| 1759 | readonly #bigint: boolean |
| 1760 | |
| 1761 | constructor(id: string, options?: { |
| 1762 | readonly description?: string | undefined |
| 1763 | readonly attributes?: Metric.Attributes | undefined |
| 1764 | readonly bigint?: boolean | undefined |
| 1765 | }) { |
| 1766 | super(id, options?.description, attributesToRecord(options?.attributes)) |
| 1767 | this.#bigint = options?.bigint ?? false |
| 1768 | } |
| 1769 | |
| 1770 | createHooks(): Metric.Hooks<Input, GaugeState<Input>> { |
| 1771 | let value = this.#bigint ? BigInt(0) as any : 0 |
| 1772 | const update = (input: number | bigint) => { |
| 1773 | value = input |
| 1774 | } |
| 1775 | const modify = (input: number | bigint) => { |
| 1776 | value = value + input |
| 1777 | } |
| 1778 | return makeHooks(() => ({ value }), update, modify) |
| 1779 | } |
| 1780 | } |
| 1781 | |
| 1782 | class FrequencyMetric extends Metric$<string, FrequencyState> { |
| 1783 | readonly type = "Frequency" |
| 1784 | readonly #preregisteredWords: ReadonlyArray<string> | undefined |
| 1785 | |
| 1786 | constructor(id: string, options?: { |
| 1787 | readonly description?: string | undefined |
| 1788 | readonly attributes?: Metric.Attributes | undefined |
| 1789 | readonly preregisteredWords?: ReadonlyArray<string> | undefined |
| 1790 | }) { |
| 1791 | super(id, options?.description, attributesToRecord(options?.attributes)) |
| 1792 | this.#preregisteredWords = options?.preregisteredWords |
| 1793 | } |
| 1794 | |
| 1795 | createHooks(): Metric.Hooks<string, FrequencyState> { |
| 1796 | const occurrences = new Map<string, number>() |
| 1797 | if (Predicate.isNotUndefined(this.#preregisteredWords)) { |
| 1798 | for (const word of this.#preregisteredWords) { |
| 1799 | occurrences.set(word, 0) |
| 1800 | } |
| 1801 | } |
| 1802 | const update = (word: string) => { |
| 1803 | const count = occurrences.get(word) ?? 0 |
| 1804 | occurrences.set(word, count + 1) |
| 1805 | } |
| 1806 | return makeHooks(() => ({ occurrences }), update) |
| 1807 | } |
| 1808 | } |
| 1809 | |
| 1810 | class HistogramMetric extends Metric$<number, HistogramState> { |
| 1811 | readonly type = "Histogram" |
| 1812 | readonly #boundaries: ReadonlyArray<number> |
| 1813 | |
| 1814 | constructor(id: string, options: { |
| 1815 | readonly description?: string | undefined |
| 1816 | readonly attributes?: Metric.Attributes | undefined |
| 1817 | readonly boundaries: ReadonlyArray<number> |
| 1818 | }) { |
| 1819 | super(id, options?.description, attributesToRecord(options?.attributes)) |
| 1820 | this.#boundaries = options.boundaries |
| 1821 | } |
| 1822 | |
| 1823 | createHooks(): Metric.Hooks<number, HistogramState> { |
| 1824 | const bounds = this.#boundaries |
| 1825 | const size = bounds.length |
| 1826 | const values = new Uint32Array(size + 1) |
| 1827 | const boundaries = new Float64Array(size) |
| 1828 | let count = 0 |
| 1829 | let sum = 0 |
| 1830 | let min = Number.MAX_VALUE |
| 1831 | let max = -Number.MAX_VALUE |
| 1832 | |
| 1833 | Arr.map(Arr.sort(bounds, Order.Number), (n, i) => { |
| 1834 | boundaries[i] = n |
| 1835 | }) |
| 1836 | |
| 1837 | // Insert the value into the right bucket with a binary search |
| 1838 | const update = (value: number) => { |
| 1839 | let from = 0 |
| 1840 | let to = size |
| 1841 | while (from !== to) { |
| 1842 | const mid = Math.floor(from + (to - from) / 2) |
| 1843 | const boundary = boundaries[mid] |
| 1844 | if (value <= boundary) { |
| 1845 | to = mid |
| 1846 | } else { |
| 1847 | from = mid |
| 1848 | } |
| 1849 | // The special case when to / from have a distance of one |
| 1850 | if (to === from + 1) { |
| 1851 | if (value <= boundaries[from]) { |
| 1852 | to = from |
| 1853 | } else { |
| 1854 | from = to |
| 1855 | } |
| 1856 | } |
| 1857 | } |
| 1858 | values[from] = values[from] + 1 |
| 1859 | count = count + 1 |
| 1860 | sum = sum + value |
| 1861 | if (value < min) { |
| 1862 | min = value |
| 1863 | } |
| 1864 | if (value > max) { |
| 1865 | max = value |
| 1866 | } |
| 1867 | } |
| 1868 | |
| 1869 | const getBuckets = (): ReadonlyArray<[number, number]> => { |
| 1870 | const builder: Array<[number, number]> = Arr.allocate(size) as any |
| 1871 | let cumulated = 0 |
| 1872 | for (let i = 0; i < size; i++) { |
| 1873 | const boundary = boundaries[i] |
| 1874 | const value = values[i] |
| 1875 | cumulated = cumulated + value |
| 1876 | builder[i] = [boundary, cumulated] |
| 1877 | } |
| 1878 | return builder |
| 1879 | } |
| 1880 | |
| 1881 | return makeHooks(() => ({ buckets: getBuckets(), count, min, max, sum }), update) |
| 1882 | } |
| 1883 | } |
| 1884 | |
| 1885 | class SummaryMetric extends Metric$<readonly [value: number, timestamp: number], SummaryState> { |
| 1886 | readonly type = "Summary" |
| 1887 | readonly #maxAge: number |
| 1888 | readonly #maxSize: number |
| 1889 | readonly #quantiles: ReadonlyArray<number> |
| 1890 | |
| 1891 | constructor(id: string, options: { |
| 1892 | readonly description?: string | undefined |
| 1893 | readonly attributes?: Metric.Attributes | undefined |
| 1894 | readonly maxAge: Duration.Input |
| 1895 | readonly maxSize: number |
| 1896 | readonly quantiles: ReadonlyArray<number> |
| 1897 | }) { |
| 1898 | super(id, options?.description, attributesToRecord(options?.attributes)) |
| 1899 | this.#maxAge = Math.max(Duration.toMillis(Duration.fromInputUnsafe(options.maxAge)), 0) |
| 1900 | this.#maxSize = options.maxSize |
| 1901 | this.#quantiles = options.quantiles |
| 1902 | } |
| 1903 | |
| 1904 | createHooks(): Metric.Hooks<readonly [value: number, timestamp: number], SummaryState> { |
| 1905 | const sortedQuantiles = Arr.sort(this.#quantiles, Order.Number) |
| 1906 | const observations = Arr.allocate<[number, number]>(this.#maxSize) |
| 1907 | |
| 1908 | for (const quantile of this.#quantiles) { |
| 1909 | if (quantile < 0 || quantile > 1) { |
| 1910 | throw new Error(`Quantile must be between 0 and 1, found: ${quantile}`) |
| 1911 | } |
| 1912 | } |
| 1913 | |
| 1914 | let head = 0 |
| 1915 | let count = 0 |
| 1916 | let sum = 0 |
| 1917 | let min = Number.MAX_VALUE |
| 1918 | let max = -Number.MAX_VALUE |
| 1919 | |
| 1920 | const snapshot = (now: number): ReadonlyArray<[number, number | undefined]> => { |
| 1921 | const builder: Array<number> = [] |
| 1922 | let i = 0 |
| 1923 | while (i < this.#maxSize) { |
| 1924 | const observation = observations[i] |
| 1925 | if (Predicate.isNotUndefined(observation)) { |
| 1926 | const [timestamp, value] = observation |
| 1927 | const age = now - timestamp |
| 1928 | if (age >= 0 && age <= this.#maxAge) { |
| 1929 | builder.push(value) |
| 1930 | } |
| 1931 | } |
| 1932 | i = i + 1 |
| 1933 | } |
| 1934 | const samples = Arr.sort(builder, Order.Number) |
| 1935 | const sampleSize = samples.length |
| 1936 | if (sampleSize === 0) { |
| 1937 | return sortedQuantiles.map((q) => [q, undefined]) |
| 1938 | } |
| 1939 | // Compute the value of the quantile in terms of rank: |
| 1940 | // For a given quantile `q`, return the maximum value `v` such that at |
| 1941 | // most `q * n` values are less than or equal to `v`. |
| 1942 | return sortedQuantiles.map((q) => { |
| 1943 | if (q <= 0) return [q, samples[0]] |
| 1944 | if (q >= 1) return [q, samples[sampleSize - 1]] |
| 1945 | const index = Math.ceil(q * sampleSize) - 1 |
| 1946 | return [q, samples[index]] |
| 1947 | }) |
| 1948 | } |
| 1949 | |
| 1950 | const observe = (value: number, timestamp: number) => { |
| 1951 | if (this.#maxSize > 0) { |
| 1952 | const target = head % this.#maxSize |
| 1953 | observations[target] = [timestamp, value] as const |
| 1954 | head = head + 1 |
| 1955 | } |
| 1956 | count = count + 1 |
| 1957 | sum = sum + value |
| 1958 | if (value < min) { |
| 1959 | min = value |
| 1960 | } |
| 1961 | if (value > max) { |
| 1962 | max = value |
| 1963 | } |
| 1964 | } |
| 1965 | |
| 1966 | const get = (context: Context.Context<never>) => { |
| 1967 | const clock = Context.get(context, InternalEffect.ClockRef) |
| 1968 | const quantiles = snapshot(clock.currentTimeMillisUnsafe()) |
| 1969 | return { quantiles, count, min, max, sum } |
| 1970 | } |
| 1971 | |
| 1972 | const update = ([value, timestamp]: readonly [value: number, timestamp: number]) => observe(value, timestamp) |
| 1973 | |
| 1974 | return makeHooks(get, update) |
| 1975 | } |
| 1976 | } |
| 1977 | |
| 1978 | class MetricTransform<in Input, out State, in Input2> extends Metric$<Input2, State> { |
| 1979 | type: Metric.Type |
| 1980 | readonly metric: Metric<Input, State> |
| 1981 | override readonly valueUnsafe: (context: Context.Context<never>) => State |
| 1982 | override readonly updateUnsafe: (input: Input2, context: Context.Context<never>) => void |
| 1983 | override readonly modifyUnsafe: (input: Input2, context: Context.Context<never>) => void |
| 1984 | |
| 1985 | constructor( |
| 1986 | metric: Metric<Input, State>, |
| 1987 | valueUnsafe: (context: Context.Context<never>) => State, |
| 1988 | updateUnsafe: (input: Input2, context: Context.Context<never>) => void, |
| 1989 | modifyUnsafe: (input: Input2, context: Context.Context<never>) => void |
| 1990 | ) { |
| 1991 | super(metric.id, metric.description, metric.attributes) |
| 1992 | this.metric = metric |
| 1993 | this.valueUnsafe = valueUnsafe |
| 1994 | this.updateUnsafe = updateUnsafe |
| 1995 | this.modifyUnsafe = modifyUnsafe |
| 1996 | this.type = metric.type |
| 1997 | } |
| 1998 | createHooks(): Metric.Hooks<Input2, State> { |
| 1999 | return (this.metric as any).createHooks() |
| 2000 | } |
| 2001 | } |
| 2002 | |
| 2003 | /** |
| 2004 | * Returns `true` if the specified value is a `Metric`, otherwise returns `false`. |
| 2005 | * |
| 2006 | * **When to use** |
| 2007 | * |
| 2008 | * Use when you need runtime type checking and ensuring that a value |
| 2009 | * conforms to the Metric interface before performing metric operations. |
| 2010 | * |
| 2011 | * **Example** (Checking metric values) |
| 2012 | * |
| 2013 | * ```ts import.meta.vitest |
| 2014 | * import { Metric } from "effect" |
| 2015 | * |
| 2016 | * Metric.isMetric(Metric.counter("requests")) // => true |
| 2017 | * Metric.isMetric({ name: "requests" }) // => false |
| 2018 | * ``` |
| 2019 | * |
| 2020 | * @category guards |
| 2021 | * @since 4.0.0 |
| 2022 | */ |
| 2023 | export const isMetric = (u: unknown): u is Metric<never, unknown> => |
| 2024 | Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId |
| 2025 | |
| 2026 | /** |
| 2027 | * Represents a Counter metric that tracks cumulative numerical values over |
| 2028 | * time. Counters can be incremented and decremented and provide a running total |
| 2029 | * of changes. |
| 2030 | * |
| 2031 | * **Details** |
| 2032 | * |
| 2033 | * The optional `description` describes the counter, and `attributes` attach |
| 2034 | * dimensions to it. Set `bigint` to create a counter that accepts `bigint` |
| 2035 | * inputs. Set `incremental` to `true` to create a counter that can only ever be |
| 2036 | * incremented. |
| 2037 | * |
| 2038 | * **Example** (Creating counter metrics) |
| 2039 | * |
| 2040 | * ```ts import.meta.vitest |
| 2041 | * import { Effect, Metric } from "effect" |
| 2042 | * |
| 2043 | * const program = Effect.gen(function*() { |
| 2044 | * // Create a basic counter for tracking requests |
| 2045 | * const requestCounter = Metric.counter("http_requests_total", { |
| 2046 | * description: "Total number of HTTP requests processed" |
| 2047 | * }) |
| 2048 | * |
| 2049 | * // Create an incremental-only counter for events |
| 2050 | * const eventCounter = Metric.counter("events_processed", { |
| 2051 | * description: "Events processed (increment only)", |
| 2052 | * incremental: true |
| 2053 | * }) |
| 2054 | * |
| 2055 | * // Create a bigint counter for large values |
| 2056 | * const bytesCounter = Metric.counter("bytes_transferred", { |
| 2057 | * description: "Total bytes transferred", |
| 2058 | * bigint: true, |
| 2059 | * attributes: { service: "file-transfer" } |
| 2060 | * }) |
| 2061 | * |
| 2062 | * // Update counters with values |
| 2063 | * yield* Metric.update(requestCounter, 1) // Increment by 1 |
| 2064 | * yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6) |
| 2065 | * yield* Metric.update(eventCounter, 1) // Increment by 1 |
| 2066 | * yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes |
| 2067 | * |
| 2068 | * // Get current counter values |
| 2069 | * const requestValue = yield* Metric.value(requestCounter) |
| 2070 | * const eventValue = yield* Metric.value(eventCounter) |
| 2071 | * const bytesValue = yield* Metric.value(bytesCounter) |
| 2072 | * |
| 2073 | * return { requestValue, eventValue, bytesValue } |
| 2074 | * }) |
| 2075 | * |
| 2076 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2077 | * const counts = [result.requestValue.count, result.eventValue.count, result.bytesValue.count] // => [6, 1, 1024n] |
| 2078 | * ``` |
| 2079 | * |
| 2080 | * @category constructors |
| 2081 | * @since 2.0.0 |
| 2082 | */ |
| 2083 | export const counter: { |
| 2084 | ( |
| 2085 | name: string, |
| 2086 | options?: { |
| 2087 | readonly description?: string | undefined |
| 2088 | readonly attributes?: Metric.Attributes | undefined |
| 2089 | readonly bigint?: false | undefined |
| 2090 | readonly incremental?: boolean | undefined |
| 2091 | } |
| 2092 | ): Counter<number> |
| 2093 | ( |
| 2094 | name: string, |
| 2095 | options: { |
| 2096 | readonly description?: string | undefined |
| 2097 | readonly attributes?: Metric.Attributes | undefined |
| 2098 | readonly bigint: true |
| 2099 | readonly incremental?: boolean | undefined |
| 2100 | } |
| 2101 | ): Counter<bigint> |
| 2102 | } = (name, options) => new CounterMetric(name, options) as any |
| 2103 | |
| 2104 | /** |
| 2105 | * Represents a `Gauge` metric that tracks and reports a single numerical value |
| 2106 | * at a specific moment. |
| 2107 | * |
| 2108 | * **When to use** |
| 2109 | * |
| 2110 | * Use when you need a metric for instantaneous values, such as memory usage or |
| 2111 | * CPU load. |
| 2112 | * |
| 2113 | * **Details** |
| 2114 | * |
| 2115 | * The optional `description` describes the gauge, and `attributes` attach |
| 2116 | * dimensions to it. Set `bigint` to create a gauge that accepts `bigint` |
| 2117 | * inputs. |
| 2118 | * |
| 2119 | * **Example** (Creating gauge metrics) |
| 2120 | * |
| 2121 | * ```ts import.meta.vitest |
| 2122 | * import { Effect, Metric } from "effect" |
| 2123 | * |
| 2124 | * const program = Effect.gen(function*() { |
| 2125 | * // Create a gauge for tracking memory usage |
| 2126 | * const memoryGauge = Metric.gauge("memory_usage_mb", { |
| 2127 | * description: "Current memory usage in megabytes" |
| 2128 | * }) |
| 2129 | * |
| 2130 | * // Create a gauge for CPU utilization |
| 2131 | * const cpuGauge = Metric.gauge("cpu_utilization", { |
| 2132 | * description: "Current CPU utilization percentage", |
| 2133 | * attributes: { host: "server-01" } |
| 2134 | * }) |
| 2135 | * |
| 2136 | * // Create a bigint gauge for large values |
| 2137 | * const diskSpaceGauge = Metric.gauge("disk_free_bytes", { |
| 2138 | * description: "Free disk space in bytes", |
| 2139 | * bigint: true |
| 2140 | * }) |
| 2141 | * |
| 2142 | * // Set gauge values (replaces current value) |
| 2143 | * yield* Metric.update(memoryGauge, 512) // Set to 512 MB |
| 2144 | * yield* Metric.update(cpuGauge, 85.5) // Set to 85.5% |
| 2145 | * yield* Metric.update(diskSpaceGauge, 1024000000n) // Set to ~1GB |
| 2146 | * |
| 2147 | * // Modify gauge values (adds to current value) |
| 2148 | * yield* Metric.modify(memoryGauge, 128) // Increase by 128 MB (total: 640) |
| 2149 | * yield* Metric.modify(cpuGauge, -10.5) // Decrease by 10.5% (total: 75%) |
| 2150 | * |
| 2151 | * // Update with new absolute values |
| 2152 | * yield* Metric.update(memoryGauge, 800) // Set to 800 MB (replaces 640) |
| 2153 | * |
| 2154 | * // Get current gauge values |
| 2155 | * const memoryValue = yield* Metric.value(memoryGauge) |
| 2156 | * const cpuValue = yield* Metric.value(cpuGauge) |
| 2157 | * const diskValue = yield* Metric.value(diskSpaceGauge) |
| 2158 | * |
| 2159 | * return { memoryValue, cpuValue, diskValue } |
| 2160 | * }) |
| 2161 | * |
| 2162 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2163 | * const values = [result.memoryValue.value, result.cpuValue.value, result.diskValue.value] // => [800, 75, 1024000000n] |
| 2164 | * ``` |
| 2165 | * |
| 2166 | * @category constructors |
| 2167 | * @since 2.0.0 |
| 2168 | */ |
| 2169 | export const gauge: { |
| 2170 | (name: string, options?: { |
| 2171 | readonly description?: string | undefined |
| 2172 | readonly attributes?: Metric.Attributes | undefined |
| 2173 | readonly bigint?: false | undefined |
| 2174 | }): Gauge<number> |
| 2175 | (name: string, options: { |
| 2176 | readonly description?: string | undefined |
| 2177 | readonly attributes?: Metric.Attributes | undefined |
| 2178 | readonly bigint: true |
| 2179 | }): Gauge<bigint> |
| 2180 | } = (name, options) => new GaugeMetric(name, options) as any |
| 2181 | |
| 2182 | /** |
| 2183 | * Creates a `Frequency` metric which can be used to count the number of |
| 2184 | * occurrences of a string. |
| 2185 | * |
| 2186 | * **When to use** |
| 2187 | * |
| 2188 | * Use when you need a metric for counting how often a specific event or |
| 2189 | * incident occurs. |
| 2190 | * |
| 2191 | * **Details** |
| 2192 | * |
| 2193 | * The optional `description` describes the frequency, and `attributes` attach |
| 2194 | * dimensions to it. Use `preregisteredWords` to initialize occurrence counts |
| 2195 | * for known string values before updates arrive. |
| 2196 | * |
| 2197 | * **Example** (Creating frequency metrics) |
| 2198 | * |
| 2199 | * ```ts import.meta.vitest |
| 2200 | * import { Effect, Metric } from "effect" |
| 2201 | * |
| 2202 | * const program = Effect.gen(function*() { |
| 2203 | * // Create a frequency metric for HTTP status codes |
| 2204 | * const statusFrequency = Metric.frequency("http_status_codes", { |
| 2205 | * description: "Frequency of HTTP response status codes", |
| 2206 | * preregisteredWords: ["200", "404", "500"] // Pre-register common codes |
| 2207 | * }) |
| 2208 | * |
| 2209 | * // Create a frequency metric for user actions |
| 2210 | * const userActionFrequency = Metric.frequency("user_actions", { |
| 2211 | * description: "Frequency of user actions performed", |
| 2212 | * attributes: { application: "web-app" } |
| 2213 | * }) |
| 2214 | * |
| 2215 | * // Create a frequency metric for error types |
| 2216 | * const errorTypeFrequency = Metric.frequency("error_types", { |
| 2217 | * description: "Frequency of different error types" |
| 2218 | * }) |
| 2219 | * |
| 2220 | * // Record different occurrences |
| 2221 | * yield* Metric.update(statusFrequency, "200") // Success response |
| 2222 | * yield* Metric.update(statusFrequency, "200") // Another success |
| 2223 | * yield* Metric.update(statusFrequency, "404") // Not found error |
| 2224 | * yield* Metric.update(statusFrequency, "500") // Server error |
| 2225 | * yield* Metric.update(statusFrequency, "200") // Another success |
| 2226 | * |
| 2227 | * yield* Metric.update(userActionFrequency, "login") |
| 2228 | * yield* Metric.update(userActionFrequency, "view_dashboard") |
| 2229 | * yield* Metric.update(userActionFrequency, "login") |
| 2230 | * yield* Metric.update(userActionFrequency, "logout") |
| 2231 | * |
| 2232 | * yield* Metric.update(errorTypeFrequency, "ValidationError") |
| 2233 | * yield* Metric.update(errorTypeFrequency, "NetworkError") |
| 2234 | * yield* Metric.update(errorTypeFrequency, "ValidationError") |
| 2235 | * |
| 2236 | * // Get frequency counts |
| 2237 | * const statusCounts = yield* Metric.value(statusFrequency) |
| 2238 | * const actionCounts = yield* Metric.value(userActionFrequency) |
| 2239 | * const errorCounts = yield* Metric.value(errorTypeFrequency) |
| 2240 | * |
| 2241 | * // statusCounts.occurrences will be: |
| 2242 | * // Map { "200" => 3, "404" => 1, "500" => 1 } |
| 2243 | * // actionCounts.occurrences will be: |
| 2244 | * // Map { "login" => 2, "view_dashboard" => 1, "logout" => 1 } |
| 2245 | * // errorCounts.occurrences will be: |
| 2246 | * // Map { "ValidationError" => 2, "NetworkError" => 1 } |
| 2247 | * |
| 2248 | * return { statusCounts, actionCounts, errorCounts } |
| 2249 | * }) |
| 2250 | * |
| 2251 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2252 | * const counts = [ |
| 2253 | * result.statusCounts.occurrences.get("200"), |
| 2254 | * result.actionCounts.occurrences.get("login"), |
| 2255 | * result.errorCounts.occurrences.get("ValidationError") |
| 2256 | * ] |
| 2257 | * counts // => [3, 2, 2] |
| 2258 | * ``` |
| 2259 | * |
| 2260 | * @category constructors |
| 2261 | * @since 2.0.0 |
| 2262 | */ |
| 2263 | export const frequency = (name: string, options?: { |
| 2264 | readonly description?: string | undefined |
| 2265 | readonly attributes?: Metric.Attributes | undefined |
| 2266 | readonly preregisteredWords?: ReadonlyArray<string> | undefined |
| 2267 | }): Frequency => new FrequencyMetric(name, options) |
| 2268 | |
| 2269 | /** |
| 2270 | * Represents a `Histogram` metric that records observations into buckets. |
| 2271 | * |
| 2272 | * **When to use** |
| 2273 | * |
| 2274 | * Use when you need a metric for measuring the distribution of values within a |
| 2275 | * range. |
| 2276 | * |
| 2277 | * **Details** |
| 2278 | * |
| 2279 | * The optional `description` describes the histogram, and `attributes` attach |
| 2280 | * dimensions to it. The required `boundaries` option defines the histogram |
| 2281 | * bucket boundaries. |
| 2282 | * |
| 2283 | * **Example** (Creating histogram metrics) |
| 2284 | * |
| 2285 | * ```ts import.meta.vitest |
| 2286 | * import { Effect, Metric } from "effect" |
| 2287 | * |
| 2288 | * const program = Effect.gen(function*() { |
| 2289 | * // Create a histogram for API response times |
| 2290 | * const responseTimeHistogram = Metric.histogram("api_response_time", { |
| 2291 | * description: "Distribution of API response times in milliseconds", |
| 2292 | * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) |
| 2293 | * // Creates buckets: 0-50ms, 50-100ms, 100-150ms, ..., 350-400ms, 400ms+ |
| 2294 | * }) |
| 2295 | * |
| 2296 | * // Create a histogram for request payload sizes |
| 2297 | * const payloadSizeHistogram = Metric.histogram("payload_size", { |
| 2298 | * description: "Distribution of request payload sizes in KB", |
| 2299 | * boundaries: Metric.exponentialBoundaries({ start: 1, factor: 2, count: 8 }), |
| 2300 | * // Creates exponential buckets: 1KB, 2KB, 4KB, 8KB, 16KB, 32KB, 64KB, 128KB+ |
| 2301 | * attributes: { service: "api-gateway" } |
| 2302 | * }) |
| 2303 | * |
| 2304 | * // Create a histogram with custom boundaries |
| 2305 | * const customHistogram = Metric.histogram("custom_metric", { |
| 2306 | * description: "Custom distribution metric", |
| 2307 | * boundaries: [0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100] |
| 2308 | * }) |
| 2309 | * |
| 2310 | * // Record various response times |
| 2311 | * yield* Metric.update(responseTimeHistogram, 25) // Goes in 0-50ms bucket |
| 2312 | * yield* Metric.update(responseTimeHistogram, 75) // Goes in 50-100ms bucket |
| 2313 | * yield* Metric.update(responseTimeHistogram, 125) // Goes in 100-150ms bucket |
| 2314 | * yield* Metric.update(responseTimeHistogram, 200) // Goes in 150-200ms bucket |
| 2315 | * yield* Metric.update(responseTimeHistogram, 75) // Another 50-100ms |
| 2316 | * |
| 2317 | * // Record payload sizes |
| 2318 | * yield* Metric.update(payloadSizeHistogram, 3) // Goes in 2-4KB bucket |
| 2319 | * yield* Metric.update(payloadSizeHistogram, 15) // Goes in 8-16KB bucket |
| 2320 | * yield* Metric.update(payloadSizeHistogram, 0.5) // Goes in 0-1KB bucket |
| 2321 | * |
| 2322 | * // Get histogram state with distribution data |
| 2323 | * const responseTimeState = yield* Metric.value(responseTimeHistogram) |
| 2324 | * const payloadSizeState = yield* Metric.value(payloadSizeHistogram) |
| 2325 | * |
| 2326 | * // responseTimeState will contain: |
| 2327 | * // - buckets: [[50, 1], [100, 3], [150, 4], [200, 5], ...] |
| 2328 | * // - count: 5, min: 25, max: 200, sum: 500 |
| 2329 | * // - Useful for calculating percentiles, averages, etc. |
| 2330 | * |
| 2331 | * return { responseTimeState, payloadSizeState } |
| 2332 | * }) |
| 2333 | * |
| 2334 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2335 | * const values = [result.responseTimeState.count, result.responseTimeState.sum, result.payloadSizeState.count] |
| 2336 | * values // => [5, 500, 3] |
| 2337 | * ``` |
| 2338 | * |
| 2339 | * @category constructors |
| 2340 | * @since 2.0.0 |
| 2341 | */ |
| 2342 | export const histogram = (name: string, options: { |
| 2343 | readonly description?: string | undefined |
| 2344 | readonly attributes?: Metric.Attributes | undefined |
| 2345 | readonly boundaries: ReadonlyArray<number> |
| 2346 | }): Histogram<number> => new HistogramMetric(name, options) |
| 2347 | |
| 2348 | /** |
| 2349 | * Creates a `Summary` metric that records observations and calculates quantiles |
| 2350 | * which takes a value as input and uses the current time. |
| 2351 | * |
| 2352 | * **When to use** |
| 2353 | * |
| 2354 | * Use when you need a metric that records statistical information about a set |
| 2355 | * of values, including quantiles. |
| 2356 | * |
| 2357 | * **Details** |
| 2358 | * |
| 2359 | * The optional `description` describes the summary, and `attributes` attach |
| 2360 | * dimensions to it. `maxAge` controls how long observations are retained, |
| 2361 | * `maxSize` controls how many observations are kept, and `quantiles` lists the |
| 2362 | * quantiles to calculate, such as `[0.5, 0.9]`. |
| 2363 | * |
| 2364 | * **Example** (Creating summary metrics) |
| 2365 | * |
| 2366 | * ```ts import.meta.vitest |
| 2367 | * import { Duration, Effect, Metric } from "effect" |
| 2368 | * |
| 2369 | * const program = Effect.gen(function*() { |
| 2370 | * // Create a summary for API response times |
| 2371 | * const responseTimeSummary = Metric.summary("api_response_time", { |
| 2372 | * description: "API response time quantiles over 5-minute windows", |
| 2373 | * maxAge: Duration.minutes(5), // Keep observations for 5 minutes |
| 2374 | * maxSize: 1000, // Maximum 1000 observations in memory |
| 2375 | * quantiles: [0.5, 0.9, 0.95, 0.99] // 50th, 90th, 95th, 99th percentiles |
| 2376 | * }) |
| 2377 | * |
| 2378 | * // Create a summary for request payload sizes |
| 2379 | * const payloadSizeSummary = Metric.summary("request_payload_size", { |
| 2380 | * description: "Request payload size distribution over 2-minute windows", |
| 2381 | * maxAge: Duration.minutes(2), // Shorter window for recent trends |
| 2382 | * maxSize: 500, // Smaller buffer for memory efficiency |
| 2383 | * quantiles: [0.5, 0.75, 0.9], // Median, 75th, 90th percentiles |
| 2384 | * attributes: { service: "upload-service" } |
| 2385 | * }) |
| 2386 | * |
| 2387 | * // Record deterministic response times |
| 2388 | * const responseTimes = [82, 96, 104, 118, 135, 170, 210, 240] |
| 2389 | * for (const responseTime of responseTimes) { |
| 2390 | * yield* Metric.update(responseTimeSummary, responseTime) |
| 2391 | * } |
| 2392 | * |
| 2393 | * // Record some payload sizes |
| 2394 | * yield* Metric.update(payloadSizeSummary, 1.2) // 1.2KB |
| 2395 | * yield* Metric.update(payloadSizeSummary, 5.8) // 5.8KB |
| 2396 | * yield* Metric.update(payloadSizeSummary, 15.6) // 15.6KB |
| 2397 | * yield* Metric.update(payloadSizeSummary, 3.4) // 3.4KB |
| 2398 | * |
| 2399 | * // Get summary statistics with quantiles |
| 2400 | * const responseStats = yield* Metric.value(responseTimeSummary) |
| 2401 | * const payloadStats = yield* Metric.value(payloadSizeSummary) |
| 2402 | * |
| 2403 | * // Both summaries include quantile information for their configured windows. |
| 2404 | * |
| 2405 | * return { responseStats, payloadStats } |
| 2406 | * }) |
| 2407 | * |
| 2408 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2409 | * const response = result.responseStats |
| 2410 | * const payload = result.payloadStats |
| 2411 | * const responseValues = [response.count, response.min, response.max, response.sum] // => [8, 82, 240, 1155] |
| 2412 | * const payloadValues = [payload.count, payload.min, payload.max, payload.sum] // => [4, 1.2, 15.6, 26] |
| 2413 | * ``` |
| 2414 | * |
| 2415 | * @category constructors |
| 2416 | * @since 2.0.0 |
| 2417 | */ |
| 2418 | export const summary = (name: string, options: { |
| 2419 | readonly description?: string | undefined |
| 2420 | readonly attributes?: Metric.Attributes | undefined |
| 2421 | readonly maxAge: Duration.Input |
| 2422 | readonly maxSize: number |
| 2423 | readonly quantiles: ReadonlyArray<number> |
| 2424 | }): Summary<number> => |
| 2425 | mapInput(summaryWithTimestamp(name, options), (input, context) => |
| 2426 | [ |
| 2427 | input, |
| 2428 | Context.get(context, InternalEffect.ClockRef).currentTimeMillisUnsafe() |
| 2429 | ] as [number, number]) |
| 2430 | |
| 2431 | /** |
| 2432 | * Creates a `Summary` metric that records observations with explicit |
| 2433 | * timestamps and calculates quantiles. |
| 2434 | * |
| 2435 | * **When to use** |
| 2436 | * |
| 2437 | * Use when you need a metric that records statistical information about a set |
| 2438 | * of values together with timestamps. |
| 2439 | * |
| 2440 | * **Details** |
| 2441 | * |
| 2442 | * Inputs to this metric are `[value, timestamp]` pairs; the current clock is |
| 2443 | * used when reading quantiles against the configured `maxAge`. |
| 2444 | * |
| 2445 | * The optional `description` describes the summary, and `attributes` attach |
| 2446 | * dimensions to it. `maxAge` controls how long observations are retained, |
| 2447 | * `maxSize` controls how many observations are kept, and `quantiles` lists the |
| 2448 | * quantiles to calculate, such as `[0.5, 0.9]`. |
| 2449 | * |
| 2450 | * **Example** (Creating summaries with explicit timestamps) |
| 2451 | * |
| 2452 | * ```ts import.meta.vitest |
| 2453 | * import { Metric } from "effect" |
| 2454 | * |
| 2455 | * const responseTimesSummary = Metric.summaryWithTimestamp( |
| 2456 | * "response_times_summary", |
| 2457 | * { |
| 2458 | * description: "Measures the distribution of response times", |
| 2459 | * maxAge: "60 seconds", // Retain observations for 60 seconds. |
| 2460 | * maxSize: 1000, // Keep a maximum of 1000 observations. |
| 2461 | * quantiles: [0.5, 0.9, 0.99] // Calculate 50th, 90th, and 99th quantiles. |
| 2462 | * } |
| 2463 | * ) |
| 2464 | * const metadata = [responseTimesSummary.id, responseTimesSummary.type] // => ["response_times_summary", "Summary"] |
| 2465 | * ``` |
| 2466 | * |
| 2467 | * @category constructors |
| 2468 | * @since 4.0.0 |
| 2469 | */ |
| 2470 | export const summaryWithTimestamp = (name: string, options: { |
| 2471 | readonly description?: string | undefined |
| 2472 | readonly attributes?: Metric.Attributes | undefined |
| 2473 | readonly maxAge: Duration.Input |
| 2474 | readonly maxSize: number |
| 2475 | readonly quantiles: ReadonlyArray<number> |
| 2476 | }): Summary<[value: number, timestamp: number]> => new SummaryMetric(name, options) |
| 2477 | |
| 2478 | /** |
| 2479 | * Creates a timer metric, based on a `Histogram`, which keeps track of |
| 2480 | * durations in milliseconds. |
| 2481 | * |
| 2482 | * **Details** |
| 2483 | * |
| 2484 | * The unit of time will automatically be added to the metric as a tag (i.e. |
| 2485 | * `"time_unit: milliseconds"`). |
| 2486 | * |
| 2487 | * If `options.boundaries` is not provided, the boundaries will be computed |
| 2488 | * using `Metric.exponentialBoundaries({ start: 0.5, factor: 2, count: 35 })`. |
| 2489 | * |
| 2490 | * **Example** (Recording durations with a timer) |
| 2491 | * |
| 2492 | * ```ts import.meta.vitest |
| 2493 | * import { Duration, Effect, Metric } from "effect" |
| 2494 | * |
| 2495 | * // Create a timer metric to track API request durations |
| 2496 | * const apiRequestTimer = Metric.timer("api_request_duration", { |
| 2497 | * description: "Duration of API requests", |
| 2498 | * attributes: { service: "user-api" } |
| 2499 | * }) |
| 2500 | * |
| 2501 | * // Record a measured API operation duration |
| 2502 | * const apiOperation = Effect.gen(function*() { |
| 2503 | * const duration = Duration.millis(120) |
| 2504 | * yield* Metric.update(apiRequestTimer, duration) |
| 2505 | * |
| 2506 | * const state = yield* Metric.value(apiRequestTimer) |
| 2507 | * return { |
| 2508 | * count: state.count, |
| 2509 | * min: state.min, |
| 2510 | * max: state.max, |
| 2511 | * sum: state.sum |
| 2512 | * } |
| 2513 | * }) |
| 2514 | * |
| 2515 | * await Effect.runPromise( |
| 2516 | * Effect.provideService(apiOperation, Metric.MetricRegistry, new Map()) |
| 2517 | * ) // => { count: 1, min: 120, max: 120, sum: 120 } |
| 2518 | * ``` |
| 2519 | * |
| 2520 | * @category constructors |
| 2521 | * @since 2.0.0 |
| 2522 | */ |
| 2523 | export const timer = (name: string, options?: { |
| 2524 | readonly description?: string | undefined |
| 2525 | readonly attributes?: Metric.Attributes | undefined |
| 2526 | readonly boundaries?: ReadonlyArray<number> |
| 2527 | }): Histogram<Duration.Duration> => { |
| 2528 | const boundaries = Predicate.isNotUndefined(options?.boundaries) |
| 2529 | ? options.boundaries |
| 2530 | : exponentialBoundaries({ start: 0.5, factor: 2, count: 35 }) |
| 2531 | const attributes = mergeAttributes(options?.attributes, { time_unit: "milliseconds" }) |
| 2532 | const metric = new HistogramMetric(name, { ...options, boundaries, attributes }) |
| 2533 | return mapInput(metric, Duration.toMillis) |
| 2534 | } |
| 2535 | |
| 2536 | /** |
| 2537 | * Retrieves the current state of the specified `Metric`. |
| 2538 | * |
| 2539 | * **Details** |
| 2540 | * |
| 2541 | * The returned state depends on the metric type. Counters return |
| 2542 | * `CounterState<number | bigint>` with `count` and `incremental`, gauges return |
| 2543 | * `GaugeState<number | bigint>` with `value`, frequencies return |
| 2544 | * `FrequencyState` with `occurrences`, histograms return `HistogramState` with |
| 2545 | * buckets, count, min, max, and sum, and summaries return `SummaryState` with |
| 2546 | * quantiles, count, min, max, and sum. |
| 2547 | * |
| 2548 | * **Example** (Reading metric state) |
| 2549 | * |
| 2550 | * ```ts import.meta.vitest |
| 2551 | * import { Effect, Metric } from "effect" |
| 2552 | * |
| 2553 | * const requestCounter = Metric.counter("modify_requests") |
| 2554 | * const responseTime = Metric.histogram("response_time", { |
| 2555 | * boundaries: [100, 500, 1000, 2000] |
| 2556 | * }) |
| 2557 | * |
| 2558 | * const program = Effect.gen(function*() { |
| 2559 | * // Update metrics |
| 2560 | * yield* Metric.update(requestCounter, 1) |
| 2561 | * yield* Metric.update(responseTime, 750) |
| 2562 | * |
| 2563 | * // Get current values |
| 2564 | * const counterState = yield* Metric.value(requestCounter) |
| 2565 | * const histogramState = yield* Metric.value(responseTime) |
| 2566 | * return { |
| 2567 | * requestCount: counterState.count, |
| 2568 | * count: histogramState.count, |
| 2569 | * min: histogramState.min, |
| 2570 | * max: histogramState.max, |
| 2571 | * average: histogramState.sum / histogramState.count |
| 2572 | * } |
| 2573 | * }) |
| 2574 | * |
| 2575 | * await Effect.runPromise( |
| 2576 | * Effect.provideService(program, Metric.MetricRegistry, new Map()) |
| 2577 | * ) // => { requestCount: 1, count: 1, min: 750, max: 750, average: 750 } |
| 2578 | * ``` |
| 2579 | * |
| 2580 | * @category getters |
| 2581 | * @since 2.0.0 |
| 2582 | */ |
| 2583 | export const value = <Input, State>( |
| 2584 | self: Metric<Input, State> |
| 2585 | ): Effect<State> => |
| 2586 | InternalEffect.flatMap( |
| 2587 | InternalEffect.context(), |
| 2588 | (context) => InternalEffect.sync(() => self.valueUnsafe(context)) |
| 2589 | ) |
| 2590 | |
| 2591 | /** |
| 2592 | * Modifies the metric with the specified input. |
| 2593 | * |
| 2594 | * **Details** |
| 2595 | * |
| 2596 | * The behavior of `modify` depends on the metric type. Counters add the input |
| 2597 | * value to the current count, gauges add the input value to the current gauge |
| 2598 | * value, frequencies increment the occurrence count for the input string, |
| 2599 | * histograms record the input value in the appropriate bucket, and summaries |
| 2600 | * record the input observation. |
| 2601 | * |
| 2602 | * **Example** (Modifying metric values) |
| 2603 | * |
| 2604 | * ```ts import.meta.vitest |
| 2605 | * import { Effect, Metric } from "effect" |
| 2606 | * |
| 2607 | * const temperatureGauge = Metric.gauge("temperature") |
| 2608 | * const requestCounter = Metric.counter("requests") |
| 2609 | * |
| 2610 | * const program = Effect.gen(function*() { |
| 2611 | * // Set initial temperature |
| 2612 | * yield* Metric.update(temperatureGauge, 20) |
| 2613 | * |
| 2614 | * // Modify by adding/subtracting values |
| 2615 | * yield* Metric.modify(temperatureGauge, 5) // Now 25 |
| 2616 | * yield* Metric.modify(temperatureGauge, -3) // Now 22 |
| 2617 | * |
| 2618 | * // For counters, modify increments by the specified amount |
| 2619 | * yield* Metric.modify(requestCounter, 10) // Add 10 to counter |
| 2620 | * yield* Metric.modify(requestCounter, 5) // Add 5 more (total: 15) |
| 2621 | * |
| 2622 | * const temp = yield* Metric.value(temperatureGauge) |
| 2623 | * const requests = yield* Metric.value(requestCounter) |
| 2624 | * return [temp.value, requests.count] as const |
| 2625 | * }) |
| 2626 | * |
| 2627 | * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [22, 15] |
| 2628 | * ``` |
| 2629 | * |
| 2630 | * @category mutations |
| 2631 | * @since 3.6.5 |
| 2632 | */ |
| 2633 | export const modify: { |
| 2634 | <Input>(input: Input): <State>(self: Metric<Input, State>) => Effect<void> |
| 2635 | <Input, State>(self: Metric<Input, State>, input: Input): Effect<void> |
| 2636 | } = dual< |
| 2637 | <Input>(input: Input) => <State>(self: Metric<Input, State>) => Effect<void>, |
| 2638 | <Input, State>(self: Metric<Input, State>, input: Input) => Effect<void> |
| 2639 | >(2, (self, input) => |
| 2640 | InternalEffect.flatMap( |
| 2641 | InternalEffect.context(), |
| 2642 | (context) => InternalEffect.sync(() => self.modifyUnsafe(input, context)) |
| 2643 | )) |
| 2644 | |
| 2645 | /** |
| 2646 | * Updates the metric with the specified input. |
| 2647 | * |
| 2648 | * **Details** |
| 2649 | * |
| 2650 | * The behavior of `update` depends on the metric type. Counters add the input |
| 2651 | * value to the current count, gauges replace the current value with the input |
| 2652 | * value, frequencies increment the occurrence count for the input string, |
| 2653 | * histograms record the input value in the appropriate bucket, and summaries |
| 2654 | * record the input value as a new observation. |
| 2655 | * |
| 2656 | * **Example** (Updating metric values) |
| 2657 | * |
| 2658 | * ```ts import.meta.vitest |
| 2659 | * import { Effect, Metric } from "effect" |
| 2660 | * |
| 2661 | * const cpuUsage = Metric.gauge("cpu_usage_percent") |
| 2662 | * const httpStatus = Metric.frequency("http_status_codes") |
| 2663 | * const responseTime = Metric.histogram("response_time_ms", { |
| 2664 | * boundaries: [100, 500, 1000, 2000] |
| 2665 | * }) |
| 2666 | * |
| 2667 | * const program = Effect.gen(function*() { |
| 2668 | * // Update gauge to specific values |
| 2669 | * yield* Metric.update(cpuUsage, 45.2) |
| 2670 | * yield* Metric.update(cpuUsage, 67.8) // Replaces previous value |
| 2671 | * |
| 2672 | * // Track HTTP status code occurrences |
| 2673 | * yield* Metric.update(httpStatus, "200") |
| 2674 | * yield* Metric.update(httpStatus, "404") |
| 2675 | * yield* Metric.update(httpStatus, "200") // Increments 200 count |
| 2676 | * |
| 2677 | * // Record response times |
| 2678 | * yield* Metric.update(responseTime, 250) |
| 2679 | * yield* Metric.update(responseTime, 750) |
| 2680 | * yield* Metric.update(responseTime, 1500) |
| 2681 | * |
| 2682 | * // Check current states |
| 2683 | * const cpu = yield* Metric.value(cpuUsage) |
| 2684 | * const statuses = yield* Metric.value(httpStatus) |
| 2685 | * const times = yield* Metric.value(responseTime) |
| 2686 | * return [cpu.value, statuses.occurrences.get("200"), times.count] as const |
| 2687 | * }) |
| 2688 | * |
| 2689 | * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [67.8, 2, 3] |
| 2690 | * ``` |
| 2691 | * |
| 2692 | * @category mutations |
| 2693 | * @since 2.0.0 |
| 2694 | */ |
| 2695 | export const update: { |
| 2696 | <Input>(input: Input): <State>(self: Metric<Input, State>) => Effect<void> |
| 2697 | <Input, State>(self: Metric<Input, State>, input: Input): Effect<void> |
| 2698 | } = dual< |
| 2699 | <Input>(input: Input) => <State>(self: Metric<Input, State>) => Effect<void>, |
| 2700 | <Input, State>(self: Metric<Input, State>, input: Input) => Effect<void> |
| 2701 | >( |
| 2702 | 2, |
| 2703 | (self, input) => |
| 2704 | InternalEffect.contextWith((services) => InternalEffect.sync(() => self.updateUnsafe(input, services))) |
| 2705 | ) |
| 2706 | |
| 2707 | /** |
| 2708 | * Returns a new metric that is powered by this one, but which accepts updates |
| 2709 | * of the specified new type, which must be transformable to the input type of |
| 2710 | * this metric. |
| 2711 | * |
| 2712 | * **Example** (Mapping metric inputs) |
| 2713 | * |
| 2714 | * ```ts import.meta.vitest |
| 2715 | * import { Effect, Metric } from "effect" |
| 2716 | * |
| 2717 | * const durationHistogram = Metric.histogram("request_duration_ms", { |
| 2718 | * description: "Request duration in milliseconds", |
| 2719 | * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) |
| 2720 | * }) |
| 2721 | * |
| 2722 | * // Accept duration strings while recording numeric milliseconds |
| 2723 | * const durationStringHistogram = Metric.mapInput( |
| 2724 | * durationHistogram, |
| 2725 | * (input: string) => Number(input) |
| 2726 | * ) |
| 2727 | * |
| 2728 | * const program = Effect.gen(function*() { |
| 2729 | * yield* Metric.update(durationStringHistogram, "250") |
| 2730 | * return yield* Metric.value(durationStringHistogram) |
| 2731 | * }) |
| 2732 | * |
| 2733 | * const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2734 | * const values = [value.count, value.sum] // => [1, 250] |
| 2735 | * ``` |
| 2736 | * |
| 2737 | * @category annotations |
| 2738 | * @since 2.0.0 |
| 2739 | */ |
| 2740 | export const mapInput: { |
| 2741 | <Input, Input2 extends Input>( |
| 2742 | f: (input: Input2, context: Context.Context<never>) => Input |
| 2743 | ): <State>(self: Metric<Input, State>) => Metric<Input2, State> |
| 2744 | <Input, State, Input2>( |
| 2745 | self: Metric<Input, State>, |
| 2746 | f: (input: Input2, context: Context.Context<never>) => Input |
| 2747 | ): Metric<Input2, State> |
| 2748 | } = dual< |
| 2749 | <Input, Input2 extends Input>( |
| 2750 | f: (input: Input2, context: Context.Context<never>) => Input |
| 2751 | ) => <State>(self: Metric<Input, State>) => Metric<Input2, State>, |
| 2752 | <Input, State, Input2>( |
| 2753 | self: Metric<Input, State>, |
| 2754 | f: (input: Input2, context: Context.Context<never>) => Input |
| 2755 | ) => Metric<Input2, State> |
| 2756 | >(2, <Input, State, Input2>( |
| 2757 | self: Metric<Input, State>, |
| 2758 | f: (input: Input2, context: Context.Context<never>) => Input |
| 2759 | ): Metric<Input2, State> => |
| 2760 | new MetricTransform( |
| 2761 | self, |
| 2762 | (context) => self.valueUnsafe(context), |
| 2763 | (input, context) => self.updateUnsafe(f(input, context), context), |
| 2764 | (input, context) => self.modifyUnsafe(f(input, context), context) |
| 2765 | )) |
| 2766 | |
| 2767 | /** |
| 2768 | * Returns a new metric that is powered by this one, but which accepts updates |
| 2769 | * of any type, and translates them to updates with the specified constant |
| 2770 | * update value. |
| 2771 | * |
| 2772 | * **Example** (Ignoring inputs with a constant value) |
| 2773 | * |
| 2774 | * ```ts import.meta.vitest |
| 2775 | * import { Effect, Metric } from "effect" |
| 2776 | * |
| 2777 | * // Create a counter that normally expects a number increment |
| 2778 | * const requestCounter = Metric.counter("total_requests", { |
| 2779 | * description: "Total number of requests processed" |
| 2780 | * }) |
| 2781 | * |
| 2782 | * // Create a version that always increments by 1, regardless of input |
| 2783 | * const simpleRequestCounter = Metric.withConstantInput(requestCounter, 1) |
| 2784 | * |
| 2785 | * const program = Effect.gen(function*() { |
| 2786 | * // These all increment the counter by 1, ignoring the input value |
| 2787 | * yield* Metric.update(simpleRequestCounter, "any string") |
| 2788 | * yield* Metric.update(simpleRequestCounter, { complex: "object" }) |
| 2789 | * yield* Metric.update(simpleRequestCounter, 999) // Still increments by 1 |
| 2790 | * |
| 2791 | * const value = yield* Metric.value(simpleRequestCounter) |
| 2792 | * return value // Counter state will show count: 3 |
| 2793 | * }) |
| 2794 | * |
| 2795 | * const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 2796 | * const count = value.count // => 3 |
| 2797 | * ``` |
| 2798 | * |
| 2799 | * @category mapping |
| 2800 | * @since 2.0.0 |
| 2801 | */ |
| 2802 | export const withConstantInput: { |
| 2803 | <Input>(input: Input): <State>(self: Metric<Input, State>) => Metric<unknown, State> |
| 2804 | <Input, State>(self: Metric<Input, State>, input: Input): Metric<unknown, State> |
| 2805 | } = dual< |
| 2806 | <Input>(input: Input) => <State>(self: Metric<Input, State>) => Metric<unknown, State>, |
| 2807 | <Input, State>(self: Metric<Input, State>, input: Input) => Metric<unknown, State> |
| 2808 | >(2, (self, input) => mapInput(self, () => input)) |
| 2809 | |
| 2810 | /** |
| 2811 | * Returns a new metric that applies the specified attributes to all operations. |
| 2812 | * |
| 2813 | * **Details** |
| 2814 | * |
| 2815 | * Attributes are key-value pairs that provide additional context for metrics, |
| 2816 | * enabling filtering, grouping, and more detailed analysis. Each combination |
| 2817 | * of attribute values creates a separate metric series. |
| 2818 | * |
| 2819 | * **Example** (Applying metric attributes) |
| 2820 | * |
| 2821 | * ```ts import.meta.vitest |
| 2822 | * import { Effect, Metric } from "effect" |
| 2823 | * |
| 2824 | * const requestCounter = Metric.counter("http_requests_total", { |
| 2825 | * description: "Total HTTP requests" |
| 2826 | * }) |
| 2827 | * |
| 2828 | * // Create tagged versions of the metric |
| 2829 | * const getRequests = Metric.withAttributes(requestCounter, { |
| 2830 | * method: "GET", |
| 2831 | * endpoint: "/api/users" |
| 2832 | * }) |
| 2833 | * |
| 2834 | * const postRequests = Metric.withAttributes(requestCounter, { |
| 2835 | * method: "POST", |
| 2836 | * endpoint: "/api/users" |
| 2837 | * }) |
| 2838 | * |
| 2839 | * const program = Effect.gen(function*() { |
| 2840 | * // These will be tracked as separate metric series |
| 2841 | * yield* Metric.update(getRequests, 1) // http_requests_total{method="GET", endpoint="/api/users"} |
| 2842 | * yield* Metric.update(postRequests, 1) // http_requests_total{method="POST", endpoint="/api/users"} |
| 2843 | * yield* Metric.update(getRequests, 1) // Increments the GET counter |
| 2844 | * |
| 2845 | * // You can also chain attributes |
| 2846 | * const taggedMetric = requestCounter.pipe( |
| 2847 | * Metric.withAttributes({ service: "user-api" }), |
| 2848 | * Metric.withAttributes({ version: "v1" }) |
| 2849 | * ) |
| 2850 | * |
| 2851 | * yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"} |
| 2852 | * }) |
| 2853 | * |
| 2854 | * const result = Effect.gen(function*() { |
| 2855 | * yield* program |
| 2856 | * const get = yield* Metric.value(getRequests) |
| 2857 | * const post = yield* Metric.value(postRequests) |
| 2858 | * return [get.count, post.count] as const |
| 2859 | * }) |
| 2860 | * |
| 2861 | * await Effect.runPromise(Effect.provideService(result, Metric.MetricRegistry, new Map())) // => [2, 1] |
| 2862 | * ``` |
| 2863 | * |
| 2864 | * @category mapping |
| 2865 | * @since 4.0.0 |
| 2866 | */ |
| 2867 | export const withAttributes: { |
| 2868 | (attributes: Metric.Attributes): <Input, State>(self: Metric<Input, State>) => Metric<Input, State> |
| 2869 | <Input, State>(self: Metric<Input, State>, attributes: Metric.Attributes): Metric<Input, State> |
| 2870 | } = dual< |
| 2871 | (attributes: Metric.Attributes) => <Input, State>(self: Metric<Input, State>) => Metric<Input, State>, |
| 2872 | <Input, State>(self: Metric<Input, State>, attributes: Metric.Attributes) => Metric<Input, State> |
| 2873 | >(2, <Input, State>( |
| 2874 | self: Metric<Input, State>, |
| 2875 | attributes: Metric.Attributes |
| 2876 | ): Metric<Input, State> => { |
| 2877 | const attributed = addAttributesToContext(attributes) |
| 2878 | return new MetricTransform( |
| 2879 | self, |
| 2880 | (context) => self.valueUnsafe(attributed(context)), |
| 2881 | (input, context) => self.updateUnsafe(input, attributed(context)), |
| 2882 | (input, context) => self.modifyUnsafe(input, attributed(context)) |
| 2883 | ) |
| 2884 | }) |
| 2885 | |
| 2886 | // Metric Snapshots |
| 2887 | |
| 2888 | /** |
| 2889 | * Captures a snapshot of all registered metrics in the current context. |
| 2890 | * |
| 2891 | * **Details** |
| 2892 | * |
| 2893 | * Returns an array of metric snapshots, each containing the metric's metadata |
| 2894 | * (name, description, type) and current state (values, counts, etc.). |
| 2895 | * |
| 2896 | * **Example** (Capturing metric snapshots) |
| 2897 | * |
| 2898 | * ```ts import.meta.vitest |
| 2899 | * import { Effect, Metric } from "effect" |
| 2900 | * |
| 2901 | * const program = Effect.gen(function*() { |
| 2902 | * // Create and update some metrics |
| 2903 | * const requestCounter = Metric.counter("http_requests", { |
| 2904 | * description: "Total HTTP requests" |
| 2905 | * }) |
| 2906 | * const responseTime = Metric.histogram("response_time_ms", { |
| 2907 | * description: "Response time in milliseconds", |
| 2908 | * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 5 }) |
| 2909 | * }) |
| 2910 | * |
| 2911 | * // Update the metrics with some values |
| 2912 | * yield* Metric.update(requestCounter, 1) |
| 2913 | * yield* Metric.update(requestCounter, 1) |
| 2914 | * yield* Metric.update(responseTime, 150) |
| 2915 | * yield* Metric.update(responseTime, 75) |
| 2916 | * |
| 2917 | * // Take a snapshot of all metrics |
| 2918 | * const snapshots = yield* Metric.snapshot |
| 2919 | * |
| 2920 | * return snapshots |
| 2921 | * }) |
| 2922 | * |
| 2923 | * const snapshots = await Effect.runPromise( |
| 2924 | * Effect.provideService(program, Metric.MetricRegistry, new Map()) |
| 2925 | * ) |
| 2926 | * const ids = snapshots.map((snapshot) => snapshot.id).sort() // => ["http_requests", "response_time_ms"] |
| 2927 | * ``` |
| 2928 | * |
| 2929 | * @category snapshotting |
| 2930 | * @since 2.0.0 |
| 2931 | */ |
| 2932 | export const snapshot: Effect<ReadonlyArray<Metric.Snapshot>> = InternalEffect.map( |
| 2933 | InternalEffect.context(), |
| 2934 | (context) => snapshotUnsafe(context) |
| 2935 | ) |
| 2936 | |
| 2937 | /** |
| 2938 | * Returns a human-readable string representation of all currently registered |
| 2939 | * metrics in a tabular format. |
| 2940 | * |
| 2941 | * **Details** |
| 2942 | * |
| 2943 | * This debugging utility captures a snapshot of all metrics and formats them |
| 2944 | * in an easy-to-read table showing names, descriptions, types, attributes, |
| 2945 | * and current state values. |
| 2946 | * |
| 2947 | * **Example** (Dumping metrics as text) |
| 2948 | * |
| 2949 | * ```ts import.meta.vitest |
| 2950 | * import { Effect, Metric } from "effect" |
| 2951 | * |
| 2952 | * const program = Effect.gen(function*() { |
| 2953 | * // Create and update some metrics for demonstration |
| 2954 | * const requestCounter = Metric.counter("http_requests_total", { |
| 2955 | * description: "Total HTTP requests" |
| 2956 | * }) |
| 2957 | * const responseTime = Metric.gauge("response_time_ms", { |
| 2958 | * description: "Current response time in milliseconds" |
| 2959 | * }) |
| 2960 | * const statusFreq = Metric.frequency("http_status_codes", { |
| 2961 | * description: "Frequency of HTTP status codes" |
| 2962 | * }) |
| 2963 | * |
| 2964 | * // Update metrics with some values |
| 2965 | * yield* Metric.update(requestCounter, 1) |
| 2966 | * yield* Metric.update(requestCounter, 1) |
| 2967 | * yield* Metric.update(responseTime, 125) |
| 2968 | * yield* Metric.update(statusFreq, "200") |
| 2969 | * yield* Metric.update(statusFreq, "404") |
| 2970 | * yield* Metric.update(statusFreq, "200") |
| 2971 | * |
| 2972 | * // Get formatted dump of all metrics |
| 2973 | * const metricsReport = yield* Metric.dump |
| 2974 | * return metricsReport |
| 2975 | * }) |
| 2976 | * |
| 2977 | * const report = await Effect.runPromise( |
| 2978 | * Effect.provideService(program, Metric.MetricRegistry, new Map()) |
| 2979 | * ) |
| 2980 | * const included = [ |
| 2981 | * report.includes("http_requests_total"), |
| 2982 | * report.includes("response_time_ms"), |
| 2983 | * report.includes("http_status_codes") |
| 2984 | * ] |
| 2985 | * included // => [true, true, true] |
| 2986 | * ``` |
| 2987 | * |
| 2988 | * @category formatting |
| 2989 | * @since 4.0.0 |
| 2990 | */ |
| 2991 | export const dump: Effect<string> = InternalEffect.flatMap(InternalEffect.context(), (context) => { |
| 2992 | const metrics = snapshotUnsafe(context) |
| 2993 | if (metrics.length > 0) { |
| 2994 | const maxNameLength = metrics.reduce((max, metric) => { |
| 2995 | const length = metric.id.length |
| 2996 | return length > max ? length : max |
| 2997 | }, 0) + 2 |
| 2998 | const maxDescriptionLength = metrics.reduce((max, metric) => { |
| 2999 | const length = Predicate.isNotUndefined(metric.description) ? metric.description.length : 0 |
| 3000 | return length > max ? length : max |
| 3001 | }, 0) + 2 |
| 3002 | const maxTypeLength = metrics.reduce((max, metric) => { |
| 3003 | const length = metric.type.length |
| 3004 | return length > max ? length : max |
| 3005 | }, 0) + 2 |
| 3006 | const maxAttributesLength = metrics.reduce((max, metric) => { |
| 3007 | const length = Predicate.isNotUndefined(metric.attributes) ? attributesToString(metric.attributes).length : 0 |
| 3008 | return length > max ? length : max |
| 3009 | }, 0) + 2 |
| 3010 | const grouped = Object.entries(Arr.groupBy(metrics, (metric) => metric.id)) |
| 3011 | const sorted = Arr.sortWith(grouped, (entry) => entry[0], _String.Order) |
| 3012 | const rendered = sorted.map(([, group]) => |
| 3013 | group.map((metric) => |
| 3014 | renderName(metric, maxNameLength) + |
| 3015 | renderDescription(metric, maxDescriptionLength) + |
| 3016 | renderType(metric, maxTypeLength) + |
| 3017 | renderAttributes(metric, maxAttributesLength) + |
| 3018 | renderState(metric) |
| 3019 | ).join("\n") |
| 3020 | ).join("\n") |
| 3021 | return InternalEffect.succeed(rendered) |
| 3022 | } |
| 3023 | return InternalEffect.succeed("") |
| 3024 | }) |
| 3025 | |
| 3026 | /** |
| 3027 | * Captures a snapshot of all registered metrics synchronously using the provided |
| 3028 | * service context. |
| 3029 | * |
| 3030 | * **When to use** |
| 3031 | * |
| 3032 | * Use to read metric snapshots from an explicit `Context` in low-level |
| 3033 | * integrations, exporters, or debugging tools that already have the context. |
| 3034 | * |
| 3035 | * **Details** |
| 3036 | * |
| 3037 | * This is the "unsafe" version that bypasses Effect's safety guarantees and requires |
| 3038 | * manual handling of the services context. Use the safe `snapshot` function for normal |
| 3039 | * application code. |
| 3040 | * |
| 3041 | * **Example** (Capturing snapshots from a context) |
| 3042 | * |
| 3043 | * ```ts import.meta.vitest |
| 3044 | * import { Effect, Metric } from "effect" |
| 3045 | * |
| 3046 | * const requestCounter = Metric.counter("http_requests") |
| 3047 | * const program = Effect.gen(function*() { |
| 3048 | * yield* Metric.update(requestCounter, 1) |
| 3049 | * const context = yield* Effect.context() |
| 3050 | * return Metric.snapshotUnsafe(context).map((snapshot) => snapshot.id) |
| 3051 | * }) |
| 3052 | * |
| 3053 | * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => ["http_requests"] |
| 3054 | * ``` |
| 3055 | * |
| 3056 | * @category snapshotting |
| 3057 | * @since 4.0.0 |
| 3058 | */ |
| 3059 | export const snapshotUnsafe = (context: Context.Context<never>): ReadonlyArray<Metric.Snapshot> => { |
| 3060 | const registry = Context.get(context, MetricRegistry) |
| 3061 | return Array.from(registry.values()).map(({ hooks, ...meta }) => ({ |
| 3062 | ...meta, |
| 3063 | state: hooks.get(context) |
| 3064 | })) |
| 3065 | } |
| 3066 | |
| 3067 | const renderName = (metric: Metric.Snapshot, padTo: number): string => `name=${metric.id.padEnd(padTo, " ")}` |
| 3068 | |
| 3069 | const renderDescription = (metric: Metric.Snapshot, padTo: number): string => |
| 3070 | `description=${(metric.description ?? "").padEnd(padTo, " ")}` |
| 3071 | |
| 3072 | const renderType = (metric: Metric.Snapshot, padTo: number): string => `type=${metric.type.padEnd(padTo, " ")}` |
| 3073 | |
| 3074 | const renderAttributes = (metric: Metric.Snapshot, padTo: number): string => { |
| 3075 | const attrs = attributesToString(metric.attributes ?? {}) |
| 3076 | const padding = " ".repeat(Math.max(0, padTo - attrs.length)) |
| 3077 | return `${attrs}${padding}` |
| 3078 | } |
| 3079 | |
| 3080 | const renderState = (metric: Metric.Snapshot): string => { |
| 3081 | const prefix: string = "state=" |
| 3082 | switch (metric.type) { |
| 3083 | case "Counter": { |
| 3084 | const state = metric.state as CounterState<number | bigint> |
| 3085 | return `${prefix}[count: [${state.count}]]` |
| 3086 | } |
| 3087 | case "Frequency": { |
| 3088 | const state = metric.state as FrequencyState |
| 3089 | return `${prefix}[occurrences: ${renderKeyValues(state.occurrences)}]` |
| 3090 | } |
| 3091 | case "Gauge": { |
| 3092 | const state = metric.state as GaugeState<number | bigint> |
| 3093 | return `${prefix}[value: [${state.value}]]` |
| 3094 | } |
| 3095 | case "Histogram": { |
| 3096 | const state = metric.state as HistogramState |
| 3097 | const buckets = `buckets: [${renderKeyValues(state.buckets)}]` |
| 3098 | const count = `count: [${state.count}]` |
| 3099 | const min = `min: [${state.min}]` |
| 3100 | const max = `max: [${state.max}]` |
| 3101 | const sum = `sum: [${state.sum}]` |
| 3102 | return `${prefix}[${buckets}, ${count}, ${min}, ${max}, ${sum}]` |
| 3103 | } |
| 3104 | case "Summary": { |
| 3105 | const state = metric.state as SummaryState |
| 3106 | const printableQuantiles = state.quantiles.map(([key, value]) => [key, value ?? 0] as [number, number]) |
| 3107 | const quantiles = `quantiles: [${renderKeyValues(printableQuantiles)}]` |
| 3108 | const count = `count: [${state.count}]` |
| 3109 | const min = `min: [${state.min}]` |
| 3110 | const max = `max: [${state.max}]` |
| 3111 | const sum = `sum: [${state.sum}]` |
| 3112 | return `${prefix}[${quantiles}, ${count}, ${min}, ${max}, ${sum}]` |
| 3113 | } |
| 3114 | } |
| 3115 | } |
| 3116 | |
| 3117 | const renderKeyValues = (keyValues: Iterable<[number | string, string | number]>): string => |
| 3118 | Array.from(keyValues).map(([key, value]) => `(${key} -> ${value})`).join(", ") |
| 3119 | |
| 3120 | const attributesToString = (attributes: Metric.AttributeSet): string => { |
| 3121 | const attrs = Object.entries(attributes) |
| 3122 | const sorted = Arr.sortWith(attrs, (attr) => attr[0], _String.Order) |
| 3123 | return `attributes=[${sorted.map(([key, value]) => `${key}: ${value}`).join(", ")}]` |
| 3124 | } |
| 3125 | |
| 3126 | // Metric Boundaries |
| 3127 | |
| 3128 | /** |
| 3129 | * Creates histogram bucket boundaries from an iterable set of values. |
| 3130 | * |
| 3131 | * **Details** |
| 3132 | * |
| 3133 | * Processes any iterable of numbers by removing duplicates, filtering out |
| 3134 | * non-positive values, and automatically appending positive infinity as the |
| 3135 | * final boundary. |
| 3136 | * |
| 3137 | * **Example** (Creating boundaries from values) |
| 3138 | * |
| 3139 | * ```ts import.meta.vitest |
| 3140 | * import { Metric } from "effect" |
| 3141 | * |
| 3142 | * Metric.boundariesFromIterable([-5, 0, 10, 10, 25, 50]) // => [10, 25, 50, Infinity] |
| 3143 | * ``` |
| 3144 | * |
| 3145 | * @category constructors |
| 3146 | * @since 4.0.0 |
| 3147 | */ |
| 3148 | export const boundariesFromIterable = (iterable: Iterable<number>): ReadonlyArray<number> => |
| 3149 | Arr.append(Arr.filter(new Set(iterable), (n) => n > 0), Number.POSITIVE_INFINITY) |
| 3150 | |
| 3151 | /** |
| 3152 | * Creates histogram bucket boundaries from a linear sequence and appends |
| 3153 | * positive infinity. |
| 3154 | * |
| 3155 | * **Details** |
| 3156 | * |
| 3157 | * Generates `count - 1` candidate boundaries using `start + index * width` for |
| 3158 | * each zero-based index, then applies the same normalization as |
| 3159 | * `boundariesFromIterable`: non-positive values are removed, duplicates are |
| 3160 | * collapsed, and `Infinity` is appended. |
| 3161 | * |
| 3162 | * **Example** (Creating linear boundaries) |
| 3163 | * |
| 3164 | * ```ts import.meta.vitest |
| 3165 | * import { Metric } from "effect" |
| 3166 | * |
| 3167 | * Metric.linearBoundaries({ start: 10, width: 20, count: 5 }) // => [10, 30, 50, 70, Infinity] |
| 3168 | * ``` |
| 3169 | * |
| 3170 | * @category constructors |
| 3171 | * @since 4.0.0 |
| 3172 | */ |
| 3173 | export const linearBoundaries = (options: { |
| 3174 | readonly start: number |
| 3175 | readonly width: number |
| 3176 | readonly count: number |
| 3177 | }): ReadonlyArray<number> => |
| 3178 | boundariesFromIterable(Arr.makeBy(options.count - 1, (n) => options.start + n * options.width)) |
| 3179 | |
| 3180 | /** |
| 3181 | * Creates histogram bucket boundaries with exponentially increasing values. |
| 3182 | * |
| 3183 | * **Details** |
| 3184 | * |
| 3185 | * Creates boundaries that grow exponentially, useful for metrics that span |
| 3186 | * multiple orders of magnitude. Each boundary is calculated as start * factor^i. |
| 3187 | * |
| 3188 | * **Example** (Creating exponential boundaries) |
| 3189 | * |
| 3190 | * ```ts import.meta.vitest |
| 3191 | * import { Metric } from "effect" |
| 3192 | * |
| 3193 | * Metric.exponentialBoundaries({ start: 1, factor: 2, count: 5 }) // => [1, 2, 4, 8, Infinity] |
| 3194 | * ``` |
| 3195 | * |
| 3196 | * @category constructors |
| 3197 | * @since 4.0.0 |
| 3198 | */ |
| 3199 | export const exponentialBoundaries = (options: { |
| 3200 | readonly start: number |
| 3201 | readonly factor: number |
| 3202 | readonly count: number |
| 3203 | }): ReadonlyArray<number> => |
| 3204 | boundariesFromIterable(Arr.makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i))) |
| 3205 | |
| 3206 | // Fiber Runtime Metrics |
| 3207 | |
| 3208 | const fibersActive = gauge("child_fibers_active", { |
| 3209 | description: "The current count of active child fibers" |
| 3210 | }) |
| 3211 | const fibersStarted = counter("child_fibers_started", { |
| 3212 | description: "The total number of child fibers that have been started", |
| 3213 | incremental: true |
| 3214 | }) |
| 3215 | const fiberSuccesses = counter("child_fiber_successes", { |
| 3216 | description: "The total number of child fibers that have succeeded", |
| 3217 | incremental: true |
| 3218 | }) |
| 3219 | const fiberFailures = counter("child_fiber_failures", { |
| 3220 | description: "The total number of child fibers that have failed", |
| 3221 | incremental: true |
| 3222 | }) |
| 3223 | |
| 3224 | /** |
| 3225 | * Service key for the fiber runtime metrics service. |
| 3226 | * |
| 3227 | * **Example** (Accessing the fiber runtime metrics key) |
| 3228 | * |
| 3229 | * ```ts import.meta.vitest |
| 3230 | * import { Metric } from "effect" |
| 3231 | * |
| 3232 | * Metric.FiberRuntimeMetricsKey // => "effect/Metric/FiberRuntimeMetrics" |
| 3233 | * ``` |
| 3234 | * |
| 3235 | * @category constants |
| 3236 | * @since 4.0.0 |
| 3237 | */ |
| 3238 | export const FiberRuntimeMetricsKey: "effect/Metric/FiberRuntimeMetrics" = InternalMetric.FiberRuntimeMetricsKey |
| 3239 | |
| 3240 | /** |
| 3241 | * Interface for the fiber runtime metrics service that tracks fiber lifecycle events. |
| 3242 | * |
| 3243 | * **Example** (Providing a custom fiber metrics service) |
| 3244 | * |
| 3245 | * ```ts import.meta.vitest |
| 3246 | * import { Context, Exit, Metric } from "effect" |
| 3247 | * |
| 3248 | * const events: Array<string> = [] |
| 3249 | * const customMetricsService: Metric.FiberRuntimeMetricsService = { |
| 3250 | * recordFiberStart: () => { |
| 3251 | * events.push("start") |
| 3252 | * }, |
| 3253 | * recordFiberEnd: (_context, exit) => { |
| 3254 | * events.push(Exit.isSuccess(exit) ? "success" : "failure") |
| 3255 | * } |
| 3256 | * } |
| 3257 | * |
| 3258 | * customMetricsService.recordFiberStart(Context.empty()) |
| 3259 | * customMetricsService.recordFiberEnd(Context.empty(), Exit.succeed("ok")) |
| 3260 | * events // => ["start", "success"] |
| 3261 | * ``` |
| 3262 | * |
| 3263 | * @category services |
| 3264 | * @since 4.0.0 |
| 3265 | */ |
| 3266 | export interface FiberRuntimeMetricsService { |
| 3267 | readonly recordFiberStart: (context: Context.Context<never>) => void |
| 3268 | readonly recordFiberEnd: (context: Context.Context<never>, exit: Exit<unknown, unknown>) => void |
| 3269 | } |
| 3270 | |
| 3271 | /** |
| 3272 | * Context reference for the optional service that records fiber runtime |
| 3273 | * metrics. |
| 3274 | * |
| 3275 | * **When to use** |
| 3276 | * |
| 3277 | * Use to provide or inspect the service that receives fiber start and end |
| 3278 | * notifications for automatic runtime metrics. |
| 3279 | * |
| 3280 | * **Details** |
| 3281 | * |
| 3282 | * When provided, the runtime can notify the service about child-fiber start and |
| 3283 | * end events. When the reference is `undefined`, automatic fiber runtime metric |
| 3284 | * collection is disabled. |
| 3285 | * |
| 3286 | * **Example** (Accessing the fiber runtime metrics service) |
| 3287 | * |
| 3288 | * ```ts import.meta.vitest |
| 3289 | * import { Effect, Metric } from "effect" |
| 3290 | * |
| 3291 | * const program = Effect.gen(function*() { |
| 3292 | * const metricsService = yield* Metric.FiberRuntimeMetrics |
| 3293 | * return metricsService === Metric.FiberRuntimeMetricsImpl |
| 3294 | * }) |
| 3295 | * |
| 3296 | * const result = await Effect.runPromise( |
| 3297 | * Effect.provideService(program, Metric.FiberRuntimeMetrics, Metric.FiberRuntimeMetricsImpl) |
| 3298 | * ) |
| 3299 | * const isDefault = result // => true |
| 3300 | * ``` |
| 3301 | * |
| 3302 | * @category services |
| 3303 | * @since 4.0.0 |
| 3304 | */ |
| 3305 | export const FiberRuntimeMetrics = Context.Reference<FiberRuntimeMetricsService | undefined>( |
| 3306 | InternalMetric.FiberRuntimeMetricsKey, |
| 3307 | { fiberCached: true, defaultValue: constUndefined } |
| 3308 | ) |
| 3309 | |
| 3310 | /** |
| 3311 | * Default implementation of the fiber runtime metrics service. |
| 3312 | * |
| 3313 | * **Example** (Accessing the default fiber metrics implementation) |
| 3314 | * |
| 3315 | * ```ts import.meta.vitest |
| 3316 | * import { Metric } from "effect" |
| 3317 | * |
| 3318 | * [ |
| 3319 | * typeof Metric.FiberRuntimeMetricsImpl.recordFiberStart, |
| 3320 | * typeof Metric.FiberRuntimeMetricsImpl.recordFiberEnd |
| 3321 | * ] // => ["function", "function"] |
| 3322 | * ``` |
| 3323 | * |
| 3324 | * @category services |
| 3325 | * @since 4.0.0 |
| 3326 | */ |
| 3327 | export const FiberRuntimeMetricsImpl: FiberRuntimeMetricsService = { |
| 3328 | recordFiberStart(context: Context.Context<never>) { |
| 3329 | fibersStarted.updateUnsafe(1, context) |
| 3330 | fibersActive.modifyUnsafe(1, context) |
| 3331 | }, |
| 3332 | recordFiberEnd(context: Context.Context<never>, exit: Exit<unknown, unknown>) { |
| 3333 | fibersActive.modifyUnsafe(-1, context) |
| 3334 | if (InternalEffect.exitIsSuccess(exit)) { |
| 3335 | fiberSuccesses.updateUnsafe(1, context) |
| 3336 | } else { |
| 3337 | fiberFailures.updateUnsafe(1, context) |
| 3338 | } |
| 3339 | } |
| 3340 | } |
| 3341 | |
| 3342 | /** |
| 3343 | * Layer that enables automatic collection of fiber runtime metrics across |
| 3344 | * an entire Effect application. |
| 3345 | * |
| 3346 | * **When to use** |
| 3347 | * |
| 3348 | * Use when you need runtime metrics collection for all Effects in the |
| 3349 | * application context rather than wrapping individual Effects. |
| 3350 | * |
| 3351 | * **Example** (Enabling runtime metrics with a layer) |
| 3352 | * |
| 3353 | * ```ts import.meta.vitest |
| 3354 | * import { Effect, Metric } from "effect" |
| 3355 | * |
| 3356 | * const program = Effect.gen(function*() { |
| 3357 | * const service = yield* Metric.FiberRuntimeMetrics |
| 3358 | * return service === Metric.FiberRuntimeMetricsImpl |
| 3359 | * }) |
| 3360 | * |
| 3361 | * await Effect.runPromise(Effect.provide(program, Metric.enableRuntimeMetricsLayer)) // => true |
| 3362 | * ``` |
| 3363 | * |
| 3364 | * @category layers |
| 3365 | * @since 4.0.0 |
| 3366 | */ |
| 3367 | export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(FiberRuntimeMetricsImpl) |
| 3368 | |
| 3369 | /** |
| 3370 | * Layer that disables automatic collection of fiber runtime metrics. |
| 3371 | * |
| 3372 | * **Example** (Disabling runtime metrics with a layer) |
| 3373 | * |
| 3374 | * ```ts import.meta.vitest |
| 3375 | * import { Effect, Metric } from "effect" |
| 3376 | * |
| 3377 | * const program = Effect.gen(function*() { |
| 3378 | * // Disable runtime metrics collection |
| 3379 | * const disabledLayer = Metric.disableRuntimeMetricsLayer |
| 3380 | * |
| 3381 | * return yield* Effect.gen(function*() { |
| 3382 | * // Check that metrics service is disabled |
| 3383 | * const metricsService = yield* Metric.FiberRuntimeMetrics |
| 3384 | * |
| 3385 | * // Run some Effects - no metrics will be collected |
| 3386 | * yield* Effect.forkChild(Effect.sleep("50 millis")) |
| 3387 | * yield* Effect.forkChild(Effect.sleep("100 millis")) |
| 3388 | * yield* Effect.sleep("200 millis") |
| 3389 | * |
| 3390 | * // Create test metrics to show they still work |
| 3391 | * const testCounter = Metric.counter("test_counter") |
| 3392 | * yield* Metric.update(testCounter, 1) |
| 3393 | * const counterValue = yield* Metric.value(testCounter) |
| 3394 | * |
| 3395 | * return { counterValue, metricsEnabled: metricsService !== undefined } |
| 3396 | * }).pipe(Effect.provide(disabledLayer)) |
| 3397 | * }) |
| 3398 | * |
| 3399 | * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) |
| 3400 | * const values = [result.counterValue.count, result.metricsEnabled] // => [1, false] |
| 3401 | * ``` |
| 3402 | * |
| 3403 | * @category layers |
| 3404 | * @since 4.0.0 |
| 3405 | */ |
| 3406 | export const disableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(undefined) |
| 3407 | |
| 3408 | /** |
| 3409 | * Enables automatic collection of fiber runtime metrics for the provided Effect. |
| 3410 | * |
| 3411 | * **Details** |
| 3412 | * |
| 3413 | * When enabled, automatically tracks fiber lifecycle metrics including active fibers, |
| 3414 | * started fibers, successful completions, and failures. These metrics provide valuable |
| 3415 | * insights into the concurrency patterns and health of your Effect application. |
| 3416 | * |
| 3417 | * **Example** (Enabling runtime metrics for an effect) |
| 3418 | * |
| 3419 | * ```ts import.meta.vitest |
| 3420 | * import { Effect, Metric } from "effect" |
| 3421 | * |
| 3422 | * const program = Effect.gen(function*() { |
| 3423 | * const service = yield* Metric.FiberRuntimeMetrics |
| 3424 | * return service === Metric.FiberRuntimeMetricsImpl |
| 3425 | * }) |
| 3426 | * |
| 3427 | * await Effect.runPromise(Metric.enableRuntimeMetrics(program)) // => true |
| 3428 | * ``` |
| 3429 | * |
| 3430 | * @category providing services |
| 3431 | * @since 4.0.0 |
| 3432 | */ |
| 3433 | export const enableRuntimeMetrics: <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, R> = InternalEffect.provideService( |
| 3434 | FiberRuntimeMetrics, |
| 3435 | FiberRuntimeMetricsImpl |
| 3436 | ) |
| 3437 | |
| 3438 | /** |
| 3439 | * Disables automatic collection of fiber runtime metrics for the provided Effect. |
| 3440 | * |
| 3441 | * **When to use** |
| 3442 | * |
| 3443 | * Use when you need to disable runtime metrics for a specific effect while |
| 3444 | * keeping them enabled elsewhere. |
| 3445 | * |
| 3446 | * **Example** (Disabling runtime metrics for an effect) |
| 3447 | * |
| 3448 | * ```ts import.meta.vitest |
| 3449 | * import { Effect, Metric } from "effect" |
| 3450 | * |
| 3451 | * const program = Effect.gen(function*() { |
| 3452 | * const service = yield* Metric.FiberRuntimeMetrics |
| 3453 | * return service === undefined |
| 3454 | * }) |
| 3455 | * |
| 3456 | * await Effect.runPromise(Metric.disableRuntimeMetrics(program)) // => true |
| 3457 | * ``` |
| 3458 | * |
| 3459 | * @category providing services |
| 3460 | * @since 4.0.0 |
| 3461 | */ |
| 3462 | export const disableRuntimeMetrics: <A, E, R>(self: Effect<A, E, R>) => Effect<A, E, R> = InternalEffect.provideService( |
| 3463 | FiberRuntimeMetrics, |
| 3464 | undefined |
| 3465 | ) |
| 3466 | |
| 3467 | // Utilities |
| 3468 | |
| 3469 | function makeKey<Input, State>( |
| 3470 | metric: Metric<Input, State>, |
| 3471 | attributes: Metric.AttributeSet |
| 3472 | ): string { |
| 3473 | // Tuple encoding keeps ids, descriptions and attributes unambiguous. |
| 3474 | const entries = Object.entries(attributes).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0) |
| 3475 | return JSON.stringify([metric.type, metric.id, metric.description, entries]) |
| 3476 | } |
| 3477 | |
| 3478 | function makeHooks<Input, State>( |
| 3479 | get: (context: Context.Context<never>) => State, |
| 3480 | update: (input: Input, context: Context.Context<never>) => void, |
| 3481 | modify?: (input: Input, context: Context.Context<never>) => void |
| 3482 | ): Metric.Hooks<Input, State> { |
| 3483 | return { get, update, modify: modify ?? update } |
| 3484 | } |
| 3485 | |
| 3486 | function mergeAttributes( |
| 3487 | self: Metric.Attributes | undefined, |
| 3488 | other: Metric.Attributes | undefined |
| 3489 | ): Metric.AttributeSet { |
| 3490 | return { ...attributesToRecord(self), ...attributesToRecord(other) } |
| 3491 | } |
| 3492 | |
| 3493 | function attributesToRecord(attributes?: Metric.Attributes): Metric.AttributeSet | undefined { |
| 3494 | if (Predicate.isNotUndefined(attributes) && Array.isArray(attributes)) { |
| 3495 | return attributes.reduce((acc, [key, value]) => { |
| 3496 | InternalRecord.assignProperty(acc, key, value) |
| 3497 | return acc |
| 3498 | }, {} as Metric.AttributeSet) |
| 3499 | } |
| 3500 | return attributes as Metric.AttributeSet | undefined |
| 3501 | } |
| 3502 | |
| 3503 | function addAttributesToContext( |
| 3504 | attributes: Metric.Attributes |
| 3505 | ): (context: Context.Context<never>) => Context.Context<never> { |
| 3506 | // Merge once per contextual attribute set, so the wrapped metric can reuse its cached series key. |
| 3507 | let current: Metric.AttributeSet | undefined |
| 3508 | let merged: Metric.AttributeSet = {} |
| 3509 | return (context) => { |
| 3510 | const extraAttributes = Context.get(context, CurrentMetricAttributes) |
| 3511 | if (extraAttributes !== current) { |
| 3512 | current = extraAttributes |
| 3513 | merged = mergeAttributes(extraAttributes, attributes) |
| 3514 | } |
| 3515 | return Context.add(context, CurrentMetricAttributes, merged) |
| 3516 | } |
| 3517 | } |