# Metrics

Where logs and traces describe individual events, **metrics** describe aggregate
behaviour over time: how many requests were served, how much memory is in use,
what the latency distribution looks like. Effect keeps a process-wide metric
registry, so a `Metric` you define is just a handle — defining it twice with the
same name and type refers to the same underlying state. You declare the metric
once, `update` it as your code runs, and export the registry to Prometheus or
OTLP at the edge.

```ts
import { Effect, Metric } from "effect"

// Declare metrics once, as module-level constants.
const requestsTotal = Metric.counter("http_requests_total", {
  description: "Total HTTP requests handled"
})
const inFlight = Metric.gauge("http_requests_in_flight", {
  description: "Requests currently being processed"
})
const latency = Metric.timer("http_request_duration", {
  description: "Request handling latency"
})

const handleRequest = Effect.fn("handleRequest")(function*(route: string) {
  yield* Metric.update(requestsTotal, 1) // increment the counter
  yield* Metric.modify(inFlight, 1)      // one more request in flight

  // Effect.timed returns [duration, result]; feed the duration to the timer.
  const [duration] = yield* Effect.sleep("25 millis").pipe(Effect.timed)
  yield* Metric.update(latency, duration)

  yield* Metric.modify(inFlight, -1)     // request finished
})
```

Each metric type answers a different question, and Effect provides five:
**counter**, **gauge**, **histogram**, **summary**, and **frequency**.

## Counters

A counter tracks a cumulative value that normally only goes up — request totals,
errors, bytes processed. `Metric.update` adds to it.

```ts
import { Effect, Metric } from "effect"

const errors = Metric.counter("errors_total", {
  description: "Total errors encountered"
})

const program = Effect.gen(function*() {
  yield* Metric.update(errors, 1)
  yield* Metric.update(errors, 1)

  // Read the current state for assertions or debugging.
  const state = yield* Metric.value(errors)
  yield* Effect.logInfo("error count", { count: state.count })
})
```

Pass `bigint: true` for counters that exceed the safe integer range, or
`incremental: true` to reject decreasing updates. To count *occurrences* of an
effect regardless of a numeric input, `Metric.withConstantInput` fixes the
update value so you can pipe the metric straight onto an effect.

## Gauges

A gauge holds a single value that moves up and down — memory usage, queue depth,
active connections. `Metric.update` sets it; `Metric.modify` adds a delta.

```ts
import { Effect, Metric } from "effect"

const queueDepth = Metric.gauge("queue_depth", {
  description: "Items waiting in the queue"
})

const program = Effect.gen(function*() {
  yield* Metric.update(queueDepth, 10) // set absolute value
  yield* Metric.modify(queueDepth, 3)  // now 13
  yield* Metric.modify(queueDepth, -5) // now 8

  const state = yield* Metric.value(queueDepth)
  yield* Effect.logInfo("queue depth", { value: state.value })
})
```

## Histograms

A histogram sorts observations into buckets so you can see a *distribution* —
typically request latencies or payload sizes. You supply the bucket boundaries;
`Metric.linearBoundaries` and `Metric.exponentialBoundaries` generate common
shapes.

```ts
import { Effect, Metric } from "effect"

const responseSize = Metric.histogram("response_size_kb", {
  description: "Distribution of response sizes in KB",
  // Exponential buckets with boundaries 1, 2, 4, 8, 16, 32, 64 KB, plus an
  // overflow bucket above 64 KB.
  boundaries: Metric.exponentialBoundaries({ start: 1, factor: 2, count: 8 })
})

const program = Effect.gen(function*() {
  yield* Metric.update(responseSize, 3.2)  // lands in the 2–4 KB bucket
  yield* Metric.update(responseSize, 40)   // lands in the 32–64 KB bucket

  const state = yield* Metric.value(responseSize)
  yield* Effect.logInfo("response sizes", {
    count: state.count,
    min: state.min,
    max: state.max,
    sum: state.sum
  })
})
```

For latency specifically, `Metric.timer` is a histogram that accepts a
`Duration` and records milliseconds, with sensible default boundaries. Pair it
with `Effect.timed` (as in the opening example) to capture how long an effect
took and feed the `Duration` straight into the timer.

## Summaries

A summary, like a histogram, describes a distribution — but it computes
**quantiles** on the fly instead of bucketing, and it ages out old observations.
Use it when you care about percentiles (p50, p95, p99) over a rolling window.

```ts
import { Duration, Effect, Metric } from "effect"

const apiLatency = Metric.summary("api_latency_ms", {
  description: "API latency quantiles over a 5-minute window",
  maxAge: Duration.minutes(5),       // drop observations older than 5 minutes
  maxSize: 1000,                     // keep at most 1000 samples in memory
  quantiles: [0.5, 0.9, 0.95, 0.99] // percentiles to compute
})

const program = Effect.gen(function*() {
  yield* Metric.update(apiLatency, 120)
  yield* Metric.update(apiLatency, 240)
})
```

