Initial Commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import type {
|
||||
Counter,
|
||||
Histogram,
|
||||
Meter,
|
||||
MeterProvider,
|
||||
} from "@opentelemetry/api";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import {
|
||||
getInstruments,
|
||||
recordOp,
|
||||
resetInstruments,
|
||||
setStorageBrowserOpfsMeterProvider,
|
||||
} from "./otel.js";
|
||||
|
||||
interface RecordedHistogram {
|
||||
name: string;
|
||||
samples: { value: number; attrs: Record<string, unknown> | undefined }[];
|
||||
}
|
||||
interface RecordedCounter {
|
||||
name: string;
|
||||
additions: { value: number; attrs: Record<string, unknown> | undefined }[];
|
||||
}
|
||||
|
||||
class StubHistogram implements Histogram {
|
||||
public readonly entry: RecordedHistogram;
|
||||
public constructor(name: string, bag: RecordedHistogram[]) {
|
||||
this.entry = { name, samples: [] };
|
||||
bag.push(this.entry);
|
||||
}
|
||||
public record(value: number, attrs?: Record<string, unknown>): void {
|
||||
this.entry.samples.push({ value, attrs });
|
||||
}
|
||||
}
|
||||
|
||||
class StubCounter implements Counter {
|
||||
public readonly entry: RecordedCounter;
|
||||
public constructor(name: string, bag: RecordedCounter[]) {
|
||||
this.entry = { name, additions: [] };
|
||||
bag.push(this.entry);
|
||||
}
|
||||
public add(value: number, attrs?: Record<string, unknown>): void {
|
||||
this.entry.additions.push({ value, attrs });
|
||||
}
|
||||
}
|
||||
|
||||
class StubMeter implements Meter {
|
||||
public histograms: RecordedHistogram[] = [];
|
||||
public counters: RecordedCounter[] = [];
|
||||
public createHistogram(name: string): Histogram {
|
||||
return new StubHistogram(name, this.histograms);
|
||||
}
|
||||
public createCounter(name: string): Counter {
|
||||
return new StubCounter(name, this.counters);
|
||||
}
|
||||
public createObservableGauge(): never {
|
||||
throw new Error("unused");
|
||||
}
|
||||
public createUpDownCounter(): never {
|
||||
throw new Error("unused");
|
||||
}
|
||||
public createObservableCounter(): never {
|
||||
throw new Error("unused");
|
||||
}
|
||||
public createObservableUpDownCounter(): never {
|
||||
throw new Error("unused");
|
||||
}
|
||||
public addBatchObservableCallback(): void {}
|
||||
public removeBatchObservableCallback(): void {}
|
||||
public createGauge(): never {
|
||||
throw new Error("unused");
|
||||
}
|
||||
}
|
||||
|
||||
class StubMeterProvider implements MeterProvider {
|
||||
public meters: StubMeter[] = [];
|
||||
public getMeter(): Meter {
|
||||
const meter = new StubMeter();
|
||||
this.meters.push(meter);
|
||||
return meter;
|
||||
}
|
||||
}
|
||||
|
||||
describe("storage-browser-opfs otel", () => {
|
||||
let provider: StubMeterProvider;
|
||||
beforeEach(() => {
|
||||
provider = new StubMeterProvider();
|
||||
setStorageBrowserOpfsMeterProvider(provider);
|
||||
});
|
||||
afterEach(() => {
|
||||
setStorageBrowserOpfsMeterProvider(undefined);
|
||||
resetInstruments();
|
||||
});
|
||||
|
||||
function findCounter(name: string): RecordedCounter | undefined {
|
||||
return provider.meters[0]?.counters.find((c) => c.name === name);
|
||||
}
|
||||
function findHistogram(name: string): RecordedHistogram | undefined {
|
||||
return provider.meters[0]?.histograms.find((h) => h.name === name);
|
||||
}
|
||||
|
||||
it("recordOp emits the documented counter and histogram set", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({
|
||||
op: "set",
|
||||
outcome: "success",
|
||||
durationMs: 12,
|
||||
bytes: 100,
|
||||
direction: "write",
|
||||
});
|
||||
const dur = findHistogram("storage_browser_opfs.op_duration_ms");
|
||||
expect(dur?.samples).toEqual([
|
||||
{ value: 12, attrs: { op: "set", outcome: "success" } },
|
||||
]);
|
||||
const writes = findCounter("storage_browser_opfs.writes_total");
|
||||
expect(writes?.additions).toEqual([
|
||||
{ value: 1, attrs: { op: "set", outcome: "success" } },
|
||||
]);
|
||||
const io = findHistogram("storage_browser_opfs.io_bytes");
|
||||
expect(io?.samples).toEqual([
|
||||
{ value: 100, attrs: { op: "set", direction: "write" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("records error counter on a failed op", { timeout: 5000 }, () => {
|
||||
recordOp({ op: "get", outcome: "error", durationMs: 3 });
|
||||
const errs = findCounter("storage_browser_opfs.errors_total");
|
||||
expect(errs?.additions).toEqual([
|
||||
{ value: 1, attrs: { op: "get", outcome: "error" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("records aborted operations on the errors counter", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({ op: "get", outcome: "aborted", durationMs: 0 });
|
||||
expect(
|
||||
findCounter("storage_browser_opfs.errors_total")?.additions[0]
|
||||
?.attrs
|
||||
).toEqual({ op: "get", outcome: "aborted" });
|
||||
});
|
||||
|
||||
it("increments reads/writes/deletes counters by op kind", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({ op: "get", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "getPartial", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "getWithMetadata", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "set", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "setPartial", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "delete", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "clear", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "exists", outcome: "success", durationMs: 1 });
|
||||
recordOp({ op: "list", outcome: "success", durationMs: 1 });
|
||||
expect(
|
||||
findCounter("storage_browser_opfs.reads_total")?.additions
|
||||
).toHaveLength(3);
|
||||
expect(
|
||||
findCounter("storage_browser_opfs.writes_total")?.additions
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
findCounter("storage_browser_opfs.deletes_total")?.additions
|
||||
).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("returns a cached instrument set on repeat getInstruments calls", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const a = getInstruments();
|
||||
const b = getInstruments();
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("rebuilds the instrument set when getInstruments(true) is called", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const a = getInstruments();
|
||||
const b = getInstruments(true);
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("omits the io_bytes record when bytes is undefined", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
recordOp({ op: "list", outcome: "success", durationMs: 5 });
|
||||
expect(
|
||||
findHistogram("storage_browser_opfs.io_bytes")?.samples ?? []
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("uses the global metrics fallback when provider is unset", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
setStorageBrowserOpfsMeterProvider(undefined);
|
||||
// Should not throw — falls back to the global no-op meter provider.
|
||||
const inst = getInstruments(true);
|
||||
expect(inst.meter).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user