- Updated package dependencies to include `@sentry/node` for error tracking. - Refactored command routing to incorporate user and guild blacklisting checks, improving command execution safety. - Enhanced health server metrics to include queue waiting times, providing better insights into system performance. - Introduced confirmation handling for guild backup commands, streamlining user interactions. - Improved moderation commands with better error handling and case management, ensuring robust moderation capabilities. - Added new duration parsing functions to handle timeout durations effectively, enhancing moderation features. - Updated localization files to reflect new command structures and user guidance improvements.
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
await redis.incr(KEY_EVENTS_TOTAL);
|
|
}
|
|
|
|
export async function publishShardMetrics(redis: Redis, client: Client): Promise<void> {
|
|
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<MetricsSnapshot> {
|
|
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`;
|
|
}
|