import { METRIC_COUNTER, METRICS_COUNTERS_KEY, METRICS_LATENCY_KEY, METRICS_LATENCY_MAX_SAMPLES, METRICS_QUOTE_START_PREFIX, METRICS_QUOTE_START_TTL_SECONDS, } from "@/lib/constants/metrics"; import { getRedis } from "@/lib/redis"; export type MetricsCounters = Record< (typeof METRIC_COUNTER)[keyof typeof METRIC_COUNTER], number >; export type MetricsRates = { api_success_rate: number; rpa_success_rate: number; realtime_rate: number; cache_hit_rate: number; }; export type MetricsSnapshot = MetricsRates & { counters: MetricsCounters; p95_latency_ms: number; latency_sample_count: number; }; type DoneSource = "cache" | "rpa" | "stale"; function quoteStartKey(quoteId: string): string { return `${METRICS_QUOTE_START_PREFIX}${quoteId}`; } async function incrCounter(field: string, by = 1): Promise { await getRedis().hincrby(METRICS_COUNTERS_KEY, field, by); } /** 安全埋点:不阻塞主流程,失败仅打日志 */ export function safeRecord(task: () => Promise): void { void task().catch((error) => { console.error("[metrics] 采集失败:", error); }); } /** 有效 POST 进入编排(校验通过后) */ export async function recordPostTotal(): Promise { await incrCounter(METRIC_COUNTER.POST_TOTAL); } /** L1 / L2 缓存命中 */ export async function recordCacheHit(tier: "l1" | "l2"): Promise { await incrCounter( tier === "l1" ? METRIC_COUNTER.CACHE_HIT_L1 : METRIC_COUNTER.CACHE_HIT_L2, ); } /** 触发 RPA 路径 */ export async function recordRpaTriggered(quoteId: string): Promise { await incrCounter(METRIC_COUNTER.RPA_TRIGGERED); await markQuoteStarted(quoteId); } /** 记录询价开始时间(用于 P95) */ export async function markQuoteStarted(quoteId: string): Promise { await getRedis().set( quoteStartKey(quoteId), String(Date.now()), "EX", METRICS_QUOTE_START_TTL_SECONDS, ); } async function recordDoneLatency(quoteId: string): Promise { const redis = getRedis(); const startKey = quoteStartKey(quoteId); const startedAt = await redis.get(startKey); if (startedAt) { await redis.del(startKey); const latencyMs = Math.max(0, Date.now() - Number(startedAt)); await redis.lpush(METRICS_LATENCY_KEY, String(latencyMs)); await redis.ltrim(METRICS_LATENCY_KEY, 0, METRICS_LATENCY_MAX_SAMPLES - 1); } } /** 报价完成 done(含 cache / rpa / stale) */ export async function recordPostDone(params: { quoteId: string; sourceType: DoneSource; isRealtime: boolean; }): Promise { await incrCounter(METRIC_COUNTER.POST_DONE); await incrCounter(METRIC_COUNTER.DONE_TOTAL); if (params.isRealtime && params.sourceType !== "stale") { await incrCounter(METRIC_COUNTER.DONE_REALTIME); } if (params.sourceType === "rpa") { await incrCounter(METRIC_COUNTER.RPA_DONE); } await recordDoneLatency(params.quoteId); } export function computeRate(numerator: number, denominator: number): number { if (denominator <= 0) { return 0; } return Math.round((numerator / denominator) * 10_000) / 100; } export function computeP95(samples: number[]): number { if (samples.length === 0) { return 0; } const sorted = [...samples].sort((a, b) => a - b); const index = Math.ceil(sorted.length * 0.95) - 1; return sorted[Math.max(0, index)]; } export function computeRates(counters: MetricsCounters): MetricsRates { const cacheHits = counters[METRIC_COUNTER.CACHE_HIT_L1] + counters[METRIC_COUNTER.CACHE_HIT_L2]; return { api_success_rate: computeRate( counters[METRIC_COUNTER.POST_DONE], counters[METRIC_COUNTER.POST_TOTAL], ), rpa_success_rate: computeRate( counters[METRIC_COUNTER.RPA_DONE], counters[METRIC_COUNTER.RPA_TRIGGERED], ), realtime_rate: computeRate( counters[METRIC_COUNTER.DONE_REALTIME], counters[METRIC_COUNTER.DONE_TOTAL], ), cache_hit_rate: computeRate( cacheHits, counters[METRIC_COUNTER.POST_TOTAL], ), }; } async function readCounters(): Promise { const raw = await getRedis().hgetall(METRICS_COUNTERS_KEY); const counters = {} as MetricsCounters; for (const value of Object.values(METRIC_COUNTER)) { counters[value] = Number(raw[value] ?? 0); } return counters; } async function readLatencySamples(): Promise { const raw = await getRedis().lrange(METRICS_LATENCY_KEY, 0, -1); return raw.map((item) => Number(item)).filter((item) => Number.isFinite(item)); } /** 汇总 GD-4 四指标 + P95 */ export async function getMetricsSnapshot(): Promise { const [counters, samples] = await Promise.all([ readCounters(), readLatencySamples(), ]); return { ...computeRates(counters), counters, p95_latency_ms: computeP95(samples), latency_sample_count: samples.length, }; } /** 测试用:清空指标 */ export async function resetMetrics(): Promise { const redis = getRedis(); const keys = await redis.keys(`${METRICS_QUOTE_START_PREFIX}*`); if (keys.length > 0) { await redis.del(...keys); } await redis.del(METRICS_COUNTERS_KEY, METRICS_LATENCY_KEY); }