gitdataai/admin/src/app/api/metrics/prometheus/route.ts
ZhenYi 27cd4ea83c
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
feat(admin/metrics): add Prometheus-compatible metrics endpoint and ops documentation
- Add /api/metrics/prometheus endpoint using prom-client (unauthenticated for scraping)
- Update middleware to allow unauthenticated access to prometheus endpoint
- Add /api/metrics permission routing (platform:read for GET)
- Install prom-client dependency
- Add metrics.md with Grafana dashboard JSON, Prometheus config, alerting rules
2026-04-26 14:49:25 +08:00

75 lines
2.1 KiB
TypeScript

import { NextResponse } from "next/server";
import { Registry, Gauge } from "prom-client";
import { query } from "@/lib/db";
export const runtime = "nodejs";
/**
* Prometheus-compatible metrics endpoint.
*
* Usage in prometheus.yml:
* - job_name: 'admin-metrics'
* scrape_interval: 60s
* static_configs:
* - targets: ['admin:3000']
* metrics_path: '/api/metrics/prometheus'
*/
export async function GET() {
const register = new Registry();
const gauge = new Gauge({
name: "platform_entity_count",
help: "Platform entity counts by time window",
labelNames: ["entity", "window"] as const,
registers: [register],
});
const entities = [
{ name: "users", table: '"user"' },
{ name: "workspaces", table: "workspace" },
{ name: "projects", table: "project" },
{ name: "repos", table: "repo" },
{ name: "rooms", table: "room" },
{ name: "skills", table: "project_skill" },
];
const results = await Promise.all(
entities.map(async ({ name, table }) => {
const res = await query<{
total: string;
new_27h: string;
new_7d: string;
new_30d: string;
}>(
`SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '27 hours') AS new_27h,
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '7 days') AS new_7d,
COUNT(*) FILTER (WHERE created_at >= NOW() - INTERVAL '30 days') AS new_30d
FROM ${table}`
);
const row = res.rows[0];
return {
name,
total: parseInt(row.total, 10),
new_27h: parseInt(row.new_27h, 10),
new_7d: parseInt(row.new_7d, 10),
new_30d: parseInt(row.new_30d, 10),
};
})
);
for (const r of results) {
gauge.set({ entity: r.name, window: "total" }, r.total);
gauge.set({ entity: r.name, window: "27h" }, r.new_27h);
gauge.set({ entity: r.name, window: "7d" }, r.new_7d);
gauge.set({ entity: r.name, window: "30d" }, r.new_30d);
}
const metrics = await register.metrics();
return new NextResponse(metrics, {
headers: { "Content-Type": register.contentType },
});
}