Enhance bot functionality with new features and improvements
- 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.
This commit is contained in:
129
apps/bot/src/metrics.ts
Normal file
129
apps/bot/src/metrics.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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`;
|
||||
}
|
||||
Reference in New Issue
Block a user