/**
* Records and reads measurements from Effect programs.
*
* A `Metric` accepts typed update values and stores an aggregated
* state that can be read directly or included in a snapshot. Metrics are used
* for counters, gauges, frequencies, histograms, summaries, and timers. This
* module includes metric constructors, update and read helpers, attributes,
* histogram boundaries, registry snapshots, text dumps, and controls for
* enabling runtime metrics.
*
* @stability stable
* @since 2.0.0
*/
import * as Arr from "./Array.ts"
import * as Context from "./Context.ts"
import * as Duration from "./Duration.ts"
import type { Effect } from "./Effect.ts"
import type { Exit } from "./Exit.ts"
import { constUndefined, constVoid, dual, identity } from "./Function.ts"
import * as InternalEffect from "./internal/effect.ts"
import * as InternalMetric from "./internal/metric.ts"
import * as InternalRecord from "./internal/record.ts"
import * as Layer from "./Layer.ts"
import * as Order from "./Order.ts"
import type { Pipeable } from "./Pipeable.ts"
import { pipeArguments } from "./Pipeable.ts"
import * as Predicate from "./Predicate.ts"
import * as _String from "./String.ts"
import type { Contravariant, Covariant } from "./Types.ts"
/**
* A `Metric` represents a concurrent metric which accepts update
* values of type `Input` and are aggregated to a value of type `State`.
*
* **Details**
*
* For example, a counter metric would have type `Metric`,
* representing the fact that the metric can be updated with numbers (the amount
* to increment or decrement the counter by), and the state of the counter is a
* number.
*
* There are five primitive metric types supported by Effect:
*
* - Counters
* - Frequencies
* - Gauges
* - Histograms
* - Summaries
*
* **Example** (Using multiple metric types)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of metrics
* const requestCounter: Metric.Counter = Metric.counter("requests", {
* description: "Total requests processed"
* })
*
* const memoryGauge: Metric.Gauge = Metric.gauge("memory_usage", {
* description: "Current memory usage in MB"
* })
*
* const statusFrequency: Metric.Frequency = Metric.frequency("status_codes", {
* description: "HTTP status code frequency"
* })
*
* // All metrics share the same interface for updates and reads
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(memoryGauge, 128)
* yield* Metric.update(statusFrequency, "200")
*
* // All metrics can be read with Metric.value
* const counterState = yield* Metric.value(requestCounter)
* const gaugeState = yield* Metric.value(memoryGauge)
* const frequencyState = yield* Metric.value(statusFrequency)
*
* // Metrics have common properties accessible through the interface:
* // - id: unique identifier
* // - type: metric type ("Counter", "Gauge", "Frequency", etc.)
* // - description: optional human-readable description
* // - attributes: optional key-value attributes for tagging
*
* return {
* counter: {
* id: requestCounter.id,
* type: requestCounter.type,
* state: counterState
* },
* gauge: { id: memoryGauge.id, type: memoryGauge.type, state: gaugeState },
* frequency: {
* id: statusFrequency.id,
* type: statusFrequency.type,
* state: frequencyState
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [
* result.counter.state.count,
* result.gauge.state.value,
* result.frequency.state.occurrences.get("200")
* ] // => [1, 128, 1]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Metric extends Pipeable {
readonly [TypeId]: typeof TypeId
readonly Input: Contravariant
readonly State: Covariant
readonly id: string
readonly type: Metric.Type
readonly description: string | undefined
readonly attributes: Metric.AttributeSet | undefined
readonly valueUnsafe: (context: Context.Context) => State
readonly updateUnsafe: (input: Input, context: Context.Context) => void
readonly modifyUnsafe: (input: Input, context: Context.Context) => void
}
/**
* A Counter metric that tracks cumulative values that typically only increase.
*
* **When to use**
*
* Use when counters are useful for tracking monotonically increasing values like request counts,
* bytes processed, errors encountered, or any value that accumulates over time.
*
* **Example** (Using counter metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of counters
* const requestCounter: Metric.Counter = Metric.counter(
* "http_requests",
* {
* description: "Total HTTP requests processed",
* incremental: true // Only allows increments
* }
* )
*
* const bytesCounter: Metric.Counter = Metric.counter(
* "bytes_processed",
* {
* description: "Total bytes processed",
* bigint: true,
* attributes: { service: "data-processor" }
* }
* )
*
* // Update counters
* yield* Metric.update(requestCounter, 1) // Increment by 1
* yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6)
* yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes
*
* // Read counter state
* const requestState: Metric.CounterState = yield* Metric.value(
* requestCounter
* )
* const bytesState: Metric.CounterState = yield* Metric.value(
* bytesCounter
* )
*
* // Counter state contains:
* // - count: current accumulated value
* // - incremental: whether only increments are allowed
*
* return {
* requests: {
* count: requestState.count,
* incremental: requestState.incremental
* },
* bytes: { count: bytesState.count, incremental: bytesState.incremental }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.requests.count, result.bytes.count] // => [6, 1024n]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Counter extends Metric> {}
/**
* State interface for Counter metrics containing the current count and increment mode.
*
* **Example** (Reading counter state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of counters
* const requestCounter = Metric.counter("http_requests_total")
* const errorCounter = Metric.counter("errors_total", { incremental: true })
* const byteCounter = Metric.counter("bytes_processed", { bigint: true })
*
* // Update counters
* yield* Metric.update(requestCounter, 5) // Add 5 requests
* yield* Metric.update(requestCounter, -2) // Subtract 2 (allowed for non-incremental)
* yield* Metric.update(errorCounter, 3) // Add 3 errors
* yield* Metric.update(errorCounter, -1) // Attempt to subtract (ignored for incremental)
* yield* Metric.update(byteCounter, 1024000n) // Add bytes as bigint
*
* // Read counter states
* const requestState: Metric.CounterState = yield* Metric.value(
* requestCounter
* )
* const errorState: Metric.CounterState = yield* Metric.value(
* errorCounter
* )
* const byteState: Metric.CounterState = yield* Metric.value(
* byteCounter
* )
* // CounterState contains:
* // - count: current count value (number or bigint based on counter type)
* // - incremental: whether counter only allows increases
*
* return {
* requests: {
* total: requestState.count, // 3 (5 - 2, decrements allowed)
* canDecrease: !requestState.incremental // true
* },
* errors: {
* total: errorState.count, // 3 (subtract ignored)
* canDecrease: !errorState.incremental // false
* },
* bytes: {
* total: byteState.count, // 1024000n
* canDecrease: !byteState.incremental // true
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.requests.total, result.errors.total, result.bytes.total] // => [3, 3, 1024000n]
* ```
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface CounterState {
readonly count: Input extends bigint ? bigint : number
readonly incremental: boolean
}
/**
* A Frequency metric interface that counts occurrences of discrete string values.
*
* **When to use**
*
* Use when frequency metrics are ideal for tracking categorical data where you want to count
* how many times specific string values occur, such as HTTP status codes, user actions,
* error types, or any discrete string-based events.
*
* **Example** (Using frequency metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* // Function that accepts any Frequency metric
* const analyzeFrequencyMetric = (freq: Metric.Frequency) =>
* Effect.gen(function*() {
* const state = yield* Metric.value(freq)
*
* // Access the frequency state
* const occurrences: ReadonlyMap = state.occurrences
*
* // Find most frequent value
* let maxCount = 0
* let mostFrequent = ""
* for (const [value, count] of occurrences) {
* if (count > maxCount) {
* maxCount = count
* mostFrequent = value
* }
* }
*
* return { mostFrequent, maxCount, totalUniqueValues: occurrences.size }
* })
*
* const program = Effect.gen(function*() {
* // Create frequency metrics
* const statusCodes: Metric.Frequency = Metric.frequency("http_status", {
* description: "HTTP status code frequency"
* })
*
* const userActions: Metric.Frequency = Metric.frequency("user_actions", {
* description: "User action frequency"
* })
*
* // Record some occurrences
* yield* Metric.update(statusCodes, "200")
* yield* Metric.update(statusCodes, "200")
* yield* Metric.update(statusCodes, "404")
* yield* Metric.update(statusCodes, "500")
* yield* Metric.update(statusCodes, "200")
*
* yield* Metric.update(userActions, "login")
* yield* Metric.update(userActions, "view_dashboard")
* yield* Metric.update(userActions, "login")
*
* // Use the function with different frequency metrics
* const statusAnalysis = yield* analyzeFrequencyMetric(statusCodes)
* const actionAnalysis = yield* analyzeFrequencyMetric(userActions)
* return { statusAnalysis, actionAnalysis }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.statusAnalysis.mostFrequent, result.actionAnalysis.mostFrequent] // => ["200", "login"]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Frequency extends Metric {}
/**
* State interface for Frequency metrics containing occurrence counts for discrete string values.
*
* **Example** (Reading frequency state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create frequency metrics for different categories
* const statusCodeFreq = Metric.frequency("http_status_codes", {
* description: "HTTP status code distribution"
* })
*
* const userActionFreq = Metric.frequency("user_actions", {
* description: "User action frequency"
* })
*
* // Record occurrences
* yield* Metric.update(statusCodeFreq, "200") // Success
* yield* Metric.update(statusCodeFreq, "200") // Another success
* yield* Metric.update(statusCodeFreq, "404") // Not found
* yield* Metric.update(statusCodeFreq, "500") // Server error
* yield* Metric.update(statusCodeFreq, "200") // Another success
*
* yield* Metric.update(userActionFreq, "login")
* yield* Metric.update(userActionFreq, "click")
* yield* Metric.update(userActionFreq, "login")
* yield* Metric.update(userActionFreq, "scroll")
* yield* Metric.update(userActionFreq, "click")
* yield* Metric.update(userActionFreq, "click")
*
* // Read frequency states
* const statusState: Metric.FrequencyState = yield* Metric.value(statusCodeFreq)
* const actionState: Metric.FrequencyState = yield* Metric.value(userActionFreq)
*
* // FrequencyState contains:
* // - occurrences: ReadonlyMap with string values and their counts
*
* // Analyze frequency distributions
* const getMostFrequent = (occurrences: ReadonlyMap) => {
* let maxKey = ""
* let maxCount = 0
* for (const [key, count] of occurrences) {
* if (count > maxCount) {
* maxKey = key
* maxCount = count
* }
* }
* return { key: maxKey, count: maxCount }
* }
*
* const topStatus = getMostFrequent(statusState.occurrences)
* const topAction = getMostFrequent(actionState.occurrences)
* return {
* statusCodes: {
* totalResponses: Array.from(statusState.occurrences.values()).reduce(
* (a, b) => a + b,
* 0
* ), // 5
* mostCommon: topStatus, // { key: "200", count: 3 }
* uniqueCodes: statusState.occurrences.size // 3
* },
* userActions: {
* totalActions: Array.from(actionState.occurrences.values()).reduce(
* (a, b) => a + b,
* 0
* ), // 6
* mostCommon: topAction, // { key: "click", count: 3 }
* uniqueActions: actionState.occurrences.size // 3
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const mostCommon = [result.statusCodes.mostCommon, result.userActions.mostCommon]
* mostCommon.map(({ key, count }) => [key, count]) // => [["200", 3], ["click", 3]]
* ```
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface FrequencyState {
readonly occurrences: ReadonlyMap
}
/**
* A Gauge metric that tracks instantaneous values that can go up or down.
*
* **When to use**
*
* Use when gauges are useful for tracking current state values like memory usage, CPU load,
* active connections, queue sizes, or any value that represents a current level.
*
* **Example** (Using gauge metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of gauges
* const memoryGauge: Metric.Gauge = Metric.gauge("memory_usage_mb", {
* description: "Current memory usage in megabytes"
* })
*
* const diskSpaceGauge: Metric.Gauge = Metric.gauge("disk_free_bytes", {
* description: "Available disk space in bytes",
* bigint: true,
* attributes: { mount: "/var" }
* })
*
* // Set gauge values (absolute values)
* yield* Metric.update(memoryGauge, 512) // Set to 512 MB
* yield* Metric.update(memoryGauge, 640) // Set to 640 MB (replaces 512)
* yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to ~5GB free
*
* // Modify gauge values (relative changes)
* yield* Metric.modify(memoryGauge, 128) // Add 128 MB (total: 768)
* yield* Metric.modify(memoryGauge, -64) // Subtract 64 MB (total: 704)
*
* // Read gauge state
* const memoryState: Metric.GaugeState = yield* Metric.value(
* memoryGauge
* )
* const diskState: Metric.GaugeState = yield* Metric.value(
* diskSpaceGauge
* )
* // Gauge state contains:
* // - value: current instantaneous value
*
* return {
* memory: { currentValue: memoryState.value }, // 704
* disk: { currentValue: diskState.value } // 5000000000n
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.memory.currentValue, result.disk.currentValue] // => [704, 5000000000n]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Gauge extends Metric> {}
/**
* State interface for Gauge metrics containing the current instantaneous value.
*
* **Example** (Reading gauge state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of gauges
* const temperatureGauge = Metric.gauge("room_temperature_celsius", {
* description: "Current room temperature"
* })
*
* const diskSpaceGauge = Metric.gauge("disk_usage_bytes", {
* description: "Current disk usage",
* bigint: true
* })
*
* const queueSizeGauge = Metric.gauge("queue_size", {
* description: "Current queue size"
* })
*
* // Set gauge values (absolute values)
* yield* Metric.update(temperatureGauge, 22.5) // Set to 22.5°C
* yield* Metric.update(diskSpaceGauge, 5000000000n) // Set to 5GB usage
* yield* Metric.update(queueSizeGauge, 10) // Set to 10 items
*
* // Update gauge values (new absolute values)
* yield* Metric.update(temperatureGauge, 23.1) // Temperature changed
* yield* Metric.update(queueSizeGauge, 15) // Queue grew
*
* // Read gauge states
* const tempState: Metric.GaugeState = yield* Metric.value(
* temperatureGauge
* )
* const diskState: Metric.GaugeState = yield* Metric.value(
* diskSpaceGauge
* )
* const queueState: Metric.GaugeState = yield* Metric.value(
* queueSizeGauge
* )
* // GaugeState contains:
* // - value: current instantaneous value (number or bigint based on gauge type)
*
* return {
* environment: {
* temperature: tempState.value, // 23.1
* temperatureUnit: "°C"
* },
* system: {
* diskUsage: diskState.value, // 5000000000n
* diskUsageGB: Number(diskState.value) / 1_000_000_000, // 5
* queueSize: queueState.value // 15
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.environment.temperature, result.system.diskUsage, result.system.queueSize]
* values // => [23.1, 5000000000n, 15]
* ```
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface GaugeState {
readonly value: Input extends bigint ? bigint : number
}
/**
* A Histogram metric that records observations in configurable buckets to analyze value distributions.
*
* **When to use**
*
* Use when histograms are ideal for measuring request durations, response sizes, and other continuous values
* where you need to understand the distribution of values rather than just aggregates.
*
* **Example** (Using histogram metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create histograms with different boundary strategies
* const responseTimeHistogram: Metric.Histogram = Metric.histogram(
* "http_response_time_ms",
* {
* description: "HTTP response time distribution in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) // 50, 100, ..., 900, Infinity
* }
* )
*
* const fileSizeHistogram: Metric.Histogram = Metric.histogram(
* "file_size_bytes",
* {
* description: "File size distribution in bytes",
* boundaries: Metric.exponentialBoundaries({
* start: 1,
* factor: 2,
* count: 10
* }) // 1, 2, 4, 8, ..., 512
* }
* )
*
* // Record observations (values get placed into appropriate buckets)
* yield* Metric.update(responseTimeHistogram, 125) // Goes into 100-150ms bucket
* yield* Metric.update(responseTimeHistogram, 75) // Goes into 50-100ms bucket
* yield* Metric.update(responseTimeHistogram, 200) // Goes into 150-200ms bucket
* yield* Metric.update(responseTimeHistogram, 45) // Goes into 0-50ms bucket
*
* yield* Metric.update(fileSizeHistogram, 3) // Goes into 2-4 bytes bucket
* yield* Metric.update(fileSizeHistogram, 15) // Goes into 8-16 bytes bucket
* yield* Metric.update(fileSizeHistogram, 100) // Goes into 64-128 bytes bucket
*
* // Read histogram state
* const responseTimeState: Metric.HistogramState = yield* Metric.value(
* responseTimeHistogram
* )
* const fileSizeState: Metric.HistogramState = yield* Metric.value(
* fileSizeHistogram
* )
* // Histogram state contains:
* // - buckets: Array of [boundary, cumulativeCount] pairs
* // - count: total number of observations
* // - min: smallest observed value
* // - max: largest observed value
* // - sum: sum of all observed values
*
* return {
* responseTime: {
* totalRequests: responseTimeState.count, // 4
* fastestRequest: responseTimeState.min, // 45
* slowestRequest: responseTimeState.max, // 200
* totalTime: responseTimeState.sum, // 445
* averageTime: responseTimeState.sum / responseTimeState.count // 111.25
* },
* fileSize: {
* totalFiles: fileSizeState.count, // 3
* smallestFile: fileSizeState.min, // 3
* largestFile: fileSizeState.max, // 100
* totalBytes: fileSizeState.sum // 118
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.responseTime.totalRequests, result.responseTime.totalTime, result.fileSize.totalBytes]
* values // => [4, 445, 118]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Histogram extends Metric {}
/**
* State interface for Histogram metrics containing bucket distributions and aggregate statistics.
*
* **Example** (Reading histogram state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create histogram with linear boundaries
* const responseTimeHistogram = Metric.histogram("api_response_time_ms", {
* description: "API response time distribution",
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) // 100, 200, ..., 800, Infinity
* })
*
* // Record observations
* yield* Metric.update(responseTimeHistogram, 50) // Fast response
* yield* Metric.update(responseTimeHistogram, 150) // Average response
* yield* Metric.update(responseTimeHistogram, 750) // Slow response
* yield* Metric.update(responseTimeHistogram, 250) // Average response
* yield* Metric.update(responseTimeHistogram, 95) // Fast response
*
* // Read histogram state
* const state: Metric.HistogramState = yield* Metric.value(
* responseTimeHistogram
* )
*
* // HistogramState contains:
* // - buckets: Array of [boundary, cumulativeCount] pairs showing distribution
* // - count: total number of observations
* // - min: smallest observed value
* // - max: largest observed value
* // - sum: sum of all observed values
*
* // Analyze bucket distribution
* const analyzeBuckets = (buckets: ReadonlyArray<[number, number]>) => {
* const analysis: Array<
* { range: string; count: number; percentage: number }
* > = []
* let previousCount = 0
* const totalCount = buckets[buckets.length - 1]?.[1] ?? 0
*
* for (let i = 0; i < buckets.length; i++) {
* const [boundary, cumulativeCount] = buckets[i]
* const bucketCount = cumulativeCount - previousCount
* const percentage = totalCount > 0 ? (bucketCount / totalCount) * 100 : 0
* const prevBoundary = i === 0 ? 0 : buckets[i - 1][0]
*
* analysis.push({
* range: `${prevBoundary}-${boundary}ms`,
* count: bucketCount,
* percentage: Math.round(percentage * 10) / 10
* })
* previousCount = cumulativeCount
* }
* return analysis
* }
*
* const bucketAnalysis = analyzeBuckets(state.buckets)
* return {
* responseTime: {
* totalRequests: state.count, // 5
* fastestResponse: state.min, // 50
* slowestResponse: state.max, // 750
* averageResponse: state.sum / state.count, // 268
* totalTime: state.sum, // 1340
* distribution: bucketAnalysis
* // Example distribution:
* // [{ range: "0-100ms", count: 2, percentage: 40.0 },
* // { range: "100-200ms", count: 1, percentage: 20.0 },
* // { range: "200-300ms", count: 1, percentage: 20.0 },
* // { range: "700-800ms", count: 1, percentage: 20.0 }]
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const stats = result.responseTime
* const values = [stats.totalRequests, stats.fastestResponse, stats.slowestResponse, stats.totalTime]
* values // => [5, 50, 750, 1295]
* ```
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface HistogramState {
readonly buckets: ReadonlyArray<[number, number]>
readonly count: number
readonly min: number
readonly max: number
readonly sum: number
}
/**
* A Summary metric that calculates quantiles over a sliding time window of observations.
*
* **When to use**
*
* Use when summaries provide statistical insights into value distributions by tracking specific quantiles
* (percentiles) such as median (50th), 95th percentile, 99th percentile, etc. They're ideal for
* understanding performance characteristics like response time distributions.
*
* **Example** (Using summary metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create summaries with different quantile configurations
* const responseTimeSummary: Metric.Summary = Metric.summary(
* "api_response_time_ms",
* {
* description: "API response time distribution in milliseconds",
* maxAge: "5 minutes", // Keep observations for 5 minutes
* maxSize: 1000, // Keep up to 1000 observations
* quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles
* }
* )
*
* const requestSizeSummary: Metric.Summary = Metric.summary(
* "request_size_bytes",
* {
* description: "Request payload size distribution",
* maxAge: "10 minutes",
* maxSize: 500,
* quantiles: [0.25, 0.5, 0.75, 0.9] // Track quartiles and 90th percentile
* }
* )
*
* // Record observations (values are stored in time-based sliding window)
* yield* Metric.update(responseTimeSummary, 120) // Fast response
* yield* Metric.update(responseTimeSummary, 250) // Average response
* yield* Metric.update(responseTimeSummary, 45) // Very fast response
* yield* Metric.update(responseTimeSummary, 890) // Slow response
* yield* Metric.update(responseTimeSummary, 156) // Average response
*
* yield* Metric.update(requestSizeSummary, 1024) // 1KB request
* yield* Metric.update(requestSizeSummary, 512) // 512B request
* yield* Metric.update(requestSizeSummary, 2048) // 2KB request
*
* // Read summary state
* const responseTimeState: Metric.SummaryState = yield* Metric.value(
* responseTimeSummary
* )
* const requestSizeState: Metric.SummaryState = yield* Metric.value(
* requestSizeSummary
* )
*
* // Summary state contains:
* // - quantiles: Array of [quantile, optionalValue] pairs
* // - count: total number of observations in window
* // - min: smallest observed value in window
* // - max: largest observed value in window
* // - sum: sum of all observed values in window
*
* // Extract quantile values safely
* const getQuantileValue = (
* quantiles: ReadonlyArray,
* q: number
* ) => quantiles.find(([quantile]) => quantile === q)?.[1]
*
* const median = getQuantileValue(responseTimeState.quantiles, 0.5)
* const p95 = getQuantileValue(responseTimeState.quantiles, 0.95)
* const p99 = getQuantileValue(responseTimeState.quantiles, 0.99)
* return {
* responseTime: {
* totalRequests: responseTimeState.count, // 5
* fastestResponse: responseTimeState.min, // 45
* slowestResponse: responseTimeState.max, // 890
* totalTime: responseTimeState.sum, // 1461
* averageTime: responseTimeState.sum / responseTimeState.count, // 292.2
* medianTime: median ?? null, // ~156
* p95Time: p95 ?? null, // ~890
* p99Time: p99 ?? null // ~890
* },
* requestSize: {
* totalRequests: requestSizeState.count, // 3
* averageSize: requestSizeState.sum / requestSizeState.count // ~1194.7
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.responseTime.totalRequests, result.responseTime.totalTime, result.requestSize.totalRequests]
* counts // => [5, 1461, 3]
* ```
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface Summary extends Metric {}
/**
* State interface for Summary metrics containing quantile calculations and aggregate statistics.
*
* **Example** (Reading summary state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create summary with specific quantiles
* const responseTimeSummary = Metric.summary("api_response_latency", {
* description: "API response time distribution with quantiles",
* maxAge: "5 minutes",
* maxSize: 1000,
* quantiles: [0.5, 0.95, 0.99] // Track median, 95th, and 99th percentiles
* })
*
* // Record observations over time
* yield* Metric.update(responseTimeSummary, 120) // Fast response
* yield* Metric.update(responseTimeSummary, 250) // Average response
* yield* Metric.update(responseTimeSummary, 45) // Very fast response
* yield* Metric.update(responseTimeSummary, 890) // Slow response
* yield* Metric.update(responseTimeSummary, 156) // Average response
* yield* Metric.update(responseTimeSummary, 78) // Fast response
* yield* Metric.update(responseTimeSummary, 340) // Slower response
*
* // Read summary state
* const state: Metric.SummaryState = yield* Metric.value(responseTimeSummary)
*
* // SummaryState contains:
* // - quantiles: Array of [quantile, optionalValue] pairs showing percentile values
* // - count: total number of observations in current window
* // - min: smallest observed value in window
* // - max: largest observed value in window
* // - sum: sum of all observed values in window
*
* // Extract quantile information safely
* const extractQuantiles = (
* quantiles: ReadonlyArray
* ) => {
* const result: Record = {}
* for (const [quantile, valueOption] of quantiles) {
* const percentile = Math.round(quantile * 100)
* result[`p${percentile}`] = valueOption ?? null
* }
* return result
* }
*
* const quantileValues = extractQuantiles(state.quantiles)
* return {
* latencyAnalysis: {
* totalRequests: state.count, // 7
* fastestResponse: state.min, // 45
* slowestResponse: state.max, // 890
* averageResponse: state.sum / state.count, // ~268.4
* totalLatency: state.sum, // 1879
* percentiles: quantileValues,
* // Example percentiles:
* // { p50: 156, p95: 890, p99: 890 }
* performance: {
* fast: quantileValues.p50 !== null && quantileValues.p50 < 200
* ? "Good"
* : "Needs improvement",
* reliability: quantileValues.p95 !== null && quantileValues.p95 < 500
* ? "Reliable"
* : "Concerning"
* }
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const analysis = result.latencyAnalysis
* const values = [analysis.totalRequests, analysis.fastestResponse, analysis.slowestResponse, analysis.totalLatency]
* values // => [7, 45, 890, 1879]
* ```
*
* @stability stable
* @category models
* @since 4.0.0
*/
export interface SummaryState {
readonly quantiles: ReadonlyArray
readonly count: number
readonly min: number
readonly max: number
readonly sum: number
}
/**
* The `Metric` namespace provides a comprehensive system for collecting, aggregating, and observing
* application metrics in Effect applications.
*
* **Example** (Collecting application metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of metrics
* const requestCounter = Metric.counter("http_requests_total")
* const responseTimeHistogram = Metric.histogram("http_response_time", {
* boundaries: Metric.linearBoundaries({ start: 0, width: 10, count: 10 })
* })
* const activeConnectionsGauge = Metric.gauge("active_connections")
* const statusFrequency = Metric.frequency("http_status_codes")
*
* // Update metrics
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(responseTimeHistogram, 45.2)
* yield* Metric.update(activeConnectionsGauge, 12)
* yield* Metric.update(statusFrequency, "200")
*
* // Get metric values
* const counterValue = yield* Metric.value(requestCounter)
* const histogramValue = yield* Metric.value(responseTimeHistogram)
* const gaugeValue = yield* Metric.value(activeConnectionsGauge)
* const frequencyValue = yield* Metric.value(statusFrequency)
*
* return {
* counter: counterValue,
* histogram: histogramValue,
* gauge: gaugeValue,
* frequency: frequencyValue
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.counter.count, result.gauge.value] // => [1, 12]
* ```
*
* @stability stable
* @since 2.0.0
*/
export declare namespace Metric {
/**
* Union type representing all available metric types in the Effect metrics system.
*
* **Example** (Inspecting metric types)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* const metrics: ReadonlyArray> = [
* Metric.counter("requests_total"),
* Metric.gauge("cpu_usage"),
* Metric.frequency("status_codes"),
* Metric.histogram("response_time", {
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 })
* }),
* Metric.summary("latency", {
* maxAge: "5 minutes",
* maxSize: 1000,
* quantiles: [0.5, 0.95, 0.99]
* })
* ]
*
* const types: ReadonlyArray = metrics.map((metric) => metric.type)
* const actual = types // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"]
* ```
*
* @category models
* @since 4.0.0
*/
export type Type = "Counter" | "Frequency" | "Gauge" | "Histogram" | "Summary"
/**
* Union type for metric attributes that can be provided as either an object or array of tuples.
*
* **Example** (Providing attributes in different formats)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Different ways to specify attributes
* const attributesAsObject = {
* service: "api",
* environment: "production",
* version: "1.2.3"
* }
*
* const attributesAsArray: ReadonlyArray<[string, string]> = [
* ["service", "api"],
* ["environment", "production"],
* ["version", "1.2.3"]
* ]
*
* // Create metrics with different attribute formats
* const requestCounter1 = Metric.counter("requests", {
* description: "Total requests",
* attributes: attributesAsObject // Using object format
* })
*
* const requestCounter2 = Metric.counter("requests", {
* description: "Total requests",
* attributes: attributesAsArray // Using array format
* })
*
* // Function to normalize attributes to object format
* const normalizeAttributes = (
* attrs: typeof attributesAsObject | ReadonlyArray<[string, string]>
* ) => {
* if (Array.isArray(attrs)) {
* return Object.fromEntries(attrs)
* }
* return attrs
* }
*
* // Add runtime attributes using withAttributes
* const contextualCounter = Metric.withAttributes(requestCounter1, {
* method: "GET",
* endpoint: "/api/users"
* })
*
* // Update metrics with different attribute combinations
* yield* Metric.update(contextualCounter, 1)
*
* // Both formats result in the same internal representation
* const normalizedObject = normalizeAttributes(attributesAsObject)
* const normalizedArray = normalizeAttributes(attributesAsArray)
* return {
* attributeFormats: {
* object: normalizedObject, // { service: "api", environment: "production", version: "1.2.3" }
* array: normalizedArray // { service: "api", environment: "production", version: "1.2.3" }
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const objectAttributes = result.attributeFormats.object
* const arrayAttributes = result.attributeFormats.array
* const sameAttributes = [objectAttributes, arrayAttributes]
* sameAttributes // => [{ service: "api", environment: "production", version: "1.2.3" }, { service: "api", environment: "production", version: "1.2.3" }]
* ```
*
* @category models
* @since 4.0.0
*/
export type Attributes = AttributeSet | ReadonlyArray<[string, string]>
/**
* Type for metric attributes as a readonly record of string key-value pairs.
*
* **Example** (Combining metric attribute sets)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Define attribute sets for different contexts
* const serviceAttributes = {
* service: "user-api",
* version: "2.1.0",
* environment: "production"
* }
*
* const operationAttributes = {
* operation: "create_user",
* method: "POST",
* endpoint: "/api/users"
* }
*
* const infrastructureAttributes = {
* region: "us-east-1",
* datacenter: "dc1",
* host: "api-server-01"
* }
*
* // Create metrics with predefined attribute sets
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total HTTP requests",
* attributes: serviceAttributes
* })
*
* // Combine attribute sets
* const combineAttributes = (...attributeSets: Array>) =>
* Object.assign({}, ...attributeSets)
*
* const fullAttributes = combineAttributes(
* serviceAttributes,
* operationAttributes,
* infrastructureAttributes
* )
*
* // Create metric with combined attributes
* const detailedCounter = Metric.withAttributes(requestCounter, fullAttributes)
*
* // Helper to validate attribute keys (all must be strings)
* const validateAttributeSet = (attrs: Record): boolean => {
* return Object.entries(attrs).every(([key, value]) =>
* typeof key === "string" && typeof value === "string"
* )
* }
*
* yield* Metric.update(detailedCounter, 1)
*
* return {
* attributes: {
* service: serviceAttributes,
* operation: operationAttributes,
* infrastructure: infrastructureAttributes,
* combined: fullAttributes,
* isValid: validateAttributeSet(fullAttributes), // true
* totalKeys: Object.keys(fullAttributes).length // 9
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const validation = [result.attributes.isValid, result.attributes.totalKeys] // => [true, 9]
* ```
*
* @category models
* @since 4.0.0
*/
export type AttributeSet = Readonly>
/**
* Utility type to extract the Input type from a Metric type.
*
* **Example** (Extracting metric input types)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* // Create various metric types
* const numberCounter = Metric.counter("requests")
* const bigintCounter = Metric.counter("bytes", { bigint: true })
* const stringFrequency = Metric.frequency("status_codes")
* const numberGauge = Metric.gauge("cpu_usage")
* const numberHistogram = Metric.histogram("response_time", {
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 })
* })
*
* // The Input utility type extracts the input type from metric types:
* // - Counter: number
* // - Counter: bigint
* // - Frequency: string
* // - Gauge: number
* // - Histogram: number
*
* // Helper function that works with any metric
* const createMetricInfo = (metric: Metric.Metric) => ({
* id: metric.id,
* type: metric.type
* })
*
* const metrics = [
* createMetricInfo(numberCounter), // { id: "requests", type: "Counter" }
* createMetricInfo(bigintCounter), // { id: "bytes", type: "Counter" }
* createMetricInfo(stringFrequency), // { id: "status_codes", type: "Frequency" }
* createMetricInfo(numberGauge), // { id: "cpu_usage", type: "Gauge" }
* createMetricInfo(numberHistogram) // { id: "response_time", type: "Histogram" }
* ]
*
* // Type safety is enforced at compile time:
* // Metric.update(numberCounter, 123) // ✓ Valid (number)
* // Metric.update(numberCounter, "abc") // ✗ Type error
* // Metric.update(stringFrequency, "ok") // ✓ Valid (string)
* // Metric.update(stringFrequency, 404) // ✗ Type error
* const metricIds = metrics.map(({ id, type }) => `${id}:${type}`)
* metricIds // => ["requests:Counter", "bytes:Counter", "status_codes:Frequency", "cpu_usage:Gauge", "response_time:Histogram"]
* ```
*
* @category utility types
* @since 4.0.0
*/
export type Input = A extends Metric ? _Input
: never
/**
* Utility type to extract the State type from a Metric type.
*
* **Example** (Extracting metric state types)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* // Create various metric types
* const requestCounter = Metric.counter("requests")
* const cpuGauge = Metric.gauge("cpu_usage")
* const statusFrequency = Metric.frequency("status_codes")
* const responseHistogram = Metric.histogram("response_time", {
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 })
* })
* const latencySummary = Metric.summary("latency", {
* maxAge: "5 minutes",
* maxSize: 1000,
* quantiles: [0.5, 0.95, 0.99]
* })
*
* // The State utility type extracts the state type from metric types:
* // - Counter: CounterState
* // - Gauge: GaugeState
* // - Frequency: FrequencyState
* // - Histogram: HistogramState
* // - Summary: SummaryState
*
* // Type-safe state analysis functions
* const program = Effect.gen(function*() {
* // Update metrics first
* yield* Metric.update(requestCounter, 10)
* yield* Metric.update(cpuGauge, 85.5)
* yield* Metric.update(statusFrequency, "200")
* yield* Metric.update(responseHistogram, 150)
* yield* Metric.update(latencySummary, 120)
*
* // Extract states with proper typing
* const counterState = yield* Metric.value(requestCounter)
* const gaugeState = yield* Metric.value(cpuGauge)
* const frequencyState = yield* Metric.value(statusFrequency)
* const histogramState = yield* Metric.value(responseHistogram)
* const summaryState = yield* Metric.value(latencySummary)
* return {
* counter: { count: counterState.count }, // { count: 10 }
* gauge: { value: gaugeState.value }, // { value: 85.5 }
* frequency: { uniqueValues: frequencyState.occurrences.size }, // { uniqueValues: 1 }
* histogram: { totalObservations: histogramState.count }, // { totalObservations: 1 }
* summary: { observations: summaryState.count } // { observations: 1 }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.counter.count, result.gauge.value, result.frequency.uniqueValues]
* values // => [10, 85.5, 1]
* ```
*
* @category utility types
* @since 4.0.0
*/
export type State = A extends Metric ? _State
: never
/**
* Interface defining the core hooks for metric operations: get, update, and modify.
*
* **Example** (Using metric hooks)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a counter metric
* const requestCounter = Metric.counter("requests_total", {
* description: "Total number of requests"
* })
*
* // The Hooks interface provides three core operations for metrics:
* // 1. get: retrieve current state
* // 2. update: add/set a value
* // 3. modify: transform the current state
*
* // These are low-level APIs. Most users should use high-level APIs:
* // - Metric.value() for getting state
* // - Metric.update() for updating values
* // - Metric.modify() for modifying values
*
* // Example using high-level APIs (recommended)
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(requestCounter, 5)
* const state = yield* Metric.value(requestCounter)
*
* return {
* currentCount: state.count, // 6
* isIncremental: state.incremental // false
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const state = result // => { currentCount: 6, isIncremental: false }
* ```
*
* @category models
* @since 4.0.0
*/
export interface Hooks {
readonly get: (context: Context.Context) => State
readonly update: (input: Input, context: Context.Context) => void
readonly modify: (input: Input, context: Context.Context) => void
}
/**
* Interface containing complete metadata information about a metric.
*
* **Example** (Inspecting metric metadata)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create metrics with different configurations
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total number of HTTP requests",
* attributes: { service: "api", version: "1.0" }
* })
*
* const memoryGauge = Metric.gauge("memory_usage_bytes", {
* description: "Current memory usage in bytes"
* })
*
* const statusFrequency = Metric.frequency("http_status_codes")
*
* // The Metadata interface contains complete information about a metric:
* // - id: metric identifier
* // - type: metric type ("Counter", "Gauge", etc.)
* // - description: optional description
* // - attributes: optional key-value attributes
* // - hooks: low-level operations interface
*
* // Each metric has associated metadata that can be inspected
* yield* Metric.update(requestCounter, 10)
* yield* Metric.update(memoryGauge, 256000000)
* yield* Metric.update(statusFrequency, "200")
*
* return {
* counter: {
* id: requestCounter.id, // "http_requests_total"
* type: requestCounter.type, // "Counter"
* description: requestCounter.description // "Total number of HTTP requests"
* },
* gauge: {
* id: memoryGauge.id, // "memory_usage_bytes"
* type: memoryGauge.type, // "Gauge"
* description: memoryGauge.description // "Current memory usage in bytes"
* },
* frequency: {
* id: statusFrequency.id, // "http_status_codes"
* type: statusFrequency.type, // "Frequency"
* description: statusFrequency.description // undefined
* }
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const types = [result.counter.type, result.gauge.type, result.frequency.type] // => ["Counter", "Gauge", "Frequency"]
* ```
*
* @category models
* @since 4.0.0
*/
export interface Metadata {
readonly id: string
readonly type: Type
readonly description: string | undefined
readonly attributes: Metric.AttributeSet | undefined
readonly hooks: Hooks
}
/**
* Protocol interface for metric snapshots containing metadata and current state.
*
* **Example** (Inspecting metric snapshot protocols)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create and update metrics
* const requestCounter = Metric.counter("requests", {
* description: "Request count",
* attributes: { service: "api" }
* })
*
* const responseTimeHistogram = Metric.histogram("response_time", {
* description: "Response time distribution",
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 })
* })
*
* yield* Metric.update(requestCounter, 25)
* yield* Metric.update(responseTimeHistogram, 150)
* yield* Metric.update(responseTimeHistogram, 75)
*
* // Take snapshot of all metrics
* const snapshots = yield* Metric.snapshot
*
* // Each snapshot follows the SnapshotProto interface:
* // - id: metric identifier
* // - type: specific metric type
* // - description: optional description
* // - attributes: optional attributes
* // - state: current metric state
*
* const counterSnapshot = snapshots.find((s) => s.id === "requests")
* const histogramSnapshot = snapshots.find((s) => s.id === "response_time")
*
* return {
* counter: counterSnapshot ?
* {
* id: counterSnapshot.id, // "requests"
* type: counterSnapshot.type, // "Counter"
* description: counterSnapshot.description, // "Request count"
* hasAttributes: counterSnapshot.attributes !== undefined, // true
* count: (counterSnapshot.state as any).count // 25
* } :
* null,
* histogram: histogramSnapshot ?
* {
* id: histogramSnapshot.id, // "response_time"
* type: histogramSnapshot.type, // "Histogram"
* observations: (histogramSnapshot.state as any).count // 2
* } :
* null
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.counter?.count, result.histogram?.observations] // => [25, 2]
* ```
*
* @category models
* @since 4.0.0
*/
export interface SnapshotProto {
readonly id: string
readonly type: T
readonly description: string | undefined
readonly attributes: Metric.AttributeSet | undefined
readonly state: State
}
/**
* Union type representing all possible metric snapshot types with their corresponding states.
*
* **Example** (Analyzing metric snapshots)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create different types of metrics
* const requestCounter = Metric.counter("requests_total")
* const cpuGauge = Metric.gauge("cpu_usage_percent")
* const statusFrequency = Metric.frequency("http_status")
* const responseHistogram = Metric.histogram("response_time_ms", {
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 })
* })
* const latencySummary = Metric.summary("request_latency", {
* maxAge: "1 minute",
* maxSize: 100,
* quantiles: [0.5, 0.95, 0.99]
* })
*
* // Update all metrics
* yield* Metric.update(requestCounter, 150)
* yield* Metric.update(cpuGauge, 45.7)
* yield* Metric.update(statusFrequency, "200")
* yield* Metric.update(statusFrequency, "404")
* yield* Metric.update(responseHistogram, 250)
* yield* Metric.update(latencySummary, 120)
*
* // Take snapshot of all metrics
* const allSnapshots = yield* Metric.snapshot
*
* // Type-safe snapshot analysis using discriminated union
* const analyzeSnapshot = (snapshot: any) => {
* switch (snapshot.type) {
* case "Counter":
* return { type: "Counter", count: snapshot.state.count }
* case "Gauge":
* return { type: "Gauge", value: snapshot.state.value }
* case "Frequency":
* return {
* type: "Frequency",
* uniqueValues: snapshot.state.occurrences.size
* }
* case "Histogram":
* return { type: "Histogram", observations: snapshot.state.count }
* case "Summary":
* return { type: "Summary", observations: snapshot.state.count }
* }
* }
*
* const analysis = allSnapshots.map(analyzeSnapshot)
*
* return {
* totalMetrics: allSnapshots.length, // 5
* metricTypes: allSnapshots.map((s) => s.type), // ["Counter", "Gauge", "Frequency", "Histogram", "Summary"]
* analysis
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const types = result.metricTypes // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"]
* ```
*
* @category models
* @since 4.0.0
*/
export type Snapshot =
| SnapshotProto<"Counter", CounterState>
| SnapshotProto<"Gauge", GaugeState>
| SnapshotProto<"Frequency", FrequencyState>
| SnapshotProto<"Histogram", HistogramState>
| SnapshotProto<"Summary", SummaryState>
}
/**
* Service key for the current metric attributes context.
*
* **Example** (Accessing the current metric attributes key)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // The key is used internally by the Effect runtime to manage metric attributes
* const key = Metric.CurrentMetricAttributesKey
*
* // Create metrics with base attributes
* const requestCounter = Metric.counter("requests_total", {
* description: "Total HTTP requests"
* })
*
* // The CurrentMetricAttributes service provides default attributes
* // that get applied to all metrics in the current context
* const baseAttributes = { service: "api", version: "1.0" }
*
* // Use withAttributes to apply attributes to metrics
* const taggedCounter1 = Metric.withAttributes(requestCounter, baseAttributes)
* const program1 = Metric.update(taggedCounter1, 1)
*
* const taggedCounter2 = Metric.withAttributes(requestCounter, {
* ...baseAttributes,
* endpoint: "/users"
* })
* const program2 = Metric.update(taggedCounter2, 5)
*
* yield* program1
* yield* program2
*
* return {
* keyValue: key, // "effect/Metric/CurrentMetricAttributes"
* keyType: typeof key, // "string"
* isConstant: key === "effect/Metric/CurrentMetricAttributes" // true
* }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const key = result // => { keyValue: "effect/Metric/CurrentMetricAttributes", keyType: "string", isConstant: true }
* ```
*
* @stability stable
* @category constants
* @since 4.0.0
*/
export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes" as const
/**
* Context reference for metric attributes applied from the current Effect
* context.
*
* **When to use**
*
* Use to provide default attributes that should be merged into metric updates
* and reads in a scoped part of a program.
*
* **Details**
*
* The default value is an empty attribute set. Metric reads and updates merge
* these contextual attributes with the metric's own attributes to select the
* metric series being accessed.
*
* **Example** (Providing current metric attributes)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Access current metric attributes
* yield* Metric.CurrentMetricAttributes
*
* // Set new attributes context
* const newAttributes = { service: "api", version: "1.0" }
* const result = yield* Effect.provideService(
* Effect.gen(function*() {
* const updatedAttributes = yield* Metric.CurrentMetricAttributes
* return updatedAttributes
* }),
* Metric.CurrentMetricAttributes,
* newAttributes
* )
*
* return result
* })
*
* const attributes = await Effect.runPromise(program)
* const actual = attributes // => { service: "api", version: "1.0" }
* ```
*
* @stability stable
* @category services
* @since 4.0.0
*/
export const CurrentMetricAttributes = Context.Reference(CurrentMetricAttributesKey, {
defaultValue: () => ({})
})
const MetricRegistryKey = "effect/Metric/MetricRegistry"
/**
* The registry used to store metric metadata and hooks.
*
* @stability stable
* @category services
* @since 4.0.0
*/
export type MetricRegistry = Map>
/**
* Context reference for the metric registry in the current context.
*
* **When to use**
*
* Use when you need a custom metric registry for an isolated program or test
* instead of the default registry.
*
* **Details**
*
* By default, the reference creates an empty `Map` the first time it is
* resolved. Metrics register their metadata and hooks lazily in this map when
* they are read or updated.
*
* **Gotchas**
*
* Because `Context.Reference` caches default values, the default `Map` is
* shared by contexts that do not provide an override. Provide `MetricRegistry`
* with a fresh `Map` when isolation matters.
*
* @see {@link snapshot} for reading all registered metrics from the current `Effect` context
* @see {@link snapshotUnsafe} for reading all registered metrics from an explicit `Context`
*
* @stability stable
* @category services
* @since 4.0.0
*/
export const MetricRegistry: Context.Reference = Context.Reference(
MetricRegistryKey,
{ defaultValue: () => new Map() }
)
const TypeId = "~effect/Metric"
abstract class Metric$ implements Metric {
readonly [TypeId] = TypeId
abstract readonly type: Metric.Type
declare readonly Input: Contravariant
declare readonly State: Covariant
#extraAttributes: Metric.AttributeSet | undefined
#mergedAttributes: Metric.AttributeSet | undefined
#key = ""
readonly id: string
readonly description: string | undefined
readonly attributes: Metric.AttributeSet | undefined
constructor(
id: string,
description: string | undefined,
attributes: Metric.AttributeSet | undefined
) {
this.id = id
this.description = description
this.attributes = attributes
}
valueUnsafe(context: Context.Context): State {
return this.hook(context).get(context)
}
modifyUnsafe(input: Input, context: Context.Context): void {
return this.hook(context).modify(input, context)
}
updateUnsafe(input: Input, context: Context.Context): void {
return this.hook(context).update(input, context)
}
abstract createHooks(): Metric.Hooks
hook(context: Context.Context): Metric.Hooks {
const key = this.key(context)
// The registry owns the series, so it is looked up on every update.
const registry = Context.get(context, MetricRegistry)
let metadata = registry.get(key)
if (Predicate.isUndefined(metadata)) {
metadata = this.metadata(this.createHooks())
registry.set(key, metadata)
}
return metadata.hooks
}
removeUnsafe(context: Context.Context): void {
Context.get(context, MetricRegistry).delete(this.key(context))
}
registerUnsafe(context: Context.Context, hooks: Metric.Hooks): Registration {
const key = this.key(context)
// Keep the registry from registration time, so removal does not depend on
// the context it is called from.
const registry = Context.get(context, MetricRegistry)
const metadata = this.metadata({ get: hooks.get, update: constVoid, modify: constVoid })
registry.set(key, metadata)
return {
remove() {
if (registry.get(key) === metadata) {
registry.delete(key)
}
}
}
}
// Merges the attributes once per contextual attribute set, so series key and
// metadata share the result.
private key(context: Context.Context): string {
const extraAttributes = Context.get(context, CurrentMetricAttributes)
if (extraAttributes !== this.#extraAttributes) {
this.#extraAttributes = extraAttributes
const merged = mergeAttributes(this.attributes, extraAttributes)
this.#mergedAttributes = Object.keys(extraAttributes).length > 0 ? merged : this.attributes
this.#key = makeKey(this, merged)
}
return this.#key
}
// Must follow `key(context)`, which computes the merged attributes.
private metadata(hooks: Metric.Hooks): Metric.Metadata {
return {
id: this.id,
type: this.type,
description: this.description,
attributes: this.#mergedAttributes,
hooks
}
}
pipe() {
return pipeArguments(this, arguments)
}
}
const bigint0 = BigInt(0)
class CounterMetric extends Metric$> {
readonly type = "Counter"
readonly #bigint: boolean
readonly #incremental: boolean
constructor(id: string, options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint?: boolean | undefined
readonly incremental?: boolean | undefined
}) {
super(id, options?.description, attributesToRecord(options?.attributes))
this.#bigint = options?.bigint ?? false
this.#incremental = options?.incremental ?? false
}
createHooks(): Metric.Hooks> {
let count = (this.#bigint ? bigint0 : 0) as any
const canUpdate = this.#incremental
? this.#bigint
? (value: bigint | number) => value >= bigint0
: (value: bigint | number) => value >= 0
: (_value: bigint | number) => true
const update = (value: Input) => {
if (canUpdate(value)) {
count = (count as any) + value
}
}
return makeHooks(() => ({ count, incremental: this.#incremental }), update)
}
}
class GaugeMetric extends Metric$> {
readonly type = "Gauge"
readonly #bigint: boolean
constructor(id: string, options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint?: boolean | undefined
}) {
super(id, options?.description, attributesToRecord(options?.attributes))
this.#bigint = options?.bigint ?? false
}
createHooks(): Metric.Hooks> {
let value = this.#bigint ? BigInt(0) as any : 0
const update = (input: number | bigint) => {
value = input
}
const modify = (input: number | bigint) => {
value = value + input
}
return makeHooks(() => ({ value }), update, modify)
}
}
class FrequencyMetric extends Metric$ {
readonly type = "Frequency"
readonly #preregisteredWords: ReadonlyArray | undefined
constructor(id: string, options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly preregisteredWords?: ReadonlyArray | undefined
}) {
super(id, options?.description, attributesToRecord(options?.attributes))
this.#preregisteredWords = options?.preregisteredWords
}
createHooks(): Metric.Hooks {
const occurrences = new Map()
if (Predicate.isNotUndefined(this.#preregisteredWords)) {
for (const word of this.#preregisteredWords) {
occurrences.set(word, 0)
}
}
const update = (word: string) => {
const count = occurrences.get(word) ?? 0
occurrences.set(word, count + 1)
}
return makeHooks(() => ({ occurrences }), update)
}
}
class HistogramMetric extends Metric$ {
readonly type = "Histogram"
readonly #boundaries: ReadonlyArray
constructor(id: string, options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly boundaries: ReadonlyArray
}) {
super(id, options?.description, attributesToRecord(options?.attributes))
this.#boundaries = options.boundaries
}
createHooks(): Metric.Hooks {
const bounds = this.#boundaries
const size = bounds.length
const values = new Uint32Array(size + 1)
const boundaries = new Float64Array(size)
let count = 0
let sum = 0
let min = Number.MAX_VALUE
let max = -Number.MAX_VALUE
Arr.map(Arr.sort(bounds, Order.Number), (n, i) => {
boundaries[i] = n
})
// Insert the value into the right bucket with a binary search
const update = (value: number) => {
let from = 0
let to = size
while (from !== to) {
const mid = Math.floor(from + (to - from) / 2)
const boundary = boundaries[mid]
if (value <= boundary) {
to = mid
} else {
from = mid
}
// The special case when to / from have a distance of one
if (to === from + 1) {
if (value <= boundaries[from]) {
to = from
} else {
from = to
}
}
}
values[from] = values[from] + 1
count = count + 1
sum = sum + value
if (value < min) {
min = value
}
if (value > max) {
max = value
}
}
const getBuckets = (): ReadonlyArray<[number, number]> => {
const builder: Array<[number, number]> = Arr.allocate(size) as any
let cumulated = 0
for (let i = 0; i < size; i++) {
const boundary = boundaries[i]
const value = values[i]
cumulated = cumulated + value
builder[i] = [boundary, cumulated]
}
return builder
}
return makeHooks(() => ({ buckets: getBuckets(), count, min, max, sum }), update)
}
}
class SummaryMetric extends Metric$ {
readonly type = "Summary"
readonly #maxAge: number
readonly #maxSize: number
readonly #quantiles: ReadonlyArray
constructor(id: string, options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly maxAge: Duration.Input
readonly maxSize: number
readonly quantiles: ReadonlyArray
}) {
super(id, options?.description, attributesToRecord(options?.attributes))
this.#maxAge = Math.max(Duration.toMillis(Duration.fromInputUnsafe(options.maxAge)), 0)
this.#maxSize = options.maxSize
this.#quantiles = options.quantiles
}
createHooks(): Metric.Hooks {
const sortedQuantiles = Arr.sort(this.#quantiles, Order.Number)
const observations = Arr.allocate<[number, number]>(this.#maxSize)
for (const quantile of this.#quantiles) {
if (quantile < 0 || quantile > 1) {
throw new Error(`Quantile must be between 0 and 1, found: ${quantile}`)
}
}
let head = 0
let count = 0
let sum = 0
let min = Number.MAX_VALUE
let max = -Number.MAX_VALUE
const snapshot = (now: number): ReadonlyArray<[number, number | undefined]> => {
const builder: Array = []
let i = 0
while (i < this.#maxSize) {
const observation = observations[i]
if (Predicate.isNotUndefined(observation)) {
const [timestamp, value] = observation
const age = now - timestamp
if (age >= 0 && age <= this.#maxAge) {
builder.push(value)
}
}
i = i + 1
}
const samples = Arr.sort(builder, Order.Number)
const sampleSize = samples.length
if (sampleSize === 0) {
return sortedQuantiles.map((q) => [q, undefined])
}
// Compute the value of the quantile in terms of rank:
// For a given quantile `q`, return the maximum value `v` such that at
// most `q * n` values are less than or equal to `v`.
return sortedQuantiles.map((q) => {
if (q <= 0) return [q, samples[0]]
if (q >= 1) return [q, samples[sampleSize - 1]]
const index = Math.ceil(q * sampleSize) - 1
return [q, samples[index]]
})
}
const observe = (value: number, timestamp: number) => {
if (this.#maxSize > 0) {
const target = head % this.#maxSize
observations[target] = [timestamp, value] as const
head = head + 1
}
count = count + 1
sum = sum + value
if (value < min) {
min = value
}
if (value > max) {
max = value
}
}
const get = (context: Context.Context) => {
const clock = Context.get(context, InternalEffect.ClockRef)
const quantiles = snapshot(clock.currentTimeMillisUnsafe())
return { quantiles, count, min, max, sum }
}
const update = ([value, timestamp]: readonly [value: number, timestamp: number]) => observe(value, timestamp)
return makeHooks(get, update)
}
}
// Every operation maps the context first and the input second, so wrappers
// such as `withAttributes` address the same series for update, remove and
// register.
class MetricTransform extends Metric$ {
type: Metric.Type
readonly metric: Metric
readonly mapInput: (input: Input2, context: Context.Context) => Input
readonly mapContext: (context: Context.Context) => Context.Context
constructor(
metric: Metric,
mapInput: (input: Input2, context: Context.Context) => Input,
mapContext: (context: Context.Context) => Context.Context
) {
super(metric.id, metric.description, metric.attributes)
this.metric = metric
this.mapInput = mapInput
this.mapContext = mapContext
this.type = metric.type
}
createHooks(): Metric.Hooks {
return (this.metric as any).createHooks()
}
override valueUnsafe(context: Context.Context): State {
return this.metric.valueUnsafe(this.mapContext(context))
}
override updateUnsafe(input: Input2, context: Context.Context): void {
const mapped = this.mapContext(context)
this.metric.updateUnsafe(this.mapInput(input, mapped), mapped)
}
override modifyUnsafe(input: Input2, context: Context.Context): void {
const mapped = this.mapContext(context)
this.metric.modifyUnsafe(this.mapInput(input, mapped), mapped)
}
override removeUnsafe(context: Context.Context): void {
removeUnsafe(this.metric, this.mapContext(context))
}
override registerUnsafe(context: Context.Context, hooks: Metric.Hooks): Registration {
return registerUnsafe(this.metric, this.mapContext(context), {
get: hooks.get,
update: constVoid,
modify: constVoid
})
}
}
/**
* Returns `true` if the specified value is a `Metric`, otherwise returns `false`.
*
* **When to use**
*
* Use when you need runtime type checking and ensuring that a value
* conforms to the Metric interface before performing metric operations.
*
* **Example** (Checking metric values)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* Metric.isMetric(Metric.counter("requests")) // => true
* Metric.isMetric({ name: "requests" }) // => false
* ```
*
* @stability stable
* @category guards
* @since 4.0.0
*/
export const isMetric = (u: unknown): u is Metric =>
Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId
/**
* Represents a Counter metric that tracks cumulative numerical values over
* time. Counters can be incremented and decremented and provide a running total
* of changes.
*
* **Details**
*
* The optional `description` describes the counter, and `attributes` attach
* dimensions to it. Set `bigint` to create a counter that accepts `bigint`
* inputs. Set `incremental` to `true` to create a counter that can only ever be
* incremented.
*
* **Example** (Creating counter metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a basic counter for tracking requests
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total number of HTTP requests processed"
* })
*
* // Create an incremental-only counter for events
* const eventCounter = Metric.counter("events_processed", {
* description: "Events processed (increment only)",
* incremental: true
* })
*
* // Create a bigint counter for large values
* const bytesCounter = Metric.counter("bytes_transferred", {
* description: "Total bytes transferred",
* bigint: true,
* attributes: { service: "file-transfer" }
* })
*
* // Update counters with values
* yield* Metric.update(requestCounter, 1) // Increment by 1
* yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6)
* yield* Metric.update(eventCounter, 1) // Increment by 1
* yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes
*
* // Get current counter values
* const requestValue = yield* Metric.value(requestCounter)
* const eventValue = yield* Metric.value(eventCounter)
* const bytesValue = yield* Metric.value(bytesCounter)
*
* return { requestValue, eventValue, bytesValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.requestValue.count, result.eventValue.count, result.bytesValue.count] // => [6, 1, 1024n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const counter: {
/**
* Represents a Counter metric that tracks cumulative numerical values over
* time. Counters can be incremented and decremented and provide a running total
* of changes.
*
* **Details**
*
* The optional `description` describes the counter, and `attributes` attach
* dimensions to it. Set `bigint` to create a counter that accepts `bigint`
* inputs. Set `incremental` to `true` to create a counter that can only ever be
* incremented.
*
* **Example** (Creating counter metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a basic counter for tracking requests
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total number of HTTP requests processed"
* })
*
* // Create an incremental-only counter for events
* const eventCounter = Metric.counter("events_processed", {
* description: "Events processed (increment only)",
* incremental: true
* })
*
* // Create a bigint counter for large values
* const bytesCounter = Metric.counter("bytes_transferred", {
* description: "Total bytes transferred",
* bigint: true,
* attributes: { service: "file-transfer" }
* })
*
* // Update counters with values
* yield* Metric.update(requestCounter, 1) // Increment by 1
* yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6)
* yield* Metric.update(eventCounter, 1) // Increment by 1
* yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes
*
* // Get current counter values
* const requestValue = yield* Metric.value(requestCounter)
* const eventValue = yield* Metric.value(eventCounter)
* const bytesValue = yield* Metric.value(bytesCounter)
*
* return { requestValue, eventValue, bytesValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.requestValue.count, result.eventValue.count, result.bytesValue.count] // => [6, 1, 1024n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
(
name: string,
options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint?: false | undefined
readonly incremental?: boolean | undefined
}
): Counter
/**
* Represents a Counter metric that tracks cumulative numerical values over
* time. Counters can be incremented and decremented and provide a running total
* of changes.
*
* **Details**
*
* The optional `description` describes the counter, and `attributes` attach
* dimensions to it. Set `bigint` to create a counter that accepts `bigint`
* inputs. Set `incremental` to `true` to create a counter that can only ever be
* incremented.
*
* **Example** (Creating counter metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a basic counter for tracking requests
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total number of HTTP requests processed"
* })
*
* // Create an incremental-only counter for events
* const eventCounter = Metric.counter("events_processed", {
* description: "Events processed (increment only)",
* incremental: true
* })
*
* // Create a bigint counter for large values
* const bytesCounter = Metric.counter("bytes_transferred", {
* description: "Total bytes transferred",
* bigint: true,
* attributes: { service: "file-transfer" }
* })
*
* // Update counters with values
* yield* Metric.update(requestCounter, 1) // Increment by 1
* yield* Metric.update(requestCounter, 5) // Increment by 5 (total: 6)
* yield* Metric.update(eventCounter, 1) // Increment by 1
* yield* Metric.update(bytesCounter, 1024n) // Add 1024 bytes
*
* // Get current counter values
* const requestValue = yield* Metric.value(requestCounter)
* const eventValue = yield* Metric.value(eventCounter)
* const bytesValue = yield* Metric.value(bytesCounter)
*
* return { requestValue, eventValue, bytesValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [result.requestValue.count, result.eventValue.count, result.bytesValue.count] // => [6, 1, 1024n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
(
name: string,
options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint: true
readonly incremental?: boolean | undefined
}
): Counter
} = (name, options) => new CounterMetric(name, options) as any
/**
* Represents a `Gauge` metric that tracks and reports a single numerical value
* at a specific moment.
*
* **When to use**
*
* Use when you need a metric for instantaneous values, such as memory usage or
* CPU load.
*
* **Details**
*
* The optional `description` describes the gauge, and `attributes` attach
* dimensions to it. Set `bigint` to create a gauge that accepts `bigint`
* inputs.
*
* **Example** (Creating gauge metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a gauge for tracking memory usage
* const memoryGauge = Metric.gauge("memory_usage_mb", {
* description: "Current memory usage in megabytes"
* })
*
* // Create a gauge for CPU utilization
* const cpuGauge = Metric.gauge("cpu_utilization", {
* description: "Current CPU utilization percentage",
* attributes: { host: "server-01" }
* })
*
* // Create a bigint gauge for large values
* const diskSpaceGauge = Metric.gauge("disk_free_bytes", {
* description: "Free disk space in bytes",
* bigint: true
* })
*
* // Set gauge values (replaces current value)
* yield* Metric.update(memoryGauge, 512) // Set to 512 MB
* yield* Metric.update(cpuGauge, 85.5) // Set to 85.5%
* yield* Metric.update(diskSpaceGauge, 1024000000n) // Set to ~1GB
*
* // Modify gauge values (adds to current value)
* yield* Metric.modify(memoryGauge, 128) // Increase by 128 MB (total: 640)
* yield* Metric.modify(cpuGauge, -10.5) // Decrease by 10.5% (total: 75%)
*
* // Update with new absolute values
* yield* Metric.update(memoryGauge, 800) // Set to 800 MB (replaces 640)
*
* // Get current gauge values
* const memoryValue = yield* Metric.value(memoryGauge)
* const cpuValue = yield* Metric.value(cpuGauge)
* const diskValue = yield* Metric.value(diskSpaceGauge)
*
* return { memoryValue, cpuValue, diskValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.memoryValue.value, result.cpuValue.value, result.diskValue.value] // => [800, 75, 1024000000n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const gauge: {
/**
* Represents a `Gauge` metric that tracks and reports a single numerical value
* at a specific moment.
*
* **When to use**
*
* Use when you need a metric for instantaneous values, such as memory usage or
* CPU load.
*
* **Details**
*
* The optional `description` describes the gauge, and `attributes` attach
* dimensions to it. Set `bigint` to create a gauge that accepts `bigint`
* inputs.
*
* **Example** (Creating gauge metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a gauge for tracking memory usage
* const memoryGauge = Metric.gauge("memory_usage_mb", {
* description: "Current memory usage in megabytes"
* })
*
* // Create a gauge for CPU utilization
* const cpuGauge = Metric.gauge("cpu_utilization", {
* description: "Current CPU utilization percentage",
* attributes: { host: "server-01" }
* })
*
* // Create a bigint gauge for large values
* const diskSpaceGauge = Metric.gauge("disk_free_bytes", {
* description: "Free disk space in bytes",
* bigint: true
* })
*
* // Set gauge values (replaces current value)
* yield* Metric.update(memoryGauge, 512) // Set to 512 MB
* yield* Metric.update(cpuGauge, 85.5) // Set to 85.5%
* yield* Metric.update(diskSpaceGauge, 1024000000n) // Set to ~1GB
*
* // Modify gauge values (adds to current value)
* yield* Metric.modify(memoryGauge, 128) // Increase by 128 MB (total: 640)
* yield* Metric.modify(cpuGauge, -10.5) // Decrease by 10.5% (total: 75%)
*
* // Update with new absolute values
* yield* Metric.update(memoryGauge, 800) // Set to 800 MB (replaces 640)
*
* // Get current gauge values
* const memoryValue = yield* Metric.value(memoryGauge)
* const cpuValue = yield* Metric.value(cpuGauge)
* const diskValue = yield* Metric.value(diskSpaceGauge)
*
* return { memoryValue, cpuValue, diskValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.memoryValue.value, result.cpuValue.value, result.diskValue.value] // => [800, 75, 1024000000n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
(
name: string,
options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint?: false | undefined
}
): Gauge
/**
* Represents a `Gauge` metric that tracks and reports a single numerical value
* at a specific moment.
*
* **When to use**
*
* Use when you need a metric for instantaneous values, such as memory usage or
* CPU load.
*
* **Details**
*
* The optional `description` describes the gauge, and `attributes` attach
* dimensions to it. Set `bigint` to create a gauge that accepts `bigint`
* inputs.
*
* **Example** (Creating gauge metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a gauge for tracking memory usage
* const memoryGauge = Metric.gauge("memory_usage_mb", {
* description: "Current memory usage in megabytes"
* })
*
* // Create a gauge for CPU utilization
* const cpuGauge = Metric.gauge("cpu_utilization", {
* description: "Current CPU utilization percentage",
* attributes: { host: "server-01" }
* })
*
* // Create a bigint gauge for large values
* const diskSpaceGauge = Metric.gauge("disk_free_bytes", {
* description: "Free disk space in bytes",
* bigint: true
* })
*
* // Set gauge values (replaces current value)
* yield* Metric.update(memoryGauge, 512) // Set to 512 MB
* yield* Metric.update(cpuGauge, 85.5) // Set to 85.5%
* yield* Metric.update(diskSpaceGauge, 1024000000n) // Set to ~1GB
*
* // Modify gauge values (adds to current value)
* yield* Metric.modify(memoryGauge, 128) // Increase by 128 MB (total: 640)
* yield* Metric.modify(cpuGauge, -10.5) // Decrease by 10.5% (total: 75%)
*
* // Update with new absolute values
* yield* Metric.update(memoryGauge, 800) // Set to 800 MB (replaces 640)
*
* // Get current gauge values
* const memoryValue = yield* Metric.value(memoryGauge)
* const cpuValue = yield* Metric.value(cpuGauge)
* const diskValue = yield* Metric.value(diskSpaceGauge)
*
* return { memoryValue, cpuValue, diskValue }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.memoryValue.value, result.cpuValue.value, result.diskValue.value] // => [800, 75, 1024000000n]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
(
name: string,
options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly bigint: true
}
): Gauge
} = (name, options) => new GaugeMetric(name, options) as any
/**
* Creates a `Frequency` metric which can be used to count the number of
* occurrences of a string.
*
* **When to use**
*
* Use when you need a metric for counting how often a specific event or
* incident occurs.
*
* **Details**
*
* The optional `description` describes the frequency, and `attributes` attach
* dimensions to it. Use `preregisteredWords` to initialize occurrence counts
* for known string values before updates arrive.
*
* **Example** (Creating frequency metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a frequency metric for HTTP status codes
* const statusFrequency = Metric.frequency("http_status_codes", {
* description: "Frequency of HTTP response status codes",
* preregisteredWords: ["200", "404", "500"] // Pre-register common codes
* })
*
* // Create a frequency metric for user actions
* const userActionFrequency = Metric.frequency("user_actions", {
* description: "Frequency of user actions performed",
* attributes: { application: "web-app" }
* })
*
* // Create a frequency metric for error types
* const errorTypeFrequency = Metric.frequency("error_types", {
* description: "Frequency of different error types"
* })
*
* // Record different occurrences
* yield* Metric.update(statusFrequency, "200") // Success response
* yield* Metric.update(statusFrequency, "200") // Another success
* yield* Metric.update(statusFrequency, "404") // Not found error
* yield* Metric.update(statusFrequency, "500") // Server error
* yield* Metric.update(statusFrequency, "200") // Another success
*
* yield* Metric.update(userActionFrequency, "login")
* yield* Metric.update(userActionFrequency, "view_dashboard")
* yield* Metric.update(userActionFrequency, "login")
* yield* Metric.update(userActionFrequency, "logout")
*
* yield* Metric.update(errorTypeFrequency, "ValidationError")
* yield* Metric.update(errorTypeFrequency, "NetworkError")
* yield* Metric.update(errorTypeFrequency, "ValidationError")
*
* // Get frequency counts
* const statusCounts = yield* Metric.value(statusFrequency)
* const actionCounts = yield* Metric.value(userActionFrequency)
* const errorCounts = yield* Metric.value(errorTypeFrequency)
*
* // statusCounts.occurrences will be:
* // Map { "200" => 3, "404" => 1, "500" => 1 }
* // actionCounts.occurrences will be:
* // Map { "login" => 2, "view_dashboard" => 1, "logout" => 1 }
* // errorCounts.occurrences will be:
* // Map { "ValidationError" => 2, "NetworkError" => 1 }
*
* return { statusCounts, actionCounts, errorCounts }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const counts = [
* result.statusCounts.occurrences.get("200"),
* result.actionCounts.occurrences.get("login"),
* result.errorCounts.occurrences.get("ValidationError")
* ]
* counts // => [3, 2, 2]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const frequency = (name: string, options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly preregisteredWords?: ReadonlyArray | undefined
}): Frequency => new FrequencyMetric(name, options)
/**
* Represents a `Histogram` metric that records observations into buckets.
*
* **When to use**
*
* Use when you need a metric for measuring the distribution of values within a
* range.
*
* **Details**
*
* The optional `description` describes the histogram, and `attributes` attach
* dimensions to it. The required `boundaries` option defines the histogram
* bucket boundaries.
*
* **Example** (Creating histogram metrics)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a histogram for API response times
* const responseTimeHistogram = Metric.histogram("api_response_time", {
* description: "Distribution of API response times in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 })
* // Creates buckets: 0-50ms, 50-100ms, 100-150ms, ..., 350-400ms, 400ms+
* })
*
* // Create a histogram for request payload sizes
* const payloadSizeHistogram = Metric.histogram("payload_size", {
* description: "Distribution of request payload sizes in KB",
* boundaries: Metric.exponentialBoundaries({ start: 1, factor: 2, count: 8 }),
* // Creates exponential buckets: 1KB, 2KB, 4KB, 8KB, 16KB, 32KB, 64KB, 128KB+
* attributes: { service: "api-gateway" }
* })
*
* // Create a histogram with custom boundaries
* const customHistogram = Metric.histogram("custom_metric", {
* description: "Custom distribution metric",
* boundaries: [0.1, 0.5, 1, 2.5, 5, 10, 25, 50, 100]
* })
*
* // Record various response times
* yield* Metric.update(responseTimeHistogram, 25) // Goes in 0-50ms bucket
* yield* Metric.update(responseTimeHistogram, 75) // Goes in 50-100ms bucket
* yield* Metric.update(responseTimeHistogram, 125) // Goes in 100-150ms bucket
* yield* Metric.update(responseTimeHistogram, 200) // Goes in 150-200ms bucket
* yield* Metric.update(responseTimeHistogram, 75) // Another 50-100ms
*
* // Record payload sizes
* yield* Metric.update(payloadSizeHistogram, 3) // Goes in 2-4KB bucket
* yield* Metric.update(payloadSizeHistogram, 15) // Goes in 8-16KB bucket
* yield* Metric.update(payloadSizeHistogram, 0.5) // Goes in 0-1KB bucket
*
* // Get histogram state with distribution data
* const responseTimeState = yield* Metric.value(responseTimeHistogram)
* const payloadSizeState = yield* Metric.value(payloadSizeHistogram)
*
* // responseTimeState will contain:
* // - buckets: [[50, 1], [100, 3], [150, 4], [200, 5], ...]
* // - count: 5, min: 25, max: 200, sum: 500
* // - Useful for calculating percentiles, averages, etc.
*
* return { responseTimeState, payloadSizeState }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.responseTimeState.count, result.responseTimeState.sum, result.payloadSizeState.count]
* values // => [5, 500, 3]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const histogram = (name: string, options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly boundaries: ReadonlyArray
}): Histogram => new HistogramMetric(name, options)
/**
* Creates a `Summary` metric that records observations and calculates quantiles
* which takes a value as input and uses the current time.
*
* **When to use**
*
* Use when you need a metric that records statistical information about a set
* of values, including quantiles.
*
* **Details**
*
* The optional `description` describes the summary, and `attributes` attach
* dimensions to it. `maxAge` controls how long observations are retained,
* `maxSize` controls how many observations are kept, and `quantiles` lists the
* quantiles to calculate, such as `[0.5, 0.9]`.
*
* **Example** (Creating summary metrics)
*
* ```ts import.meta.vitest
* import { Duration, Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a summary for API response times
* const responseTimeSummary = Metric.summary("api_response_time", {
* description: "API response time quantiles over 5-minute windows",
* maxAge: Duration.minutes(5), // Keep observations for 5 minutes
* maxSize: 1000, // Maximum 1000 observations in memory
* quantiles: [0.5, 0.9, 0.95, 0.99] // 50th, 90th, 95th, 99th percentiles
* })
*
* // Create a summary for request payload sizes
* const payloadSizeSummary = Metric.summary("request_payload_size", {
* description: "Request payload size distribution over 2-minute windows",
* maxAge: Duration.minutes(2), // Shorter window for recent trends
* maxSize: 500, // Smaller buffer for memory efficiency
* quantiles: [0.5, 0.75, 0.9], // Median, 75th, 90th percentiles
* attributes: { service: "upload-service" }
* })
*
* // Record deterministic response times
* const responseTimes = [82, 96, 104, 118, 135, 170, 210, 240]
* for (const responseTime of responseTimes) {
* yield* Metric.update(responseTimeSummary, responseTime)
* }
*
* // Record some payload sizes
* yield* Metric.update(payloadSizeSummary, 1.2) // 1.2KB
* yield* Metric.update(payloadSizeSummary, 5.8) // 5.8KB
* yield* Metric.update(payloadSizeSummary, 15.6) // 15.6KB
* yield* Metric.update(payloadSizeSummary, 3.4) // 3.4KB
*
* // Get summary statistics with quantiles
* const responseStats = yield* Metric.value(responseTimeSummary)
* const payloadStats = yield* Metric.value(payloadSizeSummary)
*
* // Both summaries include quantile information for their configured windows.
*
* return { responseStats, payloadStats }
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const response = result.responseStats
* const payload = result.payloadStats
* const responseValues = [response.count, response.min, response.max, response.sum] // => [8, 82, 240, 1155]
* const payloadValues = [payload.count, payload.min, payload.max, payload.sum] // => [4, 1.2, 15.6, 26]
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const summary = (name: string, options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly maxAge: Duration.Input
readonly maxSize: number
readonly quantiles: ReadonlyArray
}): Summary =>
mapInput(summaryWithTimestamp(name, options), (input, context) =>
[
input,
Context.get(context, InternalEffect.ClockRef).currentTimeMillisUnsafe()
] as [number, number])
/**
* Creates a `Summary` metric that records observations with explicit
* timestamps and calculates quantiles.
*
* **When to use**
*
* Use when you need a metric that records statistical information about a set
* of values together with timestamps.
*
* **Details**
*
* Inputs to this metric are `[value, timestamp]` pairs; the current clock is
* used when reading quantiles against the configured `maxAge`.
*
* The optional `description` describes the summary, and `attributes` attach
* dimensions to it. `maxAge` controls how long observations are retained,
* `maxSize` controls how many observations are kept, and `quantiles` lists the
* quantiles to calculate, such as `[0.5, 0.9]`.
*
* **Example** (Creating summaries with explicit timestamps)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* const responseTimesSummary = Metric.summaryWithTimestamp(
* "response_times_summary",
* {
* description: "Measures the distribution of response times",
* maxAge: "60 seconds", // Retain observations for 60 seconds.
* maxSize: 1000, // Keep a maximum of 1000 observations.
* quantiles: [0.5, 0.9, 0.99] // Calculate 50th, 90th, and 99th quantiles.
* }
* )
* const metadata = [responseTimesSummary.id, responseTimesSummary.type] // => ["response_times_summary", "Summary"]
* ```
*
* @stability stable
* @category constructors
* @since 4.0.0
*/
export const summaryWithTimestamp = (name: string, options: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly maxAge: Duration.Input
readonly maxSize: number
readonly quantiles: ReadonlyArray
}): Summary<[value: number, timestamp: number]> => new SummaryMetric(name, options)
/**
* Creates a timer metric, based on a `Histogram`, which keeps track of
* durations in milliseconds.
*
* **Details**
*
* The unit of time will automatically be added to the metric as a tag (i.e.
* `"time_unit: milliseconds"`).
*
* If `options.boundaries` is not provided, the boundaries will be computed
* using `Metric.exponentialBoundaries({ start: 0.5, factor: 2, count: 35 })`.
*
* **Example** (Recording durations with a timer)
*
* ```ts import.meta.vitest
* import { Duration, Effect, Metric } from "effect"
*
* // Create a timer metric to track API request durations
* const apiRequestTimer = Metric.timer("api_request_duration", {
* description: "Duration of API requests",
* attributes: { service: "user-api" }
* })
*
* // Record a measured API operation duration
* const apiOperation = Effect.gen(function*() {
* const duration = Duration.millis(120)
* yield* Metric.update(apiRequestTimer, duration)
*
* const state = yield* Metric.value(apiRequestTimer)
* return {
* count: state.count,
* min: state.min,
* max: state.max,
* sum: state.sum
* }
* })
*
* await Effect.runPromise(
* Effect.provideService(apiOperation, Metric.MetricRegistry, new Map())
* ) // => { count: 1, min: 120, max: 120, sum: 120 }
* ```
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const timer = (name: string, options?: {
readonly description?: string | undefined
readonly attributes?: Metric.Attributes | undefined
readonly boundaries?: ReadonlyArray
}): Histogram => {
const boundaries = Predicate.isNotUndefined(options?.boundaries)
? options.boundaries
: exponentialBoundaries({ start: 0.5, factor: 2, count: 35 })
const attributes = mergeAttributes(options?.attributes, { time_unit: "milliseconds" })
const metric = new HistogramMetric(name, { ...options, boundaries, attributes })
return mapInput(metric, Duration.toMillis)
}
/**
* Retrieves the current state of the specified `Metric`.
*
* **Details**
*
* The returned state depends on the metric type. Counters return
* `CounterState` with `count` and `incremental`, gauges return
* `GaugeState` with `value`, frequencies return
* `FrequencyState` with `occurrences`, histograms return `HistogramState` with
* buckets, count, min, max, and sum, and summaries return `SummaryState` with
* quantiles, count, min, max, and sum.
*
* **Example** (Reading metric state)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const requestCounter = Metric.counter("modify_requests")
* const responseTime = Metric.histogram("response_time", {
* boundaries: [100, 500, 1000, 2000]
* })
*
* const program = Effect.gen(function*() {
* // Update metrics
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(responseTime, 750)
*
* // Get current values
* const counterState = yield* Metric.value(requestCounter)
* const histogramState = yield* Metric.value(responseTime)
* return {
* requestCount: counterState.count,
* count: histogramState.count,
* min: histogramState.min,
* max: histogramState.max,
* average: histogramState.sum / histogramState.count
* }
* })
*
* await Effect.runPromise(
* Effect.provideService(program, Metric.MetricRegistry, new Map())
* ) // => { requestCount: 1, count: 1, min: 750, max: 750, average: 750 }
* ```
*
* @stability stable
* @category getters
* @since 2.0.0
*/
export const value = (
self: Metric
): Effect =>
InternalEffect.flatMap(
InternalEffect.context(),
(context) => InternalEffect.sync(() => self.valueUnsafe(context))
)
/**
* Modifies the metric with the specified input.
*
* **Details**
*
* The behavior of `modify` depends on the metric type. Counters add the input
* value to the current count, gauges add the input value to the current gauge
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input observation.
*
* **Example** (Modifying metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const temperatureGauge = Metric.gauge("temperature")
* const requestCounter = Metric.counter("requests")
*
* const program = Effect.gen(function*() {
* // Set initial temperature
* yield* Metric.update(temperatureGauge, 20)
*
* // Modify by adding/subtracting values
* yield* Metric.modify(temperatureGauge, 5) // Now 25
* yield* Metric.modify(temperatureGauge, -3) // Now 22
*
* // For counters, modify increments by the specified amount
* yield* Metric.modify(requestCounter, 10) // Add 10 to counter
* yield* Metric.modify(requestCounter, 5) // Add 5 more (total: 15)
*
* const temp = yield* Metric.value(temperatureGauge)
* const requests = yield* Metric.value(requestCounter)
* return [temp.value, requests.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [22, 15]
* ```
*
* @stability stable
* @category mutations
* @since 3.6.5
*/
export const modify: {
/**
* Modifies the metric with the specified input.
*
* **Details**
*
* The behavior of `modify` depends on the metric type. Counters add the input
* value to the current count, gauges add the input value to the current gauge
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input observation.
*
* **Example** (Modifying metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const temperatureGauge = Metric.gauge("temperature")
* const requestCounter = Metric.counter("requests")
*
* const program = Effect.gen(function*() {
* // Set initial temperature
* yield* Metric.update(temperatureGauge, 20)
*
* // Modify by adding/subtracting values
* yield* Metric.modify(temperatureGauge, 5) // Now 25
* yield* Metric.modify(temperatureGauge, -3) // Now 22
*
* // For counters, modify increments by the specified amount
* yield* Metric.modify(requestCounter, 10) // Add 10 to counter
* yield* Metric.modify(requestCounter, 5) // Add 5 more (total: 15)
*
* const temp = yield* Metric.value(temperatureGauge)
* const requests = yield* Metric.value(requestCounter)
* return [temp.value, requests.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [22, 15]
* ```
*
* @stability stable
* @category mutations
* @since 3.6.5
*/
(input: Input): (self: Metric) => Effect
/**
* Modifies the metric with the specified input.
*
* **Details**
*
* The behavior of `modify` depends on the metric type. Counters add the input
* value to the current count, gauges add the input value to the current gauge
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input observation.
*
* **Example** (Modifying metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const temperatureGauge = Metric.gauge("temperature")
* const requestCounter = Metric.counter("requests")
*
* const program = Effect.gen(function*() {
* // Set initial temperature
* yield* Metric.update(temperatureGauge, 20)
*
* // Modify by adding/subtracting values
* yield* Metric.modify(temperatureGauge, 5) // Now 25
* yield* Metric.modify(temperatureGauge, -3) // Now 22
*
* // For counters, modify increments by the specified amount
* yield* Metric.modify(requestCounter, 10) // Add 10 to counter
* yield* Metric.modify(requestCounter, 5) // Add 5 more (total: 15)
*
* const temp = yield* Metric.value(temperatureGauge)
* const requests = yield* Metric.value(requestCounter)
* return [temp.value, requests.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [22, 15]
* ```
*
* @stability stable
* @category mutations
* @since 3.6.5
*/
(self: Metric, input: Input): Effect
} = dual<
(input: Input) => (self: Metric) => Effect,
(self: Metric, input: Input) => Effect
>(2, (self, input) =>
InternalEffect.flatMap(
InternalEffect.context(),
(context) => InternalEffect.sync(() => self.modifyUnsafe(input, context))
))
/**
* Updates the metric with the specified input.
*
* **Details**
*
* The behavior of `update` depends on the metric type. Counters add the input
* value to the current count, gauges replace the current value with the input
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input value as a new observation.
*
* **Example** (Updating metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const cpuUsage = Metric.gauge("cpu_usage_percent")
* const httpStatus = Metric.frequency("http_status_codes")
* const responseTime = Metric.histogram("response_time_ms", {
* boundaries: [100, 500, 1000, 2000]
* })
*
* const program = Effect.gen(function*() {
* // Update gauge to specific values
* yield* Metric.update(cpuUsage, 45.2)
* yield* Metric.update(cpuUsage, 67.8) // Replaces previous value
*
* // Track HTTP status code occurrences
* yield* Metric.update(httpStatus, "200")
* yield* Metric.update(httpStatus, "404")
* yield* Metric.update(httpStatus, "200") // Increments 200 count
*
* // Record response times
* yield* Metric.update(responseTime, 250)
* yield* Metric.update(responseTime, 750)
* yield* Metric.update(responseTime, 1500)
*
* // Check current states
* const cpu = yield* Metric.value(cpuUsage)
* const statuses = yield* Metric.value(httpStatus)
* const times = yield* Metric.value(responseTime)
* return [cpu.value, statuses.occurrences.get("200"), times.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [67.8, 2, 3]
* ```
*
* @stability stable
* @category mutations
* @since 2.0.0
*/
export const update: {
/**
* Updates the metric with the specified input.
*
* **Details**
*
* The behavior of `update` depends on the metric type. Counters add the input
* value to the current count, gauges replace the current value with the input
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input value as a new observation.
*
* **Example** (Updating metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const cpuUsage = Metric.gauge("cpu_usage_percent")
* const httpStatus = Metric.frequency("http_status_codes")
* const responseTime = Metric.histogram("response_time_ms", {
* boundaries: [100, 500, 1000, 2000]
* })
*
* const program = Effect.gen(function*() {
* // Update gauge to specific values
* yield* Metric.update(cpuUsage, 45.2)
* yield* Metric.update(cpuUsage, 67.8) // Replaces previous value
*
* // Track HTTP status code occurrences
* yield* Metric.update(httpStatus, "200")
* yield* Metric.update(httpStatus, "404")
* yield* Metric.update(httpStatus, "200") // Increments 200 count
*
* // Record response times
* yield* Metric.update(responseTime, 250)
* yield* Metric.update(responseTime, 750)
* yield* Metric.update(responseTime, 1500)
*
* // Check current states
* const cpu = yield* Metric.value(cpuUsage)
* const statuses = yield* Metric.value(httpStatus)
* const times = yield* Metric.value(responseTime)
* return [cpu.value, statuses.occurrences.get("200"), times.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [67.8, 2, 3]
* ```
*
* @stability stable
* @category mutations
* @since 2.0.0
*/
(input: Input): (self: Metric) => Effect
/**
* Updates the metric with the specified input.
*
* **Details**
*
* The behavior of `update` depends on the metric type. Counters add the input
* value to the current count, gauges replace the current value with the input
* value, frequencies increment the occurrence count for the input string,
* histograms record the input value in the appropriate bucket, and summaries
* record the input value as a new observation.
*
* **Example** (Updating metric values)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const cpuUsage = Metric.gauge("cpu_usage_percent")
* const httpStatus = Metric.frequency("http_status_codes")
* const responseTime = Metric.histogram("response_time_ms", {
* boundaries: [100, 500, 1000, 2000]
* })
*
* const program = Effect.gen(function*() {
* // Update gauge to specific values
* yield* Metric.update(cpuUsage, 45.2)
* yield* Metric.update(cpuUsage, 67.8) // Replaces previous value
*
* // Track HTTP status code occurrences
* yield* Metric.update(httpStatus, "200")
* yield* Metric.update(httpStatus, "404")
* yield* Metric.update(httpStatus, "200") // Increments 200 count
*
* // Record response times
* yield* Metric.update(responseTime, 250)
* yield* Metric.update(responseTime, 750)
* yield* Metric.update(responseTime, 1500)
*
* // Check current states
* const cpu = yield* Metric.value(cpuUsage)
* const statuses = yield* Metric.value(httpStatus)
* const times = yield* Metric.value(responseTime)
* return [cpu.value, statuses.occurrences.get("200"), times.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [67.8, 2, 3]
* ```
*
* @stability stable
* @category mutations
* @since 2.0.0
*/
(self: Metric, input: Input): Effect
} = dual<
(input: Input) => (self: Metric) => Effect,
(self: Metric, input: Input) => Effect
>(
2,
(self, input) =>
InternalEffect.contextWith((services) => InternalEffect.sync(() => self.updateUnsafe(input, services)))
)
/**
* Handle for a series registered with `Metric.registerUnsafe`.
*
* **Details**
*
* `remove` deletes the series from the registry it was registered into, but
* only while the registry still holds this registration. Calling it again, or
* after another registration has replaced the series, does nothing.
*
* @stability unstable
* @category models
* @since 4.0.6
*/
export interface Registration {
readonly remove: () => void
}
/**
* Removes the series of a metric for the current attributes from the metric
* registry.
*
* **When to use**
*
* Use when a series describes something that has gone away, such as a
* connection or an entity, so the registry and exporters stop tracking it.
*
* **Details**
*
* The series is identified the same way `update` identifies it: by the metric
* and its attributes merged with `CurrentMetricAttributes`. Other series of the
* same metric are left alone. A later update creates the series again from its
* initial state. Removing a series that does not exist does nothing.
*
* **Example** (Removing a series)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const connections = Metric.gauge("connections")
*
* const program = Effect.gen(function*() {
* yield* Metric.update(connections, 3).pipe(
* Effect.provideService(Metric.CurrentMetricAttributes, { pool: "a" })
* )
* yield* Metric.update(connections, 5).pipe(
* Effect.provideService(Metric.CurrentMetricAttributes, { pool: "b" })
* )
* yield* Metric.remove(connections).pipe(
* Effect.provideService(Metric.CurrentMetricAttributes, { pool: "a" })
* )
* const snapshots = yield* Metric.snapshot
* return snapshots.map((snapshot) => snapshot.attributes)
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [{ pool: "b" }]
* ```
*
* @see {@link removeUnsafe} for removing a series with an explicit `Context`
*
* @stability unstable
* @category mutations
* @since 4.0.6
*/
export const remove = (self: Metric): Effect =>
InternalEffect.contextWith((context) => InternalEffect.sync(() => removeUnsafe(self, context)))
/**
* Removes the series of a metric for the attributes in the provided `Context`
* from the metric registry in that `Context`.
*
* **When to use**
*
* Use when a low-level integration already holds a `Context`. Use `remove`
* in application code.
*
* @see {@link remove} for the `Effect` version
*
* @stability unstable
* @category mutations
* @since 4.0.6
*/
export const removeUnsafe = (self: Metric, context: Context.Context): void =>
(self as Metric$).removeUnsafe(context)
/**
* Registers a series backed by caller-supplied hooks, for gauges whose value is
* read when metrics are snapshotted rather than pushed with updates.
*
* **When to use**
*
* Use when the value already lives in some data structure, such as a queue
* size, and updating a metric on every change would cost more than reading the
* value at export time.
*
* **Details**
*
* The series is identified as in `remove`, using the attributes and registry
* from `context`, and replaces any series with the same identity. `hooks.get`
* is called each time the series is read, for example by `snapshot` or an
* exporter. The returned {@link Registration} removes it again.
* `update` and `modify` on this series do nothing; the supplied mutation hooks
* are ignored.
*
* **Gotchas**
*
* The registry keeps `hooks.get` reachable until the series is removed, along
* with anything it closes over. Call `remove` once the source goes away.
*
* **Example** (Reporting a queue size on snapshot)
*
* ```ts import.meta.vitest
* import { Context, Metric } from "effect"
*
* const context = Context.make(Metric.MetricRegistry, new Map())
* const queueSize = Metric.gauge("queue_size")
* const queue: Array = ["a", "b"]
*
* const registration = Metric.registerUnsafe(queueSize, context, {
* get: () => ({ value: queue.length }),
* update: () => {},
* modify: () => {}
* })
* queue.push("c")
* const sizes = Metric.snapshotUnsafe(context).map((snapshot) => snapshot.state)
* registration.remove()
*
* sizes // => [{ value: 3 }]
* Metric.snapshotUnsafe(context).length // => 0
* ```
*
* @see {@link remove} for removing a series updated with `update`
*
* @stability unstable
* @category mutations
* @since 4.0.6
*/
export const registerUnsafe = (
self: Metric,
context: Context.Context,
hooks: Metric.Hooks
): Registration => (self as Metric$).registerUnsafe(context, hooks)
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of the specified new type, which must be transformable to the input type of
* this metric.
*
* **Example** (Mapping metric inputs)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const durationHistogram = Metric.histogram("request_duration_ms", {
* description: "Request duration in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 })
* })
*
* // Accept duration strings while recording numeric milliseconds
* const durationStringHistogram = Metric.mapInput(
* durationHistogram,
* (input: string) => Number(input)
* )
*
* const program = Effect.gen(function*() {
* yield* Metric.update(durationStringHistogram, "250")
* return yield* Metric.value(durationStringHistogram)
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [value.count, value.sum] // => [1, 250]
* ```
*
* @stability stable
* @category annotations
* @since 2.0.0
*/
export const mapInput: {
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of the specified new type, which must be transformable to the input type of
* this metric.
*
* **Example** (Mapping metric inputs)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const durationHistogram = Metric.histogram("request_duration_ms", {
* description: "Request duration in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 })
* })
*
* // Accept duration strings while recording numeric milliseconds
* const durationStringHistogram = Metric.mapInput(
* durationHistogram,
* (input: string) => Number(input)
* )
*
* const program = Effect.gen(function*() {
* yield* Metric.update(durationStringHistogram, "250")
* return yield* Metric.value(durationStringHistogram)
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [value.count, value.sum] // => [1, 250]
* ```
*
* @stability stable
* @category annotations
* @since 2.0.0
*/
(f: (input: Input2, context: Context.Context) => Input): (self: Metric) => Metric
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of the specified new type, which must be transformable to the input type of
* this metric.
*
* **Example** (Mapping metric inputs)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const durationHistogram = Metric.histogram("request_duration_ms", {
* description: "Request duration in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 })
* })
*
* // Accept duration strings while recording numeric milliseconds
* const durationStringHistogram = Metric.mapInput(
* durationHistogram,
* (input: string) => Number(input)
* )
*
* const program = Effect.gen(function*() {
* yield* Metric.update(durationStringHistogram, "250")
* return yield* Metric.value(durationStringHistogram)
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [value.count, value.sum] // => [1, 250]
* ```
*
* @stability stable
* @category annotations
* @since 2.0.0
*/
(
self: Metric,
f: (input: Input2, context: Context.Context) => Input
): Metric
} = dual<
(
f: (input: Input2, context: Context.Context) => Input
) => (self: Metric) => Metric,
(
self: Metric,
f: (input: Input2, context: Context.Context) => Input
) => Metric
>(2, (
self: Metric,
f: (input: Input2, context: Context.Context) => Input
): Metric => new MetricTransform(self, f, identity))
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of any type, and translates them to updates with the specified constant
* update value.
*
* **Example** (Ignoring inputs with a constant value)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* // Create a counter that normally expects a number increment
* const requestCounter = Metric.counter("total_requests", {
* description: "Total number of requests processed"
* })
*
* // Create a version that always increments by 1, regardless of input
* const simpleRequestCounter = Metric.withConstantInput(requestCounter, 1)
*
* const program = Effect.gen(function*() {
* // These all increment the counter by 1, ignoring the input value
* yield* Metric.update(simpleRequestCounter, "any string")
* yield* Metric.update(simpleRequestCounter, { complex: "object" })
* yield* Metric.update(simpleRequestCounter, 999) // Still increments by 1
*
* const value = yield* Metric.value(simpleRequestCounter)
* return value // Counter state will show count: 3
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const count = value.count // => 3
* ```
*
* @stability stable
* @category mapping
* @since 2.0.0
*/
export const withConstantInput: {
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of any type, and translates them to updates with the specified constant
* update value.
*
* **Example** (Ignoring inputs with a constant value)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* // Create a counter that normally expects a number increment
* const requestCounter = Metric.counter("total_requests", {
* description: "Total number of requests processed"
* })
*
* // Create a version that always increments by 1, regardless of input
* const simpleRequestCounter = Metric.withConstantInput(requestCounter, 1)
*
* const program = Effect.gen(function*() {
* // These all increment the counter by 1, ignoring the input value
* yield* Metric.update(simpleRequestCounter, "any string")
* yield* Metric.update(simpleRequestCounter, { complex: "object" })
* yield* Metric.update(simpleRequestCounter, 999) // Still increments by 1
*
* const value = yield* Metric.value(simpleRequestCounter)
* return value // Counter state will show count: 3
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const count = value.count // => 3
* ```
*
* @stability stable
* @category mapping
* @since 2.0.0
*/
(input: Input): (self: Metric) => Metric
/**
* Returns a new metric that is powered by this one, but which accepts updates
* of any type, and translates them to updates with the specified constant
* update value.
*
* **Example** (Ignoring inputs with a constant value)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* // Create a counter that normally expects a number increment
* const requestCounter = Metric.counter("total_requests", {
* description: "Total number of requests processed"
* })
*
* // Create a version that always increments by 1, regardless of input
* const simpleRequestCounter = Metric.withConstantInput(requestCounter, 1)
*
* const program = Effect.gen(function*() {
* // These all increment the counter by 1, ignoring the input value
* yield* Metric.update(simpleRequestCounter, "any string")
* yield* Metric.update(simpleRequestCounter, { complex: "object" })
* yield* Metric.update(simpleRequestCounter, 999) // Still increments by 1
*
* const value = yield* Metric.value(simpleRequestCounter)
* return value // Counter state will show count: 3
* })
*
* const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const count = value.count // => 3
* ```
*
* @stability stable
* @category mapping
* @since 2.0.0
*/
(self: Metric, input: Input): Metric
} = dual<
(input: Input) => (self: Metric) => Metric,
(self: Metric, input: Input) => Metric
>(2, (self, input) => mapInput(self, () => input))
/**
* Returns a new metric that applies the specified attributes to all operations.
*
* **Details**
*
* Attributes are key-value pairs that provide additional context for metrics,
* enabling filtering, grouping, and more detailed analysis. Each combination
* of attribute values creates a separate metric series.
*
* **Example** (Applying metric attributes)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total HTTP requests"
* })
*
* // Create tagged versions of the metric
* const getRequests = Metric.withAttributes(requestCounter, {
* method: "GET",
* endpoint: "/api/users"
* })
*
* const postRequests = Metric.withAttributes(requestCounter, {
* method: "POST",
* endpoint: "/api/users"
* })
*
* const program = Effect.gen(function*() {
* // These will be tracked as separate metric series
* yield* Metric.update(getRequests, 1) // http_requests_total{method="GET", endpoint="/api/users"}
* yield* Metric.update(postRequests, 1) // http_requests_total{method="POST", endpoint="/api/users"}
* yield* Metric.update(getRequests, 1) // Increments the GET counter
*
* // You can also chain attributes
* const taggedMetric = requestCounter.pipe(
* Metric.withAttributes({ service: "user-api" }),
* Metric.withAttributes({ version: "v1" })
* )
*
* yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"}
* })
*
* const result = Effect.gen(function*() {
* yield* program
* const get = yield* Metric.value(getRequests)
* const post = yield* Metric.value(postRequests)
* return [get.count, post.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(result, Metric.MetricRegistry, new Map())) // => [2, 1]
* ```
*
* @stability stable
* @category mapping
* @since 4.0.0
*/
export const withAttributes: {
/**
* Returns a new metric that applies the specified attributes to all operations.
*
* **Details**
*
* Attributes are key-value pairs that provide additional context for metrics,
* enabling filtering, grouping, and more detailed analysis. Each combination
* of attribute values creates a separate metric series.
*
* **Example** (Applying metric attributes)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total HTTP requests"
* })
*
* // Create tagged versions of the metric
* const getRequests = Metric.withAttributes(requestCounter, {
* method: "GET",
* endpoint: "/api/users"
* })
*
* const postRequests = Metric.withAttributes(requestCounter, {
* method: "POST",
* endpoint: "/api/users"
* })
*
* const program = Effect.gen(function*() {
* // These will be tracked as separate metric series
* yield* Metric.update(getRequests, 1) // http_requests_total{method="GET", endpoint="/api/users"}
* yield* Metric.update(postRequests, 1) // http_requests_total{method="POST", endpoint="/api/users"}
* yield* Metric.update(getRequests, 1) // Increments the GET counter
*
* // You can also chain attributes
* const taggedMetric = requestCounter.pipe(
* Metric.withAttributes({ service: "user-api" }),
* Metric.withAttributes({ version: "v1" })
* )
*
* yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"}
* })
*
* const result = Effect.gen(function*() {
* yield* program
* const get = yield* Metric.value(getRequests)
* const post = yield* Metric.value(postRequests)
* return [get.count, post.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(result, Metric.MetricRegistry, new Map())) // => [2, 1]
* ```
*
* @stability stable
* @category mapping
* @since 4.0.0
*/
(attributes: Metric.Attributes): (self: Metric) => Metric
/**
* Returns a new metric that applies the specified attributes to all operations.
*
* **Details**
*
* Attributes are key-value pairs that provide additional context for metrics,
* enabling filtering, grouping, and more detailed analysis. Each combination
* of attribute values creates a separate metric series.
*
* **Example** (Applying metric attributes)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total HTTP requests"
* })
*
* // Create tagged versions of the metric
* const getRequests = Metric.withAttributes(requestCounter, {
* method: "GET",
* endpoint: "/api/users"
* })
*
* const postRequests = Metric.withAttributes(requestCounter, {
* method: "POST",
* endpoint: "/api/users"
* })
*
* const program = Effect.gen(function*() {
* // These will be tracked as separate metric series
* yield* Metric.update(getRequests, 1) // http_requests_total{method="GET", endpoint="/api/users"}
* yield* Metric.update(postRequests, 1) // http_requests_total{method="POST", endpoint="/api/users"}
* yield* Metric.update(getRequests, 1) // Increments the GET counter
*
* // You can also chain attributes
* const taggedMetric = requestCounter.pipe(
* Metric.withAttributes({ service: "user-api" }),
* Metric.withAttributes({ version: "v1" })
* )
*
* yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"}
* })
*
* const result = Effect.gen(function*() {
* yield* program
* const get = yield* Metric.value(getRequests)
* const post = yield* Metric.value(postRequests)
* return [get.count, post.count] as const
* })
*
* await Effect.runPromise(Effect.provideService(result, Metric.MetricRegistry, new Map())) // => [2, 1]
* ```
*
* @stability stable
* @category mapping
* @since 4.0.0
*/
(self: Metric, attributes: Metric.Attributes): Metric
} = dual<
(attributes: Metric.Attributes) => (self: Metric) => Metric,
(self: Metric, attributes: Metric.Attributes) => Metric
>(2, (
self: Metric,
attributes: Metric.Attributes
): Metric => {
return new MetricTransform(self, identity, addAttributesToContext(attributes))
})
// Metric Snapshots
/**
* Captures a snapshot of all registered metrics in the current context.
*
* **Details**
*
* Returns an array of metric snapshots, each containing the metric's metadata
* (name, description, type) and current state (values, counts, etc.).
*
* **Example** (Capturing metric snapshots)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create and update some metrics
* const requestCounter = Metric.counter("http_requests", {
* description: "Total HTTP requests"
* })
* const responseTime = Metric.histogram("response_time_ms", {
* description: "Response time in milliseconds",
* boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 5 })
* })
*
* // Update the metrics with some values
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(responseTime, 150)
* yield* Metric.update(responseTime, 75)
*
* // Take a snapshot of all metrics
* const snapshots = yield* Metric.snapshot
*
* return snapshots
* })
*
* const snapshots = await Effect.runPromise(
* Effect.provideService(program, Metric.MetricRegistry, new Map())
* )
* const ids = snapshots.map((snapshot) => snapshot.id).sort() // => ["http_requests", "response_time_ms"]
* ```
*
* @stability stable
* @category snapshotting
* @since 2.0.0
*/
export const snapshot: Effect> = InternalEffect.map(
InternalEffect.context(),
(context) => snapshotUnsafe(context)
)
/**
* Returns a human-readable string representation of all currently registered
* metrics in a tabular format.
*
* **Details**
*
* This debugging utility captures a snapshot of all metrics and formats them
* in an easy-to-read table showing names, descriptions, types, attributes,
* and current state values.
*
* **Example** (Dumping metrics as text)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Create and update some metrics for demonstration
* const requestCounter = Metric.counter("http_requests_total", {
* description: "Total HTTP requests"
* })
* const responseTime = Metric.gauge("response_time_ms", {
* description: "Current response time in milliseconds"
* })
* const statusFreq = Metric.frequency("http_status_codes", {
* description: "Frequency of HTTP status codes"
* })
*
* // Update metrics with some values
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(requestCounter, 1)
* yield* Metric.update(responseTime, 125)
* yield* Metric.update(statusFreq, "200")
* yield* Metric.update(statusFreq, "404")
* yield* Metric.update(statusFreq, "200")
*
* // Get formatted dump of all metrics
* const metricsReport = yield* Metric.dump
* return metricsReport
* })
*
* const report = await Effect.runPromise(
* Effect.provideService(program, Metric.MetricRegistry, new Map())
* )
* const included = [
* report.includes("http_requests_total"),
* report.includes("response_time_ms"),
* report.includes("http_status_codes")
* ]
* included // => [true, true, true]
* ```
*
* @stability stable
* @category formatting
* @since 4.0.0
*/
export const dump: Effect = InternalEffect.flatMap(InternalEffect.context(), (context) => {
const metrics = snapshotUnsafe(context)
if (metrics.length > 0) {
const maxNameLength = metrics.reduce((max, metric) => {
const length = metric.id.length
return length > max ? length : max
}, 0) + 2
const maxDescriptionLength = metrics.reduce((max, metric) => {
const length = Predicate.isNotUndefined(metric.description) ? metric.description.length : 0
return length > max ? length : max
}, 0) + 2
const maxTypeLength = metrics.reduce((max, metric) => {
const length = metric.type.length
return length > max ? length : max
}, 0) + 2
const maxAttributesLength = metrics.reduce((max, metric) => {
const length = Predicate.isNotUndefined(metric.attributes) ? attributesToString(metric.attributes).length : 0
return length > max ? length : max
}, 0) + 2
const grouped = Object.entries(Arr.groupBy(metrics, (metric) => metric.id))
const sorted = Arr.sortWith(grouped, (entry) => entry[0], _String.Order)
const rendered = sorted.map(([, group]) =>
group.map((metric) =>
renderName(metric, maxNameLength) +
renderDescription(metric, maxDescriptionLength) +
renderType(metric, maxTypeLength) +
renderAttributes(metric, maxAttributesLength) +
renderState(metric)
).join("\n")
).join("\n")
return InternalEffect.succeed(rendered)
}
return InternalEffect.succeed("")
})
/**
* Captures a snapshot of all registered metrics synchronously using the provided
* service context.
*
* **When to use**
*
* Use to read metric snapshots from an explicit `Context` in low-level
* integrations, exporters, or debugging tools that already have the context.
*
* **Details**
*
* This is the "unsafe" version that bypasses Effect's safety guarantees and requires
* manual handling of the services context. Use the safe `snapshot` function for normal
* application code.
*
* **Example** (Capturing snapshots from a context)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const requestCounter = Metric.counter("http_requests")
* const program = Effect.gen(function*() {
* yield* Metric.update(requestCounter, 1)
* const context = yield* Effect.context()
* return Metric.snapshotUnsafe(context).map((snapshot) => snapshot.id)
* })
*
* await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => ["http_requests"]
* ```
*
* @stability stable
* @category snapshotting
* @since 4.0.0
*/
export const snapshotUnsafe = (context: Context.Context): ReadonlyArray => {
const registry = Context.get(context, MetricRegistry)
return Array.from(registry.values()).map(({ hooks, ...meta }) => ({
...meta,
state: hooks.get(context)
}))
}
const renderName = (metric: Metric.Snapshot, padTo: number): string => `name=${metric.id.padEnd(padTo, " ")}`
const renderDescription = (metric: Metric.Snapshot, padTo: number): string =>
`description=${(metric.description ?? "").padEnd(padTo, " ")}`
const renderType = (metric: Metric.Snapshot, padTo: number): string => `type=${metric.type.padEnd(padTo, " ")}`
const renderAttributes = (metric: Metric.Snapshot, padTo: number): string => {
const attrs = attributesToString(metric.attributes ?? {})
const padding = " ".repeat(Math.max(0, padTo - attrs.length))
return `${attrs}${padding}`
}
const renderState = (metric: Metric.Snapshot): string => {
const prefix: string = "state="
switch (metric.type) {
case "Counter": {
const state = metric.state as CounterState
return `${prefix}[count: [${state.count}]]`
}
case "Frequency": {
const state = metric.state as FrequencyState
return `${prefix}[occurrences: ${renderKeyValues(state.occurrences)}]`
}
case "Gauge": {
const state = metric.state as GaugeState
return `${prefix}[value: [${state.value}]]`
}
case "Histogram": {
const state = metric.state as HistogramState
const buckets = `buckets: [${renderKeyValues(state.buckets)}]`
const count = `count: [${state.count}]`
const min = `min: [${state.min}]`
const max = `max: [${state.max}]`
const sum = `sum: [${state.sum}]`
return `${prefix}[${buckets}, ${count}, ${min}, ${max}, ${sum}]`
}
case "Summary": {
const state = metric.state as SummaryState
const printableQuantiles = state.quantiles.map(([key, value]) => [key, value ?? 0] as [number, number])
const quantiles = `quantiles: [${renderKeyValues(printableQuantiles)}]`
const count = `count: [${state.count}]`
const min = `min: [${state.min}]`
const max = `max: [${state.max}]`
const sum = `sum: [${state.sum}]`
return `${prefix}[${quantiles}, ${count}, ${min}, ${max}, ${sum}]`
}
}
}
const renderKeyValues = (keyValues: Iterable<[number | string, string | number]>): string =>
Array.from(keyValues).map(([key, value]) => `(${key} -> ${value})`).join(", ")
const attributesToString = (attributes: Metric.AttributeSet): string => {
const attrs = Object.entries(attributes)
const sorted = Arr.sortWith(attrs, (attr) => attr[0], _String.Order)
return `attributes=[${sorted.map(([key, value]) => `${key}: ${value}`).join(", ")}]`
}
// Metric Boundaries
/**
* Creates histogram bucket boundaries from an iterable set of values.
*
* **Details**
*
* Processes any iterable of numbers by removing duplicates, filtering out
* non-positive values, and automatically appending positive infinity as the
* final boundary.
*
* **Example** (Creating boundaries from values)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* Metric.boundariesFromIterable([-5, 0, 10, 10, 25, 50]) // => [10, 25, 50, Infinity]
* ```
*
* @stability stable
* @category constructors
* @since 4.0.0
*/
export const boundariesFromIterable = (iterable: Iterable): ReadonlyArray =>
Arr.append(Arr.filter(new Set(iterable), (n) => n > 0), Number.POSITIVE_INFINITY)
/**
* Creates histogram bucket boundaries from a linear sequence and appends
* positive infinity.
*
* **Details**
*
* Generates `count - 1` candidate boundaries using `start + index * width` for
* each zero-based index, then applies the same normalization as
* `boundariesFromIterable`: non-positive values are removed, duplicates are
* collapsed, and `Infinity` is appended.
*
* **Example** (Creating linear boundaries)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* Metric.linearBoundaries({ start: 10, width: 20, count: 5 }) // => [10, 30, 50, 70, Infinity]
* ```
*
* @stability stable
* @category constructors
* @since 4.0.0
*/
export const linearBoundaries = (options: {
readonly start: number
readonly width: number
readonly count: number
}): ReadonlyArray =>
boundariesFromIterable(Arr.makeBy(options.count - 1, (n) => options.start + n * options.width))
/**
* Creates histogram bucket boundaries with exponentially increasing values.
*
* **Details**
*
* Creates boundaries that grow exponentially, useful for metrics that span
* multiple orders of magnitude. Each boundary is calculated as start * factor^i.
*
* **Example** (Creating exponential boundaries)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* Metric.exponentialBoundaries({ start: 1, factor: 2, count: 5 }) // => [1, 2, 4, 8, Infinity]
* ```
*
* @stability stable
* @category constructors
* @since 4.0.0
*/
export const exponentialBoundaries = (options: {
readonly start: number
readonly factor: number
readonly count: number
}): ReadonlyArray =>
boundariesFromIterable(Arr.makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i)))
// Fiber Runtime Metrics
const fibersActive = gauge("child_fibers_active", {
description: "The current count of active child fibers"
})
const fibersStarted = counter("child_fibers_started", {
description: "The total number of child fibers that have been started",
incremental: true
})
const fiberSuccesses = counter("child_fiber_successes", {
description: "The total number of child fibers that have succeeded",
incremental: true
})
const fiberFailures = counter("child_fiber_failures", {
description: "The total number of child fibers that have failed",
incremental: true
})
/**
* Service key for the fiber runtime metrics service.
*
* **Example** (Accessing the fiber runtime metrics key)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* Metric.FiberRuntimeMetricsKey // => "effect/Metric/FiberRuntimeMetrics"
* ```
*
* @stability stable
* @category constants
* @since 4.0.0
*/
export const FiberRuntimeMetricsKey: "effect/Metric/FiberRuntimeMetrics" = InternalMetric.FiberRuntimeMetricsKey
/**
* Interface for the fiber runtime metrics service that tracks fiber lifecycle events.
*
* **Example** (Providing a custom fiber metrics service)
*
* ```ts import.meta.vitest
* import { Context, Exit, Metric } from "effect"
*
* const events: Array = []
* const customMetricsService: Metric.FiberRuntimeMetricsService = {
* recordFiberStart: () => {
* events.push("start")
* },
* recordFiberEnd: (_context, exit) => {
* events.push(Exit.isSuccess(exit) ? "success" : "failure")
* }
* }
*
* customMetricsService.recordFiberStart(Context.empty())
* customMetricsService.recordFiberEnd(Context.empty(), Exit.succeed("ok"))
* events // => ["start", "success"]
* ```
*
* @stability stable
* @category services
* @since 4.0.0
*/
export interface FiberRuntimeMetricsService {
readonly recordFiberStart: (context: Context.Context) => void
readonly recordFiberEnd: (context: Context.Context, exit: Exit) => void
}
/**
* Context reference for the optional service that records fiber runtime
* metrics.
*
* **When to use**
*
* Use to provide or inspect the service that receives fiber start and end
* notifications for automatic runtime metrics.
*
* **Details**
*
* When provided, the runtime can notify the service about child-fiber start and
* end events. When the reference is `undefined`, automatic fiber runtime metric
* collection is disabled.
*
* **Example** (Accessing the fiber runtime metrics service)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* const metricsService = yield* Metric.FiberRuntimeMetrics
* return metricsService === Metric.FiberRuntimeMetricsImpl
* })
*
* const result = await Effect.runPromise(
* Effect.provideService(program, Metric.FiberRuntimeMetrics, Metric.FiberRuntimeMetricsImpl)
* )
* const isDefault = result // => true
* ```
*
* @stability stable
* @category services
* @since 4.0.0
*/
export const FiberRuntimeMetrics = Context.Reference(
InternalMetric.FiberRuntimeMetricsKey,
{ fiberCached: true, defaultValue: constUndefined }
)
/**
* Default implementation of the fiber runtime metrics service.
*
* **Example** (Accessing the default fiber metrics implementation)
*
* ```ts import.meta.vitest
* import { Metric } from "effect"
*
* [
* typeof Metric.FiberRuntimeMetricsImpl.recordFiberStart,
* typeof Metric.FiberRuntimeMetricsImpl.recordFiberEnd
* ] // => ["function", "function"]
* ```
*
* @stability stable
* @category services
* @since 4.0.0
*/
export const FiberRuntimeMetricsImpl: FiberRuntimeMetricsService = {
recordFiberStart(context: Context.Context) {
fibersStarted.updateUnsafe(1, context)
fibersActive.modifyUnsafe(1, context)
},
recordFiberEnd(context: Context.Context, exit: Exit) {
fibersActive.modifyUnsafe(-1, context)
if (InternalEffect.exitIsSuccess(exit)) {
fiberSuccesses.updateUnsafe(1, context)
} else {
fiberFailures.updateUnsafe(1, context)
}
}
}
/**
* Layer that enables automatic collection of fiber runtime metrics across
* an entire Effect application.
*
* **When to use**
*
* Use when you need runtime metrics collection for all Effects in the
* application context rather than wrapping individual Effects.
*
* **Example** (Enabling runtime metrics with a layer)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* const service = yield* Metric.FiberRuntimeMetrics
* return service === Metric.FiberRuntimeMetricsImpl
* })
*
* await Effect.runPromise(Effect.provide(program, Metric.enableRuntimeMetricsLayer)) // => true
* ```
*
* @stability stable
* @category layers
* @since 4.0.0
*/
export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(FiberRuntimeMetricsImpl)
/**
* Layer that disables automatic collection of fiber runtime metrics.
*
* **Example** (Disabling runtime metrics with a layer)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* // Disable runtime metrics collection
* const disabledLayer = Metric.disableRuntimeMetricsLayer
*
* return yield* Effect.gen(function*() {
* // Check that metrics service is disabled
* const metricsService = yield* Metric.FiberRuntimeMetrics
*
* // Run some Effects - no metrics will be collected
* yield* Effect.forkChild(Effect.sleep("50 millis"))
* yield* Effect.forkChild(Effect.sleep("100 millis"))
* yield* Effect.sleep("200 millis")
*
* // Create test metrics to show they still work
* const testCounter = Metric.counter("test_counter")
* yield* Metric.update(testCounter, 1)
* const counterValue = yield* Metric.value(testCounter)
*
* return { counterValue, metricsEnabled: metricsService !== undefined }
* }).pipe(Effect.provide(disabledLayer))
* })
*
* const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map()))
* const values = [result.counterValue.count, result.metricsEnabled] // => [1, false]
* ```
*
* @stability stable
* @category layers
* @since 4.0.0
*/
export const disableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(undefined)
/**
* Enables automatic collection of fiber runtime metrics for the provided Effect.
*
* **Details**
*
* When enabled, automatically tracks fiber lifecycle metrics including active fibers,
* started fibers, successful completions, and failures. These metrics provide valuable
* insights into the concurrency patterns and health of your Effect application.
*
* **Example** (Enabling runtime metrics for an effect)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* const service = yield* Metric.FiberRuntimeMetrics
* return service === Metric.FiberRuntimeMetricsImpl
* })
*
* await Effect.runPromise(Metric.enableRuntimeMetrics(program)) // => true
* ```
*
* @stability stable
* @category providing services
* @since 4.0.0
*/
export const enableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService(
FiberRuntimeMetrics,
FiberRuntimeMetricsImpl
)
/**
* Disables automatic collection of fiber runtime metrics for the provided Effect.
*
* **When to use**
*
* Use when you need to disable runtime metrics for a specific effect while
* keeping them enabled elsewhere.
*
* **Example** (Disabling runtime metrics for an effect)
*
* ```ts import.meta.vitest
* import { Effect, Metric } from "effect"
*
* const program = Effect.gen(function*() {
* const service = yield* Metric.FiberRuntimeMetrics
* return service === undefined
* })
*
* await Effect.runPromise(Metric.disableRuntimeMetrics(program)) // => true
* ```
*
* @stability stable
* @category providing services
* @since 4.0.0
*/
export const disableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService(
FiberRuntimeMetrics,
undefined
)
// Utilities
function makeKey(
metric: Metric,
attributes: Metric.AttributeSet
): string {
// Tuple encoding keeps ids, descriptions and attributes unambiguous.
const entries = Object.entries(attributes).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
return JSON.stringify([metric.type, metric.id, metric.description, entries])
}
function makeHooks(
get: (context: Context.Context) => State,
update: (input: Input, context: Context.Context) => void,
modify?: (input: Input, context: Context.Context) => void
): Metric.Hooks {
return { get, update, modify: modify ?? update }
}
function mergeAttributes(
self: Metric.Attributes | undefined,
other: Metric.Attributes | undefined
): Metric.AttributeSet {
return { ...attributesToRecord(self), ...attributesToRecord(other) }
}
function attributesToRecord(attributes?: Metric.Attributes): Metric.AttributeSet | undefined {
if (Predicate.isNotUndefined(attributes) && Array.isArray(attributes)) {
return attributes.reduce((acc, [key, value]) => {
InternalRecord.assignProperty(acc, key, value)
return acc
}, {} as Metric.AttributeSet)
}
return attributes as Metric.AttributeSet | undefined
}
function addAttributesToContext(
attributes: Metric.Attributes
): (context: Context.Context) => Context.Context {
// Merge once per contextual attribute set, so the wrapped metric can reuse its cached series key.
let current: Metric.AttributeSet | undefined
let merged: Metric.AttributeSet = {}
return (context) => {
const extraAttributes = Context.get(context, CurrentMetricAttributes)
if (extraAttributes !== current) {
current = extraAttributes
merged = mergeAttributes(extraAttributes, attributes)
}
return Context.add(context, CurrentMetricAttributes, merged)
}
}