import type { Redis } from 'ioredis'; import type { Client } from 'discord.js'; const KEY_COMMANDS_TOTAL = 'nexumi:metrics:commands_total'; const KEY_COMMANDS_ERRORS = 'nexumi:metrics:commands_errors'; const KEY_COMMAND_DURATION_SUM = 'nexumi:metrics:command_duration_ms_sum'; const KEY_COMMAND_DURATION_COUNT = 'nexumi:metrics:command_duration_ms_count'; const KEY_EVENTS_TOTAL = 'nexumi:metrics:events_total'; const shardKey = (id: number) => `nexumi:metrics:shard:${id}`; export async function recordCommandMetric( redis: Redis, opts: { ok: boolean; durationMs: number } ): Promise { const multi = redis.multi(); multi.incr(KEY_COMMANDS_TOTAL); if (!opts.ok) { multi.incr(KEY_COMMANDS_ERRORS); } multi.incrbyfloat(KEY_COMMAND_DURATION_SUM, opts.durationMs); multi.incr(KEY_COMMAND_DURATION_COUNT); await multi.exec(); } export async function recordEventMetric(redis: Redis): Promise { await redis.incr(KEY_EVENTS_TOTAL); } export async function publishShardMetrics(redis: Redis, client: Client): Promise { const ids = client.shard?.ids ?? [0]; const ping = client.ws.ping; const guilds = client.guilds.cache.size; const payload = JSON.stringify({ ping, guilds, updatedAt: Date.now() }); await Promise.all(ids.map((id) => redis.set(shardKey(id), payload, 'EX', 120))); } export type MetricsSnapshot = { up: 1; commandsTotal: number; commandsErrors: number; commandDurationSumMs: number; commandDurationCount: number; eventsTotal: number; shards: Array<{ id: number; ping: number; guilds: number; updatedAt: number }>; queueWaiting?: number; }; export async function collectMetricsSnapshot(redis: Redis): Promise { const [commandsTotal, commandsErrors, durationSum, durationCount, eventsTotal, shardKeys] = await Promise.all([ redis.get(KEY_COMMANDS_TOTAL), redis.get(KEY_COMMANDS_ERRORS), redis.get(KEY_COMMAND_DURATION_SUM), redis.get(KEY_COMMAND_DURATION_COUNT), redis.get(KEY_EVENTS_TOTAL), redis.keys('nexumi:metrics:shard:*') ]); const shards: MetricsSnapshot['shards'] = []; if (shardKeys.length > 0) { const values = await redis.mget(...shardKeys); for (let i = 0; i < shardKeys.length; i++) { const id = Number(shardKeys[i]!.replace('nexumi:metrics:shard:', '')); const raw = values[i]; if (!raw || Number.isNaN(id)) continue; try { const parsed = JSON.parse(raw) as { ping: number; guilds: number; updatedAt: number }; shards.push({ id, ping: parsed.ping, guilds: parsed.guilds, updatedAt: parsed.updatedAt }); } catch { // ignore malformed } } shards.sort((a, b) => a.id - b.id); } return { up: 1, commandsTotal: Number(commandsTotal ?? 0), commandsErrors: Number(commandsErrors ?? 0), commandDurationSumMs: Number(durationSum ?? 0), commandDurationCount: Number(durationCount ?? 0), eventsTotal: Number(eventsTotal ?? 0), shards }; } export function formatPrometheus(snapshot: MetricsSnapshot): string { const lines: string[] = [ '# HELP nexumi_up 1 if the metrics endpoint is healthy', '# TYPE nexumi_up gauge', `nexumi_up ${snapshot.up}`, '# HELP nexumi_commands_total Total slash commands handled', '# TYPE nexumi_commands_total counter', `nexumi_commands_total ${snapshot.commandsTotal}`, '# HELP nexumi_commands_errors_total Total slash command failures', '# TYPE nexumi_commands_errors_total counter', `nexumi_commands_errors_total ${snapshot.commandsErrors}`, '# HELP nexumi_command_duration_ms_sum Sum of command durations in milliseconds', '# TYPE nexumi_command_duration_ms_sum counter', `nexumi_command_duration_ms_sum ${snapshot.commandDurationSumMs}`, '# HELP nexumi_command_duration_ms_count Count of timed commands', '# TYPE nexumi_command_duration_ms_count counter', `nexumi_command_duration_ms_count ${snapshot.commandDurationCount}`, '# HELP nexumi_events_total Approximate event throughput counter', '# TYPE nexumi_events_total counter', `nexumi_events_total ${snapshot.eventsTotal}`, '# HELP nexumi_guilds Guild count per shard', '# TYPE nexumi_guilds gauge', '# HELP nexumi_shard_ping_ms Websocket heartbeat latency per shard', '# TYPE nexumi_shard_ping_ms gauge' ]; for (const shard of snapshot.shards) { lines.push(`nexumi_guilds{shard="${shard.id}"} ${shard.guilds}`); lines.push(`nexumi_shard_ping_ms{shard="${shard.id}"} ${shard.ping}`); } if (snapshot.queueWaiting !== undefined) { lines.push('# HELP nexumi_queue_waiting Approximate waiting jobs across known queues'); lines.push('# TYPE nexumi_queue_waiting gauge'); lines.push(`nexumi_queue_waiting ${snapshot.queueWaiting}`); } return `${lines.join('\n')}\n`; }