1import { assert, describe, it } from "@effect/vitest";
2import { ProviderDriverKind } from "@t3tools/contracts";
3import * as Deferred from "effect/Deferred";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as Metric from "effect/Metric";
8import * as TestClock from "effect/testing/TestClock";
9
10import { withMetrics } from "./Metrics.ts";
11
12const hasMetricSnapshot = (
13 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
14 id: string,
15 attributes: Readonly<Record<string, string>>,
16) =>
17 snapshots.some(
18 (snapshot) =>
19 snapshot.id === id &&
20 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),
21 );
22
23const findHistogramSnapshot = (
24 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
25 id: string,
26 attributes: Readonly<Record<string, string>>,
27) =>
28 snapshots.find(
29 (snapshot): snapshot is Extract<Metric.Metric.Snapshot, { readonly type: "Histogram" }> =>
30 snapshot.type === "Histogram" &&
31 snapshot.id === id &&
32 Object.entries(attributes).every(([key, value]) => snapshot.attributes?.[key] === value),
33 );
34
35describe("withMetrics", () => {
36 it.effect("supports pipe-style usage", () =>
37 Effect.gen(function* () {
38 const counter = Metric.counter("with_metrics_pipe_total");
39 const timer = Metric.timer("with_metrics_pipe_duration");
40
41 const result = yield* Effect.succeed("ok").pipe(
42 withMetrics({
43 counter,
44 timer,
45 attributes: {
46 operation: "pipe",
47 },
48 }),
49 );
50
51 assert.equal(result, "ok");
52
53 const snapshots = yield* Metric.snapshot;
54 assert.equal(
55 hasMetricSnapshot(snapshots, "with_metrics_pipe_total", {
56 operation: "pipe",
57 outcome: "success",
58 }),
59 true,
60 );
61 assert.equal(
62 hasMetricSnapshot(snapshots, "with_metrics_pipe_duration", {
63 operation: "pipe",
64 }),
65 true,
66 );
67 }),
68 );
69
70 it.effect("supports direct invocation", () =>
71 Effect.gen(function* () {
72 const counter = Metric.counter("with_metrics_direct_total");
73
74 yield* withMetrics(Effect.fail("boom"), {
75 counter,
76 attributes: {
77 operation: "direct",
78 },
79 }).pipe(Effect.exit);
80
81 const snapshots = yield* Metric.snapshot;
82 assert.equal(
83 hasMetricSnapshot(snapshots, "with_metrics_direct_total", {
84 operation: "direct",
85 outcome: "failure",
86 }),
87 true,
88 );
89 }),
90 );
91
92 it.effect("evaluates attributes lazily after the wrapped effect runs", () =>
93 Effect.gen(function* () {
94 const counter = Metric.counter("with_metrics_lazy_total");
95 const timer = Metric.timer("with_metrics_lazy_duration");
96 let provider = ProviderDriverKind.make("unknown");
97 const lazyInittedProvider = ProviderDriverKind.make("codex");
98
99 yield* Effect.sync(() => {
100 provider = lazyInittedProvider;
101 }).pipe(
102 withMetrics({
103 counter,
104 timer,
105 attributes: () => ({
106 provider,
107 operation: "lazy",
108 }),
109 }),
110 );
111
112 const snapshots = yield* Metric.snapshot;
113 assert.equal(
114 hasMetricSnapshot(snapshots, "with_metrics_lazy_total", {
115 provider: lazyInittedProvider,
116 operation: "lazy",
117 outcome: "success",
118 }),
119 true,
120 );
121 assert.equal(
122 hasMetricSnapshot(snapshots, "with_metrics_lazy_duration", {
123 provider: lazyInittedProvider,
124 operation: "lazy",
125 }),
126 true,
127 );
128 }),
129 );
130
131 it.effect("counts interrupted work with an interrupt outcome and its duration", () =>
132 Effect.gen(function* () {
133 const counter = Metric.counter("with_metrics_interrupt_total");
134 const timer = Metric.timer("with_metrics_interrupt_duration");
135 const started = yield* Deferred.make<void>();
136
137 const fiber = yield* Deferred.succeed(started, undefined).pipe(
138 Effect.andThen(Effect.never),
139 withMetrics({ counter, timer, attributes: { operation: "interrupt" } }),
140 Effect.forkChild,
141 );
142 yield* Deferred.await(started);
143 yield* TestClock.adjust(Duration.millis(5));
144 yield* Fiber.interrupt(fiber);
145
146 const snapshots = yield* Metric.snapshot;
147 assert.equal(
148 hasMetricSnapshot(snapshots, "with_metrics_interrupt_total", {
149 operation: "interrupt",
150 outcome: "interrupt",
151 }),
152 true,
153 );
154 const duration = findHistogramSnapshot(snapshots, "with_metrics_interrupt_duration", {
155 operation: "interrupt",
156 });
157 assert.equal(duration?.state.count, 1);
158 assert.equal(duration?.state.sum, 5);
159 }),
160 );
161
162 it.effect("measures durations on the monotonic clock, not the wall clock", () =>
163 Effect.gen(function* () {
164 const timer = Metric.timer("with_metrics_monotonic_duration");
165 const started = yield* Deferred.make<void>();
166 const finish = yield* Deferred.make<void>();
167
168 const fiber = yield* Deferred.succeed(started, undefined).pipe(
169 Effect.andThen(Deferred.await(finish)),
170 withMetrics({ timer, attributes: { operation: "monotonic" } }),
171 Effect.forkChild,
172 );
173 yield* Deferred.await(started);
174 yield* TestClock.adjust(Duration.millis(10));
175 // A backward wall-clock correction must not shorten the measured duration.
176 yield* TestClock.setTime(0);
177 yield* Deferred.succeed(finish, undefined);
178 yield* Fiber.join(fiber);
179
180 const snapshots = yield* Metric.snapshot;
181 const duration = findHistogramSnapshot(snapshots, "with_metrics_monotonic_duration", {
182 operation: "monotonic",
183 });
184 assert.equal(duration?.state.count, 1);
185 assert.equal(duration?.state.sum, 10);
186 }),
187 );
188
189 it.effect("records timer durations from nanosecond clock readings", () =>
190 Effect.gen(function* () {
191 const duration = Duration.nanos(1_500_000n);
192 const timer = Metric.timer("with_metrics_nanos_duration");
193
194 yield* Effect.gen(function* () {
195 const fiber = yield* Effect.sleep(duration).pipe(
196 withMetrics({
197 timer,
198 attributes: {
199 operation: "nanos",
200 },
201 }),
202 Effect.forkChild,
203 );
204
205 yield* Effect.yieldNow;
206 yield* TestClock.adjust(duration);
207 yield* Fiber.join(fiber);
208 }).pipe(Effect.provide(TestClock.layer()));
209
210 const snapshots = yield* Metric.snapshot;
211 const snapshot = findHistogramSnapshot(snapshots, "with_metrics_nanos_duration", {
212 operation: "nanos",
213 });
214
215 assert.equal(snapshot?.state.count, 1);
216 assert.equal(snapshot?.state.sum, 1.5);
217 }),
218 );
219});