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>; addCalls: { value: number; attributes?: Readonly>; }[]; recordCalls: { value: number; attributes?: Readonly>; }[]; } 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> ) { const inst: RecordedInstrument = { kind: "counter", name: n, options: opts ?? {}, addCalls: [], recordCalls: [], }; captured.instruments.push(inst); return { add( value: number, attributes?: Readonly> ) { inst.addCalls.push({ value, attributes }); }, } as ReturnType; }, createHistogram( n: string, opts?: Readonly> ) { const inst: RecordedInstrument = { kind: "histogram", name: n, options: opts ?? {}, addCalls: [], recordCalls: [], }; captured.instruments.push(inst); return { record( value: number, attributes?: Readonly> ) { inst.recordCalls.push({ value, attributes }); }, } as ReturnType; }, } 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" } }, ]); }); });