feat(data): 废弃 usage 表 HTTP/结算列,迁移至 settlement_snapshots 与 http_audits

- 新增迁移 20260413030000:标记 billing_status、finalized_at、request_headers 等列为 DEPRECATED
- 更新 baseline_v2.sql 同步废弃注释,BASELINE_V2_CUTOFF_VERSION 升至 20260413030000
- usage/sql.rs:inline body 阈值归零,强制所有 body 走 blob 存储;upsert 时清空 legacy header/output_price 列
- 查询层优先读 usage_settlement_snapshots 的 billing_status、finalized_at、output_price_per_1m
- runtime.rs:stale usage 处理同步写入 usage_settlement_snapshots;SELECT FOR UPDATE 改为 FOR UPDATE OF usage
- 前端:PerformanceAnalysis 页面重构为实时面板,新增 prometheus 工具函数与 monitoring API
This commit is contained in:
fawney19
2026-04-13 14:53:46 +08:00
parent e46629d11a
commit 6aa16ec792
16 changed files with 1587 additions and 87 deletions

View File

@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import {
findMetricValueNumber,
parsePrometheusSamples,
sumMetricValues,
} from '../prometheus'
describe('parsePrometheusSamples', () => {
it('parses labeled samples and finds gate metrics by suffix name', () => {
const samples = parsePrometheusSamples(`
# HELP aether_gateway_concurrency_in_flight Current number of in-flight operations.
# TYPE aether_gateway_concurrency_in_flight gauge
aether_gateway_concurrency_in_flight{gate="gateway_requests"} 7
aether_gateway_concurrency_rejected_total{gate="gateway_requests"} 12
`)
expect(
findMetricValueNumber(samples, 'concurrency_in_flight', {
gate: 'gateway_requests',
})
).toBe(7)
expect(
findMetricValueNumber(samples, 'concurrency_rejected_total', {
gate: 'gateway_requests',
})
).toBe(12)
})
it('sums fallback counters across labeled samples', () => {
const samples = parsePrometheusSamples(`
decision_remote_total{route_kind="chat",reason="local_decision_miss"} 2
decision_remote_total{route_kind="responses",reason="remote_decision_miss"} 3
`)
expect(sumMetricValues(samples, 'decision_remote_total')).toBe(5)
})
})

View File

@@ -0,0 +1,106 @@
export interface PrometheusSample {
name: string
labels: Record<string, string>
value: string
}
export function parsePrometheusSamples(text: string): PrometheusSample[] {
return text
.split(/\r?\n/)
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('#'))
.map(parsePrometheusLine)
.filter((sample): sample is PrometheusSample => sample !== null)
}
export function findMetricValueNumber(
samples: PrometheusSample[],
metricName: string,
labels: Record<string, string> = {}
): number | null {
const sample = samples.find(item =>
metricNameMatches(item.name, metricName) && labelsMatch(item.labels, labels)
)
if (!sample) {
return null
}
const value = Number(sample.value)
return Number.isFinite(value) ? value : null
}
export function sumMetricValues(
samples: PrometheusSample[],
metricName: string
): number {
return samples.reduce((total, sample) => {
if (!metricNameMatches(sample.name, metricName)) {
return total
}
const value = Number(sample.value)
return Number.isFinite(value) ? total + value : total
}, 0)
}
function metricNameMatches(actual: string, expected: string): boolean {
return actual === expected || actual.split('_').pop() === expected || actual.endsWith(`_${expected}`)
}
function labelsMatch(
actual: Record<string, string>,
expected: Record<string, string>
): boolean {
return Object.entries(expected).every(([key, value]) => actual[key] === value)
}
function parsePrometheusLine(line: string): PrometheusSample | null {
const separatorIndex = line.lastIndexOf(' ')
if (separatorIndex === -1) {
return null
}
const metric = line.slice(0, separatorIndex).trim()
const value = line.slice(separatorIndex + 1).trim()
if (!metric || !value) {
return null
}
const labelStart = metric.indexOf('{')
if (labelStart === -1 || !metric.endsWith('}')) {
return {
name: metric,
labels: {},
value,
}
}
return {
name: metric.slice(0, labelStart),
labels: parseLabels(metric.slice(labelStart + 1, -1)),
value,
}
}
function parseLabels(raw: string): Record<string, string> {
const labels: Record<string, string> = {}
const pattern = /([^=,\s]+)="((?:\\.|[^"])*)"/g
for (const match of raw.matchAll(pattern)) {
const [, key, value] = match
if (!key) {
continue
}
labels[key] = unescapePrometheusLabel(value ?? '')
}
return labels
}
function unescapePrometheusLabel(value: string): string {
return value
.replace(/\\"/g, '"')
.replace(/\\n/g, '\n')
.replace(/\\\\/g, '\\')
}