Initial Commit
This commit is contained in:
13
packages/api-stack/api/api-host/src/otel/index.ts
Normal file
13
packages/api-stack/api/api-host/src/otel/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type {
|
||||
HostBaseAttributes,
|
||||
HostCounterName,
|
||||
HostHistogramName,
|
||||
HostInstruments,
|
||||
HostInstrumentsConfig,
|
||||
PluginInstruments,
|
||||
} from "./otel.js";
|
||||
export {
|
||||
createHostInstruments,
|
||||
HOST_COUNTER_NAMES,
|
||||
HOST_HISTOGRAM_NAMES,
|
||||
} from "./otel.js";
|
||||
86
packages/api-stack/api/api-host/src/otel/otel.md
Normal file
86
packages/api-stack/api/api-host/src/otel/otel.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# `otel` — host instrument registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createHostInstruments` builds the frozen bundle of every OpenTelemetry counter and histogram the host emits on a production path. The instrument list mirrors design doc §12.3 exactly — every name is a published surface and removing one is a breaking change. The package never installs an OTel SDK; the meter-provider source is, in priority order, the explicit `meterProvider` override and then the OTel global provider (which falls back to a no-op when no SDK is wired). This keeps SDK initialisation an operator concern, not a library concern.
|
||||
|
||||
The registry also vends per-plugin meters via `forPlugin(name)` — each call returns a cached meter namespaced under `@suaveplan/api-host/plugin/<name>` with the plugin label pre-attached. Stage-4 packages plug their plugin-declared `manifest.telemetry` instruments onto these meters.
|
||||
|
||||
## Features
|
||||
|
||||
- Twelve counters (`HOST_COUNTER_NAMES`) and four histograms (`HOST_HISTOGRAM_NAMES`) covering the full §12.3 metric surface.
|
||||
- Pre-tagged host meter with `host.id` + `host.version` attributes.
|
||||
- Histogram units (`ms`, `By`) set at construction time.
|
||||
- Cached per-plugin meters with `plugin.name` pre-attached.
|
||||
- `tenantLabelOptIn` flag — defaults to `false` to bound cardinality.
|
||||
- `dispose()` clears the per-plugin meter cache for graceful teardown.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createHostInstruments } from "@suaveplan/api-host/otel";
|
||||
|
||||
const instruments = createHostInstruments({
|
||||
hostId: "host-1",
|
||||
hostVersion: "0.1.0",
|
||||
});
|
||||
|
||||
instruments.counters["api.requests_total"].add(1, { route: "/billing/invoices" });
|
||||
instruments.histograms["api.request_duration_ms"].record(42, {
|
||||
route: "/billing/invoices",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Plug an explicit meter-provider (test harness or operator-supplied SDK):
|
||||
|
||||
```ts
|
||||
import {
|
||||
HOST_COUNTER_NAMES,
|
||||
createHostInstruments,
|
||||
} from "@suaveplan/api-host/otel";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const provider = new MeterProvider({
|
||||
/* readers / views configured by the operator */
|
||||
});
|
||||
|
||||
const instruments = createHostInstruments({
|
||||
hostId: "host-1",
|
||||
hostVersion: "0.1.0",
|
||||
tenantLabelOptIn: true,
|
||||
meterProvider: provider,
|
||||
});
|
||||
|
||||
// Per-plugin meter — caches per `pluginName`.
|
||||
const billingMeter = instruments.forPlugin("billing").meter;
|
||||
const errorsByReason = billingMeter.createCounter("billing.errors_total");
|
||||
|
||||
// Verify the surface is complete.
|
||||
for (const name of HOST_COUNTER_NAMES) {
|
||||
instruments.counters[name]; // present.
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createHostInstruments(config: HostInstrumentsConfig): HostInstruments`
|
||||
- `HOST_COUNTER_NAMES` — `readonly` tuple of every counter name.
|
||||
- `HOST_HISTOGRAM_NAMES` — `readonly` tuple of every histogram name.
|
||||
- `HostInstruments`:
|
||||
- `meter: Meter`
|
||||
- `tenantLabelOptIn: boolean`
|
||||
- `counters: Readonly<Record<HostCounterName, Counter>>`
|
||||
- `histograms: Readonly<Record<HostHistogramName, Histogram>>`
|
||||
- `forPlugin(name): PluginInstruments`
|
||||
- `dispose(): void`
|
||||
- `PluginInstruments`: `{ meter: Meter; pluginName: string }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The counter / histogram lists are exported as `readonly` tuples so consumers can iterate and assert against them in tests — a missing instrument shows up as a test failure, not a runtime no-op. Per-plugin meters are cached by name to avoid leaking meter objects on every reload (one meter per plugin is the design invariant — disposal happens at uninstall, mirrored by `dispose()` here clearing the whole cache). Tenant-label opt-in is gated at the call site rather than the registry level: the registry exposes the flag and the request pipeline (Chunk B) decides whether to attach `tenant` to a given measurement. This keeps the cardinality story explicit at every emit site rather than hidden behind a registry policy.
|
||||
262
packages/api-stack/api/api-host/src/otel/otel.test.ts
Normal file
262
packages/api-stack/api/api-host/src/otel/otel.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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" } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
196
packages/api-stack/api/api-host/src/otel/otel.ts
Normal file
196
packages/api-stack/api/api-host/src/otel/otel.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type {
|
||||
Meter,
|
||||
MeterProvider,
|
||||
Counter as OtelCounter,
|
||||
Histogram as OtelHistogram,
|
||||
} from "@opentelemetry/api";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
|
||||
/**
|
||||
* Every counter the host emits on a production path. The list mirrors
|
||||
* design doc §12.3 exactly so a missing entry is a spec deviation and
|
||||
* a CI gate. Names follow `<domain>.<noun>` per OTel naming conventions.
|
||||
*
|
||||
* Counters are monotonically increasing — they only `.add(n, labels)`.
|
||||
* Histograms (see {@link HOST_HISTOGRAM_NAMES}) handle distributions.
|
||||
*/
|
||||
export const HOST_COUNTER_NAMES = [
|
||||
"api.requests_total",
|
||||
"api.errors_total",
|
||||
"api.auth.failures_total",
|
||||
"api.rbac.denials_total",
|
||||
"api.tenant.unresolved_total",
|
||||
"api.validation.failures_total",
|
||||
"api.idempotency.replays_total",
|
||||
"api.rate_limit.exceeded_total",
|
||||
"plugin.lifecycle_transitions_total",
|
||||
"plugin.reload_total",
|
||||
"plugin.unsafe_query_total",
|
||||
"host.admin_action_total",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Every histogram the host emits on a production path. Distribution
|
||||
* metrics — request duration, body / response sizes, reload duration.
|
||||
*/
|
||||
export const HOST_HISTOGRAM_NAMES = [
|
||||
"api.request_duration_ms",
|
||||
"api.body.bytes",
|
||||
"api.response.bytes",
|
||||
"plugin.reload_duration_ms",
|
||||
"host.admin_call_duration_ms",
|
||||
] as const;
|
||||
|
||||
export type HostCounterName = (typeof HOST_COUNTER_NAMES)[number];
|
||||
export type HostHistogramName = (typeof HOST_HISTOGRAM_NAMES)[number];
|
||||
|
||||
/**
|
||||
* Configuration consumed by {@link createHostInstruments}.
|
||||
*/
|
||||
export interface HostInstrumentsConfig {
|
||||
/** Stable host id pre-tagged on every measurement (cardinality-bounded). */
|
||||
readonly hostId: string;
|
||||
/** Host semver pre-tagged on every measurement. */
|
||||
readonly hostVersion: string;
|
||||
/**
|
||||
* When `true`, the `tenant` label is permitted on per-request metrics.
|
||||
* Disabled by default to bound cardinality — operators opt in for
|
||||
* environments where per-tenant slicing is required.
|
||||
*/
|
||||
readonly tenantLabelOptIn?: boolean;
|
||||
/**
|
||||
* Optional OTel meter-provider override. Defaults to the global
|
||||
* `metrics.getMeterProvider()`. The package never installs an SDK —
|
||||
* SDK initialisation is operator config per the proposal §Out-of-scope.
|
||||
*/
|
||||
readonly meterProvider?: MeterProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable attribute set every host-emitted measurement should carry. The
|
||||
* SDK-level resource carries these for free; the registry surfaces them
|
||||
* here so call-site code can spread them onto labels deterministically
|
||||
* when running with the no-op SDK or a metric-exporter that does not
|
||||
* forward resource attributes.
|
||||
*/
|
||||
export type HostBaseAttributes = Readonly<{
|
||||
"host.id": string;
|
||||
"host.version": string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Frozen bundle of every instrument the host emits. Per-plugin meters
|
||||
* are constructed via {@link HostInstruments.forPlugin} so per-plugin
|
||||
* cardinality stays bounded to the documented label set.
|
||||
*/
|
||||
export interface HostInstruments {
|
||||
/** OTel meter for host-level instruments. */
|
||||
readonly meter: Meter;
|
||||
/** Base attribute set (host id + version) to spread onto measurements. */
|
||||
readonly baseAttributes: HostBaseAttributes;
|
||||
/** `true` iff the tenant label is permitted on per-request metrics. */
|
||||
readonly tenantLabelOptIn: boolean;
|
||||
readonly counters: Readonly<Record<HostCounterName, OtelCounter>>;
|
||||
readonly histograms: Readonly<Record<HostHistogramName, OtelHistogram>>;
|
||||
/**
|
||||
* Construct a per-plugin instrument view. Stage-2 ships the meter +
|
||||
* default counters; Stage-3+ may add plugin-declared instruments to
|
||||
* this surface (per `manifest.telemetry`).
|
||||
*/
|
||||
forPlugin(pluginName: string): PluginInstruments;
|
||||
/** Dispose every per-plugin meter created via `forPlugin`. */
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-plugin instrument view returned by {@link HostInstruments.forPlugin}.
|
||||
* Plugin meter names are namespaced under `@suaveplan/api-host/plugin/<name>`
|
||||
* so per-plugin metrics never collide with host-level ones.
|
||||
*/
|
||||
export interface PluginInstruments {
|
||||
readonly meter: Meter;
|
||||
readonly pluginName: string;
|
||||
/** Base attribute set with `plugin.name` added. */
|
||||
readonly baseAttributes: HostBaseAttributes & { "plugin.name": string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the host's instrument registry. The returned object is frozen;
|
||||
* counters and histograms are created at construction time so the first
|
||||
* `.add()` / `.record()` call has zero allocation cost.
|
||||
*
|
||||
* The meter-provider source is, in priority order:
|
||||
* 1. `config.meterProvider` (explicit override),
|
||||
* 2. the OTel global `metrics.getMeterProvider()` (which falls back to
|
||||
* a no-op provider when no SDK is installed).
|
||||
*
|
||||
* Operators who care about real export configure their SDK once at
|
||||
* process start and the host picks it up via the global provider.
|
||||
*/
|
||||
export function createHostInstruments(
|
||||
config: HostInstrumentsConfig
|
||||
): HostInstruments {
|
||||
const provider = config.meterProvider ?? metrics.getMeterProvider();
|
||||
const baseAttributes: HostBaseAttributes = Object.freeze({
|
||||
"host.id": config.hostId,
|
||||
"host.version": config.hostVersion,
|
||||
});
|
||||
const meter = provider.getMeter("@suaveplan/api-host");
|
||||
const counters = Object.create(null) as Record<
|
||||
HostCounterName,
|
||||
OtelCounter
|
||||
>;
|
||||
for (const name of HOST_COUNTER_NAMES) {
|
||||
counters[name] = meter.createCounter(name);
|
||||
}
|
||||
const histograms = Object.create(null) as Record<
|
||||
HostHistogramName,
|
||||
OtelHistogram
|
||||
>;
|
||||
for (const name of HOST_HISTOGRAM_NAMES) {
|
||||
histograms[name] = meter.createHistogram(name, {
|
||||
unit: histogramUnit(name),
|
||||
});
|
||||
}
|
||||
const pluginMeters = new Map<string, Meter>();
|
||||
const registry: HostInstruments = Object.freeze({
|
||||
meter,
|
||||
baseAttributes,
|
||||
tenantLabelOptIn: config.tenantLabelOptIn === true,
|
||||
counters: Object.freeze(counters),
|
||||
histograms: Object.freeze(histograms),
|
||||
forPlugin(pluginName: string): PluginInstruments {
|
||||
let m = pluginMeters.get(pluginName);
|
||||
if (m === undefined) {
|
||||
m = provider.getMeter(
|
||||
`@suaveplan/api-host/plugin/${pluginName}`
|
||||
);
|
||||
pluginMeters.set(pluginName, m);
|
||||
}
|
||||
return {
|
||||
meter: m,
|
||||
pluginName,
|
||||
baseAttributes: Object.freeze({
|
||||
...baseAttributes,
|
||||
"plugin.name": pluginName,
|
||||
}),
|
||||
};
|
||||
},
|
||||
dispose(): void {
|
||||
pluginMeters.clear();
|
||||
},
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
|
||||
const HISTOGRAM_UNITS: Readonly<Record<HostHistogramName, string>> = {
|
||||
"api.request_duration_ms": "ms",
|
||||
"plugin.reload_duration_ms": "ms",
|
||||
"api.body.bytes": "By",
|
||||
"api.response.bytes": "By",
|
||||
"host.admin_call_duration_ms": "ms",
|
||||
};
|
||||
|
||||
function histogramUnit(name: HostHistogramName): string {
|
||||
return HISTOGRAM_UNITS[name];
|
||||
}
|
||||
Reference in New Issue
Block a user