Initial Commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user