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
14import * as Arr from "./Array.ts"
15import * as Context from "./Context.ts"
16import * as Duration from "./Duration.ts"
17import type { Effect } from "./Effect.ts"
18import type { Exit } from "./Exit.ts"
19import { constUndefined, dual } from "./Function.ts"
20import * as InternalEffect from "./internal/effect.ts"
21import * as InternalMetric from "./internal/metric.ts"
22import * as InternalRecord from "./internal/record.ts"
23import * as Layer from "./Layer.ts"
24import * as Order from "./Order.ts"
25import type { Pipeable } from "./Pipeable.ts"
26import { pipeArguments } from "./Pipeable.ts"
27import * as Predicate from "./Predicate.ts"
28import * as _String from "./String.ts"
29import 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 */
111export 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 */
189export 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 */
249export 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 */
323export 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 */
407export 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 */
468export 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 */
536export 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 */
624export 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 */
711export 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 */
816export 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 */
898export 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 */
950export 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 */
1561export 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 */
1608export const CurrentMetricAttributes = Context.Reference<Metric.AttributeSet>(CurrentMetricAttributesKey, {
1609 defaultValue: () => ({})
1610})
1611
1612const 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 */
1620export 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 */
1648export const MetricRegistry: Context.Reference<MetricRegistry> = Context.Reference<MetricRegistry>(
1649 MetricRegistryKey,
1650 { defaultValue: () => new Map() }
1651)
1652
1653const TypeId = "~effect/Metric"
1654
1655abstract 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
1723const bigint0 = BigInt(0)
1724
1725class 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
1757class 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
1782class 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
1810class 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
1885class 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
1978class 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 */
2023export 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 */
2083export 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 */
2169export 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 */
2263export 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 */
2342export 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 */
2418export 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 */
2470export 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 */
2523export 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 */
2583export 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 */
2633export 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 */
2695export 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 */
2740export 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 */
2802export 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 */
2867export 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 */
2932export 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 */
2991export 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 */
3059export 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
3067const renderName = (metric: Metric.Snapshot, padTo: number): string => `name=${metric.id.padEnd(padTo, " ")}`
3068
3069const renderDescription = (metric: Metric.Snapshot, padTo: number): string =>
3070 `description=${(metric.description ?? "").padEnd(padTo, " ")}`
3071
3072const renderType = (metric: Metric.Snapshot, padTo: number): string => `type=${metric.type.padEnd(padTo, " ")}`
3073
3074const 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
3080const 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
3117const renderKeyValues = (keyValues: Iterable<[number | string, string | number]>): string =>
3118 Array.from(keyValues).map(([key, value]) => `(${key} -> ${value})`).join(", ")
3119
3120const 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 */
3148export 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 */
3173export 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 */
3199export 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
3208const fibersActive = gauge("child_fibers_active", {
3209 description: "The current count of active child fibers"
3210})
3211const fibersStarted = counter("child_fibers_started", {
3212 description: "The total number of child fibers that have been started",
3213 incremental: true
3214})
3215const fiberSuccesses = counter("child_fiber_successes", {
3216 description: "The total number of child fibers that have succeeded",
3217 incremental: true
3218})
3219const 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 */
3238export 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 */
3266export 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 */
3305export 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 */
3327export 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 */
3367export 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 */
3406export 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 */
3433export 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 */
3462export 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
3469function 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
3478function 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
3486function mergeAttributes(
3487 self: Metric.Attributes | undefined,
3488 other: Metric.Attributes | undefined
3489): Metric.AttributeSet {
3490 return { ...attributesToRecord(self), ...attributesToRecord(other) }
3491}
3492
3493function 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
3503function 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}