- 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
75 lines
2.1 KiB
TypeScript
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 },
|
|
});
|
|
}
|