import "server-only"; import { redis } from "@/lib/redis"; // === CrowdSec daily counters =============================================== // // Small Redis counters so ops can see whether the reputation pipeline is // actually doing anything: lookups executed, blocks created, signals pushed, // and push failures — all bucketed per UTC calendar day // (crowdsec:stat:{metric}:{YYYY-MM-DD}). Both the CTI client and the signal // pusher feed them; the admin panel renders the last N days. Cheap INCRs on // non-hot paths only, so they never tax the request path. export type CrowdsecStatMetric = | "lookups" | "blocks" | "reports" | "report_fail"; const STAT_PREFIX = "crowdsec:stat:"; const STAT_KEY_TTL_SECONDS = 16 * 24 * 3_600; function statDate(): string { return new Date().toISOString().slice(0, 10); } function statKey(metric: CrowdsecStatMetric, date: string): string { return `${STAT_PREFIX}${metric}:${date}`; } /** Count one occurrence of a pipeline event for today. Best effort. */ export async function bumpCrowdsecStat( metric: CrowdsecStatMetric, ): Promise { if (!redis) return; const key = statKey(metric, statDate()); try { await redis.incr(key); await redis.expire(key, STAT_KEY_TTL_SECONDS); } catch { // tracking is best-effort — a miss only loses a day's histogram } } export interface CrowdsecDailyStat { /** UTC calendar day (YYYY-MM-DD). */ date: string; lookups: number; blocks: number; reports: number; reportFailures: number; } /** * Read the per-day counters for the last `days` days (oldest first, ending * with today). Reads the four metric keys per day via one round-trip of * parallel GETs; never throws. */ export async function getCrowdsecStats( days = 14, ): Promise { const today = statDate(); const rows: CrowdsecDailyStat[] = []; if (!redis) { // No shared store — still return a blank timeline for the UI. for (let i = days - 1; i >= 0; i -= 1) { const date = new Date(Date.now() - i * 86_400_000) .toISOString() .slice(0, 10); rows.push({ date, lookups: 0, blocks: 0, reports: 0, reportFailures: 0 }); } return rows; } for (let i = days - 1; i >= 0; i -= 1) { const date = new Date(Date.now() - i * 86_400_000) .toISOString() .slice(0, 10); try { const [lookups, blocks, reports, reportFailures] = await Promise.all([ redis.get(statKey("lookups", date)), redis.get(statKey("blocks", date)), redis.get(statKey("reports", date)), redis.get(statKey("report_fail", date)), ]); const num = (raw: string | null): number => { const n = Number(raw ?? 0); return Number.isFinite(n) ? n : 0; }; rows.push({ date, lookups: num(lookups), blocks: num(blocks), reports: num(reports), reportFailures: num(reportFailures), }); } catch { rows.push({ date, lookups: 0, blocks: 0, reports: 0, reportFailures: 0 }); } } // Guard against an odd clock roll-back leaving a hole; keep chronological. if (rows.length && rows[rows.length - 1]?.date !== today) { rows.push({ date: today, lookups: 0, blocks: 0, reports: 0, reportFailures: 0, }); } return rows; }