feat(core-testing): RecordingMetrics test double
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
export { RecordingTracer, type RecordedSpan } from "./recording-tracer";
|
export { RecordingTracer, type RecordedSpan } from "./recording-tracer";
|
||||||
export { RecordingLogger, type RecordedCapture } from "./recording-logger";
|
export { RecordingLogger, type RecordedCapture } from "./recording-logger";
|
||||||
|
export { RecordingMetrics, type RecordedMetric } from "./recording-metrics";
|
||||||
export { RecordingJobQueue } from "./recording-job-queue";
|
export { RecordingJobQueue } from "./recording-job-queue";
|
||||||
export { RecordingEventBus } from "./recording-event-bus";
|
export { RecordingEventBus } from "./recording-event-bus";
|
||||||
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { RecordingMetrics } from "@/instrumentation/recording-metrics";
|
||||||
|
|
||||||
|
describe("RecordingMetrics", () => {
|
||||||
|
it("records counter calls with kind, name, value, and attributes", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("http.requests", 1, { method: "GET", route: "/api/me" });
|
||||||
|
|
||||||
|
expect(recording.metrics).toHaveLength(1);
|
||||||
|
const [m] = recording.metrics;
|
||||||
|
expect(m!.kind).toBe("counter");
|
||||||
|
expect(m!.name).toBe("http.requests");
|
||||||
|
expect(m!.value).toBe(1);
|
||||||
|
expect(m!.attributes).toEqual({ method: "GET", route: "/api/me" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("counter() defaults value to 1 when omitted", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("events.signups");
|
||||||
|
|
||||||
|
expect(recording.metrics[0]!.value).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records histogram calls", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.histogram("http.duration", 123, { route: "/api/list" });
|
||||||
|
|
||||||
|
expect(recording.metrics).toHaveLength(1);
|
||||||
|
const [m] = recording.metrics;
|
||||||
|
expect(m!.kind).toBe("histogram");
|
||||||
|
expect(m!.name).toBe("http.duration");
|
||||||
|
expect(m!.value).toBe(123);
|
||||||
|
expect(m!.attributes).toEqual({ route: "/api/list" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records gauge calls", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.gauge("queue.depth", 42, { queue: "emails" });
|
||||||
|
|
||||||
|
expect(recording.metrics).toHaveLength(1);
|
||||||
|
const [m] = recording.metrics;
|
||||||
|
expect(m!.kind).toBe("gauge");
|
||||||
|
expect(m!.name).toBe("queue.depth");
|
||||||
|
expect(m!.value).toBe(42);
|
||||||
|
expect(m!.attributes).toEqual({ queue: "emails" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accumulates multiple calls across all kinds", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("a");
|
||||||
|
recording.histogram("b", 10);
|
||||||
|
recording.gauge("c", 5);
|
||||||
|
|
||||||
|
expect(recording.metrics).toHaveLength(3);
|
||||||
|
expect(recording.metrics.map((m) => m.kind)).toEqual([
|
||||||
|
"counter",
|
||||||
|
"histogram",
|
||||||
|
"gauge",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reset() clears all recorded metrics", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("x");
|
||||||
|
recording.histogram("y", 1);
|
||||||
|
expect(recording.metrics).toHaveLength(2);
|
||||||
|
|
||||||
|
recording.reset();
|
||||||
|
expect(recording.metrics).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("find() returns the first matching metric", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("a");
|
||||||
|
recording.histogram("b", 10);
|
||||||
|
|
||||||
|
const found = recording.find((m) => m.kind === "histogram");
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found!.name).toBe("b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("find() returns undefined when no metric matches", () => {
|
||||||
|
const recording = new RecordingMetrics();
|
||||||
|
recording.counter("a");
|
||||||
|
|
||||||
|
const found = recording.find((m) => m.kind === "gauge");
|
||||||
|
expect(found).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Local type alias matching the contract in @repo/core-shared/instrumentation.
|
||||||
|
// Kept inline to avoid a build-graph cycle between core-testing and core-shared.
|
||||||
|
type MetricAttributeValue = string | number | boolean;
|
||||||
|
|
||||||
|
interface IMetrics {
|
||||||
|
counter(
|
||||||
|
name: string,
|
||||||
|
value?: number,
|
||||||
|
attributes?: Record<string, MetricAttributeValue>,
|
||||||
|
): void;
|
||||||
|
histogram(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
attributes?: Record<string, MetricAttributeValue>,
|
||||||
|
): void;
|
||||||
|
gauge(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
attributes?: Record<string, MetricAttributeValue>,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecordedMetric = {
|
||||||
|
kind: "counter" | "histogram" | "gauge";
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
attributes: Record<string, MetricAttributeValue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RecordingMetrics implements IMetrics {
|
||||||
|
metrics: RecordedMetric[] = [];
|
||||||
|
|
||||||
|
counter(
|
||||||
|
name: string,
|
||||||
|
value = 1,
|
||||||
|
attributes: Record<string, MetricAttributeValue> = {},
|
||||||
|
): void {
|
||||||
|
this.metrics.push({ kind: "counter", name, value, attributes });
|
||||||
|
}
|
||||||
|
|
||||||
|
histogram(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
attributes: Record<string, MetricAttributeValue> = {},
|
||||||
|
): void {
|
||||||
|
this.metrics.push({ kind: "histogram", name, value, attributes });
|
||||||
|
}
|
||||||
|
|
||||||
|
gauge(
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
attributes: Record<string, MetricAttributeValue> = {},
|
||||||
|
): void {
|
||||||
|
this.metrics.push({ kind: "gauge", name, value, attributes });
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.metrics = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
find(
|
||||||
|
predicate: (m: RecordedMetric) => boolean,
|
||||||
|
): RecordedMetric | undefined {
|
||||||
|
return this.metrics.find(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user