## Frequencies

A frequency counts how often each distinct *string* value appears — HTTP status
codes, feature flags, error tags. It maintains an occurrence count per unique
value with no need to know the values ahead of time.

```ts
import { Effect, Metric } from "effect"

const statusCodes = Metric.frequency("http_status_codes", {
  description: "Count of responses by status code"
})

const program = Effect.gen(function*() {
  yield* Metric.update(statusCodes, "200")
  yield* Metric.update(statusCodes, "200")
  yield* Metric.update(statusCodes, "404")

  const state = yield* Metric.value(statusCodes)
  // state.occurrences is a Map: { "200" => 2, "404" => 1 }
  yield* Effect.logInfo("status code counts", {
    counts: Object.fromEntries(state.occurrences)
  })
})
```

## Attributes (labels)

Attributes (Prometheus calls them labels) split a metric into independent series
by dimension — per route, per region, per status. Set fixed attributes when
declaring the metric, or derive a labelled view with `Metric.withAttributes`.

```ts
import { Effect, Metric } from "effect"

// Fixed attributes shared by every update.
const dbQueries = Metric.counter("db_queries_total", {
  attributes: { database: "primary" }
})

const recordQuery = Effect.fn("recordQuery")(function*(table: string) {
  // Add a per-call attribute without redefining the metric.
  yield* Metric.update(Metric.withAttributes(dbQueries, { table }), 1)
})
```
**Caution:** Keep attribute values low-cardinality. Each unique combination of attribute
values is a separate time series, so labelling with unbounded values (user ids,
request ids, raw URLs) can explode your metrics backend.

## Exporting metrics

Metrics live in the in-process registry until you export them. The
`effect/unstable/observability` package offers two paths.

### Prometheus

`PrometheusMetrics.layerHttp` adds a `GET /metrics` route to an Effect HTTP
router that serves the registry in Prometheus exposition format, ready to be
scraped.

```ts
import { Layer } from "effect"
import { PrometheusMetrics } from "effect/unstable/observability"

// Serve metrics on /metrics for Prometheus to scrape.
const Metrics = PrometheusMetrics.layerHttp({ path: "/metrics" })

// Provide this alongside your HttpRouter layer.
export const MetricsLayer = Metrics
```

You can also format the registry to a string on demand with
`PrometheusMetrics.format`.

### OTLP

`OtlpMetrics.layer` pushes the registry to an OTLP metrics endpoint on an
interval. Like the tracer and logger exporters, it needs an OTLP serializer and
an `HttpClient`.

```ts
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpMetrics, OtlpSerialization } from "effect/unstable/observability"

export const MetricsLayer = OtlpMetrics.layer({
  url: "http://localhost:4318/v1/metrics",
  resource: { serviceName: "checkout-api", serviceVersion: "1.0.0" },
  exportInterval: "10 seconds"
}).pipe(
  Layer.provide(OtlpSerialization.layerJson),
  Layer.provide(FetchHttpClient.layer)
)
```
**Tip:** For ad-hoc inspection during development, `Metric.snapshot` returns the current
state of every registered metric, and `Metric.dump` renders them as a
human-readable string — handy in a test or a debug endpoint.

## Tracking effects with metrics

The `Metric.update`/`Metric.modify` calls in the examples above are explicit: you
decide where and when to feed a value into a metric. That is the right tool when
the value you record is *computed* inside the effect. But the most common thing
you want to measure is the **outcome** of an effect — did it succeed, did it
fail, how long did it take — and threading that by hand (capturing the exit,
timing with `Effect.timed`, remembering to record on every branch) is tedious and
easy to get wrong.

The `Effect.track*` combinators wire an effect's outcome into a metric for you.
They are pipeable, leave the effect's success/error channels untouched, and only
record when their condition is met. Stack several of them onto one handler to get
a full picture from a single `.pipe`:

```ts
import { Effect, Metric } from "effect"

// One metric per question we want to answer about the handler.
const ok = Metric.counter("checkout_success_total", {
  description: "Successful checkouts"
}).pipe(Metric.withConstantInput(1)) // each success counts as 1

const failed = Metric.counter("checkout_failure_total", {
  description: "Failed checkouts"
}).pipe(Metric.withConstantInput(1)) // each typed failure counts as 1

const latency = Metric.timer("checkout_duration", {
  description: "Checkout handling latency"
})

declare const processCheckout: Effect.Effect<string, Error>

// A single pipe records the count of successes, the count of failures, and the
// elapsed duration — without touching the body of `processCheckout`.
const handler = processCheckout.pipe(
  Effect.trackSuccesses(ok),
  Effect.trackErrors(failed),
  Effect.trackDuration(latency)
)
```
**Note:** Each `track*` combinator returns the original effect unchanged — it taps the
relevant channel and updates the metric as a side effect. The metric's `Input`
type must match what the combinator feeds it (a value, an error, a defect, an
`Exit`, or a `Duration`), so pair each combinator with a metric of the right
shape: a counter with `Metric.withConstantInput(1)` to count occurrences, a
`Metric.frequency` to bucket by tag, or a `Metric.timer` to record durations.

### Effect.track

Records into a metric every time the effect completes, regardless of outcome.
The two-argument form takes a metric typed over `Exit<A, E>` and feeds it the
exit directly; the three-argument form takes a mapping function from the `Exit`
to the metric's input, letting you classify or extract a value first.

```ts
import { Effect, Exit, Metric } from "effect"

// Count every execution by giving the counter a constant input of 1.
const executions = Metric.counter("effect_executions").pipe(
  Metric.withConstantInput(1)
)

const counted = Effect.succeed("hello").pipe(Effect.track(executions))
// running `counted` => state.count === 1

// Or classify the exit into a frequency metric with a mapping function.
const outcomes = Metric.frequency("effect_outcomes")

const classified = Effect.succeed("result").pipe(
  Effect.track(outcomes, (exit: Exit.Exit<string, Error>) =>
    Exit.isSuccess(exit) ? "success" : "failure")
)
// running `classified` => occurrences: { "success" => 1 }
```

### Effect.trackSuccesses

Updates the metric only when the effect succeeds. With no function it feeds the
success value straight in (the metric's input must match `A`); with a mapping
function it derives the input from the success value first.

```ts
import { Effect, Metric } from "effect"

const successes = Metric.counter("successes").pipe(
  Metric.withConstantInput(1)
)

const a = Effect.succeed(42).pipe(Effect.trackSuccesses(successes))
// running `a` => { count: 1, incremental: false }

// Map the success value into the metric's input — here, record its length.
const requestSize = Metric.gauge("request_size_bytes")

const b = Effect.succeed("Hello World!").pipe(
  Effect.trackSuccesses(requestSize, (value) => value.length)
)
// running `b` => { value: 12 }
```

### Effect.trackErrors

Updates the metric only on a **typed** failure (the `E` channel) — defects are
not recorded. Without a function it feeds the error value in directly; with a
function it maps the error first, e.g. into a status string for a frequency.

```ts
import { Data, Effect, Metric } from "effect"

class ConnectionFailedError extends Data.TaggedError("ConnectionFailedError")<{}> {}

// Bucket failures by their tag using a frequency metric.
const errorTypes = Metric.frequency("error_types")

const program = Effect.fail(new ConnectionFailedError()).pipe(
  Effect.trackErrors(errorTypes, (error) => error._tag)
)
// running `program` => occurrences: { "ConnectionFailedError" => 1 }
```

### Effect.trackDefects

Updates the metric only on a **defect** (an unexpected, untyped failure raised by
`Effect.die` or a thrown exception). The optional function maps the defect — of
type `unknown` — into the metric's input.

```ts
import { Effect, Metric } from "effect"

const defects = Metric.counter("defects").pipe(Metric.withConstantInput(1))

const program = Effect.die("Critical system failure").pipe(
  Effect.trackDefects(defects)
)
// running `program` => { count: 1, incremental: false }
```

### Effect.trackDuration

Records how long the effect took to run. By default it feeds a `Duration` into
the metric (use a `Metric.timer`, which accepts a `Duration`); the optional
function converts the `Duration` first, e.g. `Duration.toMillis` for a gauge.

```ts
import { Duration, Effect, Metric } from "effect"

// A timer accepts a Duration directly.
const executionTimer = Metric.timer("execution_time")

const timed = Effect.sleep("100 millis").pipe(
  Effect.trackDuration(executionTimer)
)
// running `timed` => { count: 1, min: ~100ms, max: ~100ms, sum: ~100ms }

// Or convert the Duration to a number before recording it.
const millis = Metric.gauge("execution_millis")

const asMillis = Effect.sleep("200 millis").pipe(
  Effect.trackDuration(millis, (duration) => Duration.toMillis(duration))
)
// running `asMillis` => { value: 200 }
```

## Related

- [Logging](https://effect.plants.sh/observability/logging/) — structured event logs.
- [Tracing](https://effect.plants.sh/observability/tracing/) — distributed traces and OTLP export.
- [Services & Layers](https://effect.plants.sh/services-and-layers/) — provide exporters as layers.
- [Http API](https://effect.plants.sh/http-api/) — serve a Prometheus endpoint from your app.