mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-16 22:38:39 +08:00
135 lines
3.5 KiB
TypeScript
135 lines
3.5 KiB
TypeScript
import { performance } from 'perf_hooks';
|
|
import Logger from '../loaders/logger';
|
|
|
|
interface Metric {
|
|
name: string;
|
|
value: number;
|
|
timestamp: number;
|
|
tags?: Record<string, string>;
|
|
}
|
|
|
|
export const MAX_METRIC_SAMPLES = 1000;
|
|
const METRIC_RETENTION_MS = 60 * 60 * 1000;
|
|
|
|
export class MetricsService {
|
|
private metrics: Array<Metric | undefined> = new Array(MAX_METRIC_SAMPLES);
|
|
private metricCount = 0;
|
|
private nextMetricIndex = 0;
|
|
private static instance: MetricsService;
|
|
|
|
private constructor() {
|
|
// 定期清理旧数据
|
|
const cleanupTimer = setInterval(() => this.removeExpiredMetrics(), 60000);
|
|
cleanupTimer.unref();
|
|
}
|
|
|
|
static getInstance(): MetricsService {
|
|
if (!MetricsService.instance) {
|
|
MetricsService.instance = new MetricsService();
|
|
}
|
|
return MetricsService.instance;
|
|
}
|
|
|
|
record(name: string, value: number, tags?: Record<string, string>) {
|
|
this.metrics[this.nextMetricIndex] = {
|
|
name,
|
|
value,
|
|
timestamp: Date.now(),
|
|
tags,
|
|
};
|
|
this.nextMetricIndex = (this.nextMetricIndex + 1) % MAX_METRIC_SAMPLES;
|
|
this.metricCount = Math.min(this.metricCount + 1, MAX_METRIC_SAMPLES);
|
|
}
|
|
|
|
measure(name: string, fn: () => void, tags?: Record<string, string>) {
|
|
const start = performance.now();
|
|
try {
|
|
fn();
|
|
} finally {
|
|
const duration = performance.now() - start;
|
|
this.record(name, duration, tags);
|
|
}
|
|
}
|
|
|
|
async measureAsync(
|
|
name: string,
|
|
fn: () => Promise<void>,
|
|
tags?: Record<string, string>,
|
|
) {
|
|
const start = performance.now();
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
const duration = performance.now() - start;
|
|
this.record(name, duration, tags);
|
|
}
|
|
}
|
|
|
|
getMetrics(name?: string, tags?: Record<string, string>) {
|
|
this.removeExpiredMetrics();
|
|
let filtered = this.getMetricSnapshot();
|
|
|
|
if (name) {
|
|
filtered = filtered.filter((m) => m.name === name);
|
|
}
|
|
|
|
if (tags) {
|
|
filtered = filtered.filter((m) => {
|
|
if (!m.tags) return false;
|
|
return Object.entries(tags).every(
|
|
([key, value]) => m.tags![key] === value,
|
|
);
|
|
});
|
|
}
|
|
|
|
const values = filtered.map((metric) => metric.value);
|
|
return {
|
|
count: filtered.length,
|
|
average: values.length
|
|
? values.reduce((acc, value) => acc + value, 0) / values.length
|
|
: 0,
|
|
min: values.length ? Math.min(...values) : 0,
|
|
max: values.length ? Math.max(...values) : 0,
|
|
metrics: filtered,
|
|
};
|
|
}
|
|
|
|
private getMetricSnapshot(): Metric[] {
|
|
if (this.metricCount < MAX_METRIC_SAMPLES) {
|
|
return this.metrics.slice(0, this.metricCount) as Metric[];
|
|
}
|
|
return [
|
|
...this.metrics.slice(this.nextMetricIndex),
|
|
...this.metrics.slice(0, this.nextMetricIndex),
|
|
] as Metric[];
|
|
}
|
|
|
|
private removeExpiredMetrics() {
|
|
const oldestTimestamp = Date.now() - METRIC_RETENTION_MS;
|
|
const retained = this.getMetricSnapshot().filter(
|
|
(metric) => metric.timestamp > oldestTimestamp,
|
|
);
|
|
if (retained.length === this.metricCount) return;
|
|
|
|
this.metrics = new Array(MAX_METRIC_SAMPLES);
|
|
this.metricCount = 0;
|
|
this.nextMetricIndex = 0;
|
|
for (const metric of retained) {
|
|
this.metrics[this.nextMetricIndex] = metric;
|
|
this.nextMetricIndex = (this.nextMetricIndex + 1) % MAX_METRIC_SAMPLES;
|
|
this.metricCount++;
|
|
}
|
|
}
|
|
|
|
report() {
|
|
const report = {
|
|
timestamp: Date.now(),
|
|
metrics: this.getMetrics(),
|
|
};
|
|
Logger.info('性能指标报告:', report);
|
|
return report;
|
|
}
|
|
}
|
|
|
|
export const metricsService = MetricsService.getInstance();
|