263 lines
9.1 KiB
TypeScript
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" } },
|
|
]);
|
|
});
|
|
});
|