Files
2026-05-27 23:29:53 +01:00

263 lines
9.1 KiB
TypeScript

import type { Meter, MeterProvider } from "@opentelemetry/api";
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
createHostInstruments,
HOST_COUNTER_NAMES,
HOST_HISTOGRAM_NAMES,
} from "./otel.js";
interface RecordedInstrument {
readonly kind: "counter" | "histogram";
readonly name: string;
readonly options: Readonly<Record<string, unknown>>;
addCalls: {
value: number;
attributes?: Readonly<Record<string, unknown>>;
}[];
recordCalls: {
value: number;
attributes?: Readonly<Record<string, unknown>>;
}[];
}
interface CapturedMeter {
readonly name: string;
readonly instruments: RecordedInstrument[];
}
function buildFakeProvider(): {
provider: MeterProvider;
meters: CapturedMeter[];
} {
const meters: CapturedMeter[] = [];
const provider: MeterProvider = {
getMeter(name: string): Meter {
const captured: CapturedMeter = {
name,
instruments: [],
};
meters.push(captured);
const meter: Meter = {
createCounter(
n: string,
opts?: Readonly<Record<string, unknown>>
) {
const inst: RecordedInstrument = {
kind: "counter",
name: n,
options: opts ?? {},
addCalls: [],
recordCalls: [],
};
captured.instruments.push(inst);
return {
add(
value: number,
attributes?: Readonly<Record<string, unknown>>
) {
inst.addCalls.push({ value, attributes });
},
} as ReturnType<Meter["createCounter"]>;
},
createHistogram(
n: string,
opts?: Readonly<Record<string, unknown>>
) {
const inst: RecordedInstrument = {
kind: "histogram",
name: n,
options: opts ?? {},
addCalls: [],
recordCalls: [],
};
captured.instruments.push(inst);
return {
record(
value: number,
attributes?: Readonly<Record<string, unknown>>
) {
inst.recordCalls.push({ value, attributes });
},
} as ReturnType<Meter["createHistogram"]>;
},
} as unknown as Meter;
return meter;
},
} as MeterProvider;
return { provider, meters };
}
describe("createHostInstruments", () => {
it("creates every documented counter on the host meter", {
timeout: 5000,
}, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
for (const name of HOST_COUNTER_NAMES) {
expect(reg.counters[name]).toBeDefined();
}
expect(meters[0]?.name).toBe("@suaveplan/api-host");
expect(reg.baseAttributes).toEqual({
"host.id": "host-1",
"host.version": "0.1.0",
});
// Every counter should be present in the host meter's instrument list.
const counterNames = meters[0]?.instruments
.filter((i) => i.kind === "counter")
.map((i) => i.name);
expect(counterNames).toEqual([...HOST_COUNTER_NAMES]);
});
it("creates every documented histogram with the expected unit", {
timeout: 5000,
}, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
for (const name of HOST_HISTOGRAM_NAMES) {
expect(reg.histograms[name]).toBeDefined();
}
const histograms = meters[0]?.instruments.filter(
(i) => i.kind === "histogram"
);
const byName = new Map(histograms?.map((h) => [h.name, h]));
expect(byName.get("api.request_duration_ms")?.options).toEqual({
unit: "ms",
});
expect(byName.get("plugin.reload_duration_ms")?.options).toEqual({
unit: "ms",
});
expect(byName.get("api.body.bytes")?.options).toEqual({ unit: "By" });
expect(byName.get("api.response.bytes")?.options).toEqual({
unit: "By",
});
expect(byName.get("host.admin_call_duration_ms")?.options).toEqual({
unit: "ms",
});
});
it("forPlugin returns a stable per-plugin meter", { timeout: 5000 }, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
const first = reg.forPlugin("contacts");
const second = reg.forPlugin("contacts");
expect(first.pluginName).toBe("contacts");
expect(first.meter).toBe(second.meter);
// Only one plugin meter was created (host + 1 plugin = 2 meters).
expect(meters.filter((m) => m.name.includes("/plugin/"))).toHaveLength(
1
);
const pluginMeter = meters.find((m) =>
m.name.endsWith("/plugin/contacts")
);
expect(pluginMeter).toBeDefined();
expect(first.baseAttributes).toEqual({
"host.id": "host-1",
"host.version": "0.1.0",
"plugin.name": "contacts",
});
});
it("forPlugin caches per-plugin meters by name", { timeout: 5000 }, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
reg.forPlugin("a");
reg.forPlugin("b");
reg.forPlugin("a");
// host meter + 2 plugin meters.
expect(meters).toHaveLength(3);
});
it("dispose() clears the per-plugin meter cache", { timeout: 5000 }, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
reg.forPlugin("a");
reg.dispose();
reg.forPlugin("a");
// host meter + 1st 'a' meter + 2nd 'a' meter post-dispose = 3 total.
expect(meters).toHaveLength(3);
});
it("tenantLabelOptIn defaults to false", { timeout: 5000 }, () => {
const { provider } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
expect(reg.tenantLabelOptIn).toBe(false);
});
it("tenantLabelOptIn reflects the config flag when true", {
timeout: 5000,
}, () => {
const { provider } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
tenantLabelOptIn: true,
meterProvider: provider,
});
expect(reg.tenantLabelOptIn).toBe(true);
});
it("falls back to the global meter-provider when none is supplied", {
timeout: 5000,
}, () => {
const reg = createHostInstruments({
hostId: "host-2",
hostVersion: "0.2.0",
});
// Counters / histograms still exist (noop SDK provides them).
expect(reg.counters["api.requests_total"]).toBeDefined();
expect(reg.histograms["api.request_duration_ms"]).toBeDefined();
// Add a no-op call to prove the surface is wired through.
reg.counters["api.requests_total"].add(1);
reg.histograms["api.request_duration_ms"].record(5);
});
it("counters and histograms forward to the underlying instrument", {
timeout: 5000,
}, () => {
const { provider, meters } = buildFakeProvider();
const reg = createHostInstruments({
hostId: "host-1",
hostVersion: "0.1.0",
meterProvider: provider,
});
reg.counters["api.requests_total"].add(2, { route: "/x" });
reg.histograms["api.request_duration_ms"].record(42, { route: "/x" });
const counter = meters[0]?.instruments.find(
(i) => i.name === "api.requests_total"
);
const histogram = meters[0]?.instruments.find(
(i) => i.name === "api.request_duration_ms"
);
expect(counter?.addCalls).toEqual([
{ value: 2, attributes: { route: "/x" } },
]);
expect(histogram?.recordCalls).toEqual([
{ value: 42, attributes: { route: "/x" } },
]);
});
});