Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
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;
}