127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
import { type Counter, type Histogram, metrics } from "@opentelemetry/api";
|
|
|
|
/**
|
|
* OTel instruments for `@suaveplan/i18n-backend-http`.
|
|
*
|
|
* Built lazily via `metrics.getMeter('suaveplan.i18n-backend-http')`.
|
|
* All instruments are no-ops when no MeterProvider is registered (the
|
|
* default), so consumers pay zero overhead unless they wire up the SDK.
|
|
*
|
|
* In addition to emitting via OTel, this module retains a small
|
|
* in-memory state ring so spec-conformance tests can assert exact
|
|
* attribute payloads without standing up a full SDK +
|
|
* InMemoryMetricExporter pipeline. Tests call
|
|
* `resetHttpBackendMetricsState()` between cases and read
|
|
* `getHttpBackendMetricsState()` to inspect counters / histogram
|
|
* counts.
|
|
*/
|
|
|
|
export interface HttpBackendMetricSample {
|
|
readonly attributes: Record<string, string>;
|
|
}
|
|
|
|
export interface HttpBackendMetricsState {
|
|
readonly readDurationMs: HttpBackendMetricSample[];
|
|
readonly readTotal: HttpBackendMetricSample[];
|
|
readonly readErrorsTotal: HttpBackendMetricSample[];
|
|
}
|
|
|
|
interface CachedInstruments {
|
|
readonly readDurationMs: Histogram;
|
|
readonly readTotal: Counter;
|
|
readonly readErrorsTotal: Counter;
|
|
}
|
|
|
|
let cached: CachedInstruments | undefined;
|
|
|
|
const state: HttpBackendMetricsState = {
|
|
readDurationMs: [],
|
|
readTotal: [],
|
|
readErrorsTotal: [],
|
|
};
|
|
|
|
/**
|
|
* Build (or return cached) OTel instruments. Idempotent — safe to call
|
|
* from any production code path.
|
|
*/
|
|
export function getHttpBackendMetrics(): CachedInstruments {
|
|
if (cached) return cached;
|
|
const meter = metrics.getMeter("suaveplan.i18n-backend-http");
|
|
cached = {
|
|
readDurationMs: meter.createHistogram(
|
|
"i18n.backend.http.read_duration_ms",
|
|
{
|
|
description:
|
|
"Duration in ms of an HttpBackend.read call; labels: locale, namespace, outcome.",
|
|
unit: "ms",
|
|
}
|
|
),
|
|
readTotal: meter.createCounter("i18n.backend.http.read_total", {
|
|
description:
|
|
"HttpBackend.read call count; label: outcome (hit | miss | error).",
|
|
}),
|
|
readErrorsTotal: meter.createCounter(
|
|
"i18n.backend.http.read_errors_total",
|
|
{
|
|
description:
|
|
"HttpBackend.read error count; labels: code (the I18N error code), status (HTTP status when applicable).",
|
|
}
|
|
),
|
|
};
|
|
return cached;
|
|
}
|
|
|
|
/**
|
|
* Record a `read` call's duration + outcome. Mirrors the
|
|
* `read_total{outcome}` and per-error-code `read_errors_total{code,status?}`
|
|
* counters and pushes one sample into the in-memory state ring per counter
|
|
* touched. The optional `status` attribute is included on
|
|
* `read_errors_total` when the error originated from a known HTTP status
|
|
* code (404 / 5xx); transport / parse failures emit only `code`.
|
|
*/
|
|
export function recordRead(
|
|
locale: string,
|
|
namespace: string,
|
|
outcome: "hit" | "miss" | "error",
|
|
durationMs: number,
|
|
errorCode?: string,
|
|
status?: number
|
|
): void {
|
|
const ins = getHttpBackendMetrics();
|
|
const histogramAttrs: Record<string, string> = {
|
|
locale,
|
|
namespace,
|
|
outcome,
|
|
};
|
|
ins.readDurationMs.record(durationMs, histogramAttrs);
|
|
state.readDurationMs.push({ attributes: histogramAttrs });
|
|
const aggregate: Record<string, string> = { outcome };
|
|
ins.readTotal.add(1, aggregate);
|
|
state.readTotal.push({ attributes: aggregate });
|
|
if (errorCode !== undefined) {
|
|
const errorAttrs: Record<string, string> = { code: errorCode };
|
|
if (status !== undefined) {
|
|
errorAttrs["status"] = String(status);
|
|
}
|
|
ins.readErrorsTotal.add(1, errorAttrs);
|
|
state.readErrorsTotal.push({ attributes: errorAttrs });
|
|
}
|
|
}
|
|
|
|
/** Return a frozen snapshot of the in-memory state. */
|
|
export function getHttpBackendMetricsState(): HttpBackendMetricsState {
|
|
return state;
|
|
}
|
|
|
|
/** Reset the in-memory state ring. Tests call this between cases. */
|
|
export function resetHttpBackendMetricsState(): void {
|
|
state.readDurationMs.length = 0;
|
|
state.readTotal.length = 0;
|
|
state.readErrorsTotal.length = 0;
|
|
}
|
|
|
|
/** Test-only: drop the cached instruments so a fresh meter can be built. */
|
|
export function resetHttpBackendMetricsCache(): void {
|
|
cached = undefined;
|
|
}
|