import { assert, describe, it } from "@effect/vitest" import { Duration, Fiber, Metric, String } from "effect" import * as Effect from "effect/Effect" import { TestClock } from "effect/testing" const attributes = { x: "a", y: "b" } describe("Metric", () => { it.effect("should be referentially transparent", () => Effect.gen(function*() { const id = nextId() const counter1 = Metric.counter(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) const counter2 = Metric.counter(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) const counter3 = Metric.counter(id).pipe( Metric.withAttributes({ z: "c" }), Metric.withConstantInput(1) ) yield* Effect.track(Effect.void, counter1) yield* Effect.track(Effect.void, counter2) yield* Effect.track(Effect.void, counter3) const result1 = yield* Metric.value(counter1) const result2 = yield* Metric.value(counter2) const result3 = yield* Metric.value(counter3) assert.deepStrictEqual(result1, { count: 2, incremental: false }) assert.deepStrictEqual(result2, { count: 2, incremental: false }) assert.deepStrictEqual(result3, { count: 1, incremental: false }) })) it.effect("should dump the current state of all metrics", () => Effect.gen(function*() { const counter1 = Metric.counter("counter").pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) const counter2 = Metric.counter("counter").pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) const counter3 = Metric.counter("counter").pipe( Metric.withAttributes({ z: "c" }), Metric.withConstantInput(1) ) yield* Effect.track(Effect.void, counter1) yield* Effect.track(Effect.void, counter2) yield* Effect.track(Effect.void, counter3) const result = yield* Metric.dump const expected = String.stripMargin( `|name=counter description= type=Counter attributes=[x: a, y: b] state=[count: [2]] |name=counter description= type=Counter attributes=[z: c] state=[count: [1]]` ) assert.strictEqual(result, expected) }).pipe(Effect.provideService(Metric.MetricRegistry, new Map()))) it.effect("should record fiber runtime metrics once for yielded fibers", () => { let starts = 0 let ends = 0 const service: Metric.FiberRuntimeMetricsService = { recordFiberStart: () => { starts++ }, recordFiberEnd: () => { ends++ } } return Effect.gen(function*() { const fiber = yield* Effect.forkChild(Effect.gen(function*() { yield* Effect.yieldNow yield* Effect.yieldNow })) yield* Fiber.join(fiber) assert.strictEqual(starts, 1) assert.strictEqual(ends, 1) }).pipe(Effect.provideService(Metric.FiberRuntimeMetrics, service)) }) describe("Counter", () => { it.effect("custom increment with value", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(1), counter) yield* Effect.trackSuccesses(Effect.succeed(2), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 3, incremental: false }) })) it.effect("custom increment with constant", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) yield* Effect.trackSuccesses(Effect.succeed(1), counter) yield* Effect.trackSuccesses(Effect.succeed(2), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 2, incremental: false }) })) it.effect("custom decrement with value", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(-1), counter) yield* Effect.trackSuccesses(Effect.succeed(-2), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: -3, incremental: false }) })) it.effect("custom decrement with constant", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(-1) ) yield* Effect.trackSuccesses(Effect.succeed(-1), counter) yield* Effect.trackSuccesses(Effect.succeed(-2), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: -2, incremental: false }) })) it.effect("custom increment with bigint value", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { bigint: true }).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(1)), counter) yield* Effect.trackSuccesses(Effect.succeed(BigInt(2)), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: BigInt(3), incremental: false }) })) it.effect("custom increment with bigint constant", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { bigint: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(BigInt(1)) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(1)), counter) yield* Effect.trackSuccesses(Effect.succeed(BigInt(2)), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: BigInt(2), incremental: false }) })) it.effect("custom decrement with bigint value", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { bigint: true }).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-1)), counter) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-2)), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: BigInt(-3), incremental: false }) })) it.effect("custom decrement with bigint constant", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { bigint: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(BigInt(-1)) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-1)), counter) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-2)), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: BigInt(-2), incremental: false }) })) it.effect("fails to decrement incremental counter", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { incremental: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(-1) ) yield* Effect.trackSuccesses(Effect.succeed(-1), counter) yield* Effect.trackSuccesses(Effect.succeed(-2), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 0, incremental: true }) })) it.effect("fails to decrement incremental bigint counter", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id, { bigint: true, incremental: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(BigInt(-1)) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-1)), counter) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-2)), counter) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: BigInt(0), incremental: true }) })) }) describe("Gauge", () => { it.effect("custom set with value", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(1), gauge) yield* Effect.trackSuccesses(Effect.succeed(2), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: 2 }) })) it.effect("custom set with constant", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) yield* Effect.trackSuccesses(Effect.succeed(1), gauge) yield* Effect.trackSuccesses(Effect.succeed(2), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: 1 }) })) it.effect("custom set with negative value", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(-1), gauge) yield* Effect.trackSuccesses(Effect.succeed(-2), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: -2 }) })) it.effect("custom set with negative constant", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(-1) ) yield* Effect.trackSuccesses(Effect.succeed(-1), gauge) yield* Effect.trackSuccesses(Effect.succeed(-2), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: -1 }) })) it.effect("custom set with bigint value", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id, { bigint: true }).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(1)), gauge) yield* Effect.trackSuccesses(Effect.succeed(BigInt(2)), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: BigInt(2) }) })) it.effect("custom set with bigint constant", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id, { bigint: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(BigInt(1)) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(1)), gauge) yield* Effect.trackSuccesses(Effect.succeed(BigInt(2)), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: BigInt(1) }) })) it.effect("custom set with negative bigint value", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id, { bigint: true }).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-1)), gauge) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-2)), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: BigInt(-2) }) })) it.effect("custom set with negative bigint constant", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id, { bigint: true }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(BigInt(-1)) ) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-1)), gauge) yield* Effect.trackSuccesses(Effect.succeed(BigInt(-2)), gauge) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: BigInt(-1) }) })) }) describe("Frequency", () => { it.effect("custom occurence with value", () => Effect.gen(function*() { const id = nextId() const frequency = Metric.frequency(id).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed("foo"), frequency) yield* Effect.trackSuccesses(Effect.succeed("bar"), frequency) const result = yield* Metric.value(frequency) assert.deepStrictEqual(result, { occurrences: new Map([["foo", 1], ["bar", 1]]) }) })) it.effect("custom set with constant", () => Effect.gen(function*() { const id = nextId() const frequency = Metric.frequency(id).pipe( Metric.withAttributes(attributes), Metric.withConstantInput("constant") ) yield* Effect.trackSuccesses(Effect.succeed("foo"), frequency) yield* Effect.trackSuccesses(Effect.succeed("bar"), frequency) const result = yield* Metric.value(frequency) assert.deepStrictEqual(result, { occurrences: new Map([["constant", 2]]) }) })) }) describe("Histogram", () => { it.effect("custom observe with value", () => Effect.gen(function*() { const id = nextId() const boundaries = Metric.linearBoundaries({ start: 0, width: 1, count: 10 }) const histogram = Metric.histogram(id, { boundaries }).pipe( Metric.withAttributes(attributes) ) yield* Effect.trackSuccesses(Effect.succeed(1), histogram) yield* Effect.trackSuccesses(Effect.succeed(3), histogram) const result = yield* Metric.value(histogram) assert.deepStrictEqual(result, { buckets: makeBuckets(boundaries, [1, 3]), count: 2, sum: 4, min: 1, max: 3 }) })) it.effect("custom observe with constant", () => Effect.gen(function*() { const id = nextId() const boundaries = Metric.linearBoundaries({ start: 0, width: 1, count: 10 }) const histogram = Metric.histogram(id, { boundaries }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) yield* Effect.trackSuccesses(Effect.succeed(1), histogram) yield* Effect.trackSuccesses(Effect.succeed(3), histogram) const result = yield* Metric.value(histogram) assert.deepStrictEqual(result, { buckets: makeBuckets(boundaries, [1, 1]), count: 2, sum: 2, min: 1, max: 1 }) })) it.effect("preserves precision of boundary values", () => Effect.gen(function*() { const boundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1] const histogram = Metric.histogram("precision_test", { boundaries }) const result = yield* Metric.value(histogram) result.buckets.forEach(([boundary], index) => { if (index < boundaries.length) { assert.strictEqual(boundary, boundaries[index]) } }) })) }) describe("Summary", () => { it.effect("custom observe with value", () => Effect.gen(function*() { const id = nextId() const quantiles = [0, 0.1, .9] const summary = Metric.summary(id, { maxAge: "1 minute", maxSize: 10, quantiles }).pipe(Metric.withAttributes(attributes)) yield* Effect.trackSuccesses(Effect.succeed(1), summary) yield* Effect.trackSuccesses(Effect.succeed(3), summary) const result = yield* Metric.value(summary) assert.deepStrictEqual(result, { quantiles: [[0, 1], [0.1, 1], [0.9, 3]], count: 2, sum: 4, min: 1, max: 3 }) })) it.effect("custom observe with constant", () => Effect.gen(function*() { const id = nextId() const quantiles = [0, 0.1, .9] const summary = Metric.summary(id, { maxAge: "1 minute", maxSize: 10, quantiles }).pipe( Metric.withAttributes(attributes), Metric.withConstantInput(1) ) yield* Effect.trackSuccesses(Effect.succeed(1), summary) yield* Effect.trackSuccesses(Effect.succeed(3), summary) const result = yield* Metric.value(summary) assert.deepStrictEqual(result, { quantiles: [[0, 1], [0.1, 1], [0.9, 1]], count: 2, sum: 2, min: 1, max: 1 }) })) it.effect("should return the correct quantile when the first chunk overshoots", () => Effect.gen(function*() { const id = nextId() const samples = [10, 10, 10, 10, 10, 10, 20, 30, 40, 50] const summary = Metric.summary(id, { maxAge: "1 minute", maxSize: 15, quantiles: [0.5] }) yield* Effect.forEach(samples, (sample) => Metric.update(summary, sample)) const result = yield* Metric.value(summary) assert.deepStrictEqual(result, { quantiles: [[0.5, 10]], count: 10, min: 10, max: 50, sum: 200 }) })) }) describe("track", () => { it.effect("updates on success", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.succeed(1).pipe( Effect.track(counter) ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) it.effect("updates on failure", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.fail(1).pipe( Effect.track(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) it.effect("updates on defect", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.die(1).pipe( Effect.track(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) }) describe("track", () => { it.effect("maps exits while preserving the instrumented effect result", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withConstantInput(1) ) yield* Effect.succeed(1).pipe( Effect.track(counter) ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) it.effect("updates on failure", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withConstantInput(1) ) yield* Effect.fail(1).pipe( Effect.track(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) it.effect("updates on defect", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe( Metric.withConstantInput(1) ) yield* Effect.die(1).pipe( Effect.track(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) }) describe("trackErrors", () => { it.effect("does not update on success", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.succeed(1).pipe( Effect.trackErrors(counter) ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 0, incremental: false }) })) it.effect("updates on failure", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.fail(1).pipe( Effect.trackErrors(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) it.effect("does not update on defect", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.die(1).pipe( Effect.trackErrors(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 0, incremental: false }) })) }) describe("trackDefects", () => { it.effect("does not update on success", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.succeed(1).pipe( Effect.trackDefects(counter) ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 0, incremental: false }) })) it.effect("does not update on failure", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.fail(1).pipe( Effect.trackDefects(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 0, incremental: false }) })) it.effect("updates on defect", () => Effect.gen(function*() { const id = nextId() const counter = Metric.counter(id).pipe(Metric.withConstantInput(1)) yield* Effect.die(1).pipe( Effect.trackDefects(counter), Effect.exit ) const result = yield* Metric.value(counter) assert.deepStrictEqual(result, { count: 1, incremental: false }) })) }) describe("trackDuration", () => { it.effect("tracks execution duration", () => Effect.gen(function*() { const id = nextId() const timer = Metric.timer(id) const fiber = yield* Effect.sleep("1 hour").pipe( Effect.trackDuration(timer), Effect.forkChild ) yield* TestClock.adjust("1 hour") yield* Fiber.join(fiber) const result = yield* Metric.value(timer) assert.strictEqual(result.count, 1) assert.strictEqual(result.min, Duration.toMillis(Duration.hours(1))) assert.strictEqual(result.max, Duration.toMillis(Duration.hours(1))) assert.strictEqual(result.sum, Duration.toMillis(Duration.hours(1))) })) }) describe("trackDurationWith", () => { it.effect("tracks execution duration", () => Effect.gen(function*() { const id = nextId() const gauge = Metric.gauge(id) const fiber = yield* Effect.sleep("1 hour").pipe( Effect.trackDuration(gauge, (duration) => Duration.toMinutes(duration)), Effect.forkChild ) yield* TestClock.adjust("1 hour") yield* Fiber.join(fiber) const result = yield* Metric.value(gauge) assert.deepStrictEqual(result, { value: 60 }) })) }) }) let idCounter = 0 function nextId() { return `metric-${++idCounter}` } const makeBuckets = ( boundaries: ReadonlyArray, values: ReadonlyArray ): ReadonlyArray<[number, number]> => { const results: Array<[number, number]> = [] let count = 0 let index = 0 for (const bucket of boundaries) { while (index < values.length && values[index] <= bucket) { count++ index++ } results.push([bucket, count]) } return results }