Skip to content

Commit 09ce6a5

Browse files
authored
Merge pull request #7 from CapituloJaverianoACM/develop
[Feat]: implement centralized logging system with error metrics and a…
2 parents 4b4ef93 + 4832fab commit 09ce6a5

File tree

11 files changed

+1601
-81
lines changed

11 files changed

+1601
-81
lines changed

DOCUMENTATION.md

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,148 @@ Este documento resume la documentación agregada a todo el proyecto del Bot de D
199199

200200
---
201201

202+
## 🔍 Monitoreo y Logging
203+
204+
### 📄 `src/utils/logger.ts`
205+
**Sistema centralizado de logging**
206+
- ✅ Descripción completa del sistema de logging profesional
207+
- ✅ JSDoc para enum `LogLevel` (DEBUG/INFO/WARN/ERROR/CRITICAL)
208+
- ✅ JSDoc para interface `LogContext` (requestId, userId, guildId, commandName, duration)
209+
- ✅ JSDoc para interface `LogEntry` (timestamp, level, message, context)
210+
- ✅ JSDoc para interface `ErrorWindow` (total, errors)
211+
- ✅ JSDoc para interface `ErrorMetrics` (errorRate, totalRequests, totalErrors, topErrorCommands, topErrorTypes)
212+
- ✅ JSDoc para función `getErrorRate()` - Calcula porcentaje de errores en ventana de 5 minutos
213+
- ✅ JSDoc para función `getRequestCount()` - Obtiene total de requests en ventana actual
214+
- ✅ JSDoc para función `getErrorCount()` - Obtiene total de errores en ventana actual
215+
- ✅ JSDoc para función `checkErrorThreshold(threshold)` - Verifica si supera threshold (requiere min 10 requests)
216+
- ✅ JSDoc para función `getErrorsByCommand()` - Map de comando -> cantidad de errores
217+
- ✅ JSDoc para función `getErrorsByType()` - Map de tipo de error -> cantidad
218+
- ✅ JSDoc para función `getErrorMetrics()` - Métricas completas agregadas
219+
- ✅ JSDoc para función `resetMetrics()` - Limpia todas las métricas
220+
- ✅ JSDoc para función `maskSensitive(text)` - Enmascara emails y tokens automáticamente
221+
- ✅ JSDoc para función `generateRequestId()` - Genera UUID v4 único
222+
- ✅ JSDoc para clase `Logger` con métodos debug/info/warn/error/critical
223+
- ✅ Documentación del sistema de ventanas deslizantes (5 min)
224+
- ✅ Documentación de limpieza automática (>15 min)
225+
- ✅ Ejemplo: `logger.info('User verified', {requestId, userId, email: maskEmail(email)})`
226+
227+
### 📄 `src/utils/rateLimit.ts`
228+
**Sistema de rate limiting**
229+
- ✅ Descripción del sistema en memoria
230+
- ✅ JSDoc para interface `RateLimitResult` (allowed, remainingMs)
231+
- ✅ JSDoc para interface `RateLimitInfo` (lastUsed, isActive)
232+
- ✅ JSDoc para función `checkRateLimit(key, ttlMs)` - Valida cooldowns y retorna estado
233+
- ✅ JSDoc para función `getRateLimitInfo(key)` - Obtiene info de rate limit
234+
- ✅ JSDoc para función `clearRateLimit(key)` - Limpia rate limit específico (testing/admin)
235+
- ✅ JSDoc para función `getRateLimitSize()` - Número de entradas activas
236+
- ✅ Documentación de limpieza automática cada 5 minutos
237+
- ✅ Documentación de formato de keys: `action:guildId:userId`
238+
- ✅ Ejemplo: `checkRateLimit("verify:123456789:987654321", 30000)`
239+
240+
### 📄 `src/utils/retry.ts`
241+
**Sistema de retry inteligente**
242+
- ✅ Descripción del sistema con exponential backoff
243+
- ✅ JSDoc para interface `RetryOptions` (maxAttempts, delays, shouldRetry, onRetry)
244+
- ✅ JSDoc para función `retryWithBackoff(fn, options)` - Ejecuta con reintentos y backoff
245+
- ✅ JSDoc para función `isTemporaryError(error)` - Detecta errores retriables (network, timeout, 5xx)
246+
- ✅ JSDoc para función `isPermanentError(error)` - Detecta errores permanentes (auth, 4xx)
247+
- ✅ Documentación de errores temporales: AbortError, NetworkError, TimeoutError, ECONNRESET, ETIMEDOUT, códigos HTTP 408/429/500-504
248+
- ✅ Documentación de errores permanentes: EAUTH, códigos HTTP 400/401/403/404/422, validación de email
249+
- ✅ Documentación de propagación de error original con contexto enriquecido
250+
- ✅ Ejemplo: `retryWithBackoff(() => fetchData(), {maxAttempts: 3, delays: [2000, 4000, 8000], shouldRetry: isTemporaryError})`
251+
252+
### 📄 `src/utils/alerts.ts`
253+
**Sistema de alertas a Discord**
254+
- ✅ Descripción del sistema de notificaciones a admins
255+
- ✅ JSDoc para type `AlertSeverity` (info/warning/critical)
256+
- ✅ JSDoc para interface `AlertOptions` (title, description, severity, fields, timestamp)
257+
- ✅ JSDoc para función `sendAdminAlert(client, guildId, options)` - Envía alerta al canal configurado
258+
- ✅ Documentación de colores por severidad: azul (#5865F2) info, naranja (#F59E0B) warning, rojo (#EF4444) critical
259+
- ✅ Documentación de emojis por severidad: ℹ️ info, ⚠️ warning, 🚨 critical
260+
- ✅ Documentación de rate limiting interno (1 alerta del mismo tipo cada 10 minutos)
261+
- ✅ Documentación de fallback: canal alerts > announcements
262+
- ✅ Documentación de validación de permisos (ViewChannel, SendMessages, EmbedLinks)
263+
- ✅ JSDoc para función `hashAlertKey(title)` - Hash MD5 para rate limiting
264+
- ✅ JSDoc para función `clearAlertHistory()` - Limpia historial (testing)
265+
- ✅ Ejemplo: `sendAdminAlert(client, "123", {title: "High Error Rate", description: "25% errors", severity: "warning", fields: [...]})`
266+
267+
---
268+
269+
## 📊 Comandos de Administración
270+
271+
### 📄 `src/commands/metrics.ts`
272+
**Visualización de métricas en tiempo real**
273+
- ✅ Descripción del comando de estadísticas
274+
- ✅ JSDoc para función `formatDuration(ms)` - Convierte ms a formato legible (días/horas/minutos)
275+
- ✅ JSDoc para función `execute(interaction)` - Muestra métricas del bot
276+
- ✅ Documentación de permisos requeridos: Administrator o rol admin/junta
277+
- ✅ Documentación de métricas mostradas:
278+
- Error rate con emoji según severidad (🔴 >20%, 🟡 >10%, 🟢 <10%)
279+
- Total requests/errors en ventana de 5 minutos
280+
- Uptime del bot formateado
281+
- Rate limits activos
282+
- Ping del websocket
283+
- Top comandos con errores
284+
- Top tipos de errores
285+
- ✅ Documentación de respuesta efímera (solo visible para quien ejecuta)
286+
- ✅ Ejemplo de uso: `/metrics` muestra embed con todas las estadísticas
287+
288+
---
289+
202290
## ⚙️ Configuración
203291

292+
### 📄 `src/commands/setup.ts`
293+
**Configuración del servidor**
294+
- ✅ Documentación ampliada con nuevas opciones
295+
- ✅ Nueva opción `channel_alerts` (opcional) - Canal para alertas del sistema
296+
- ✅ Nueva opción `alert_threshold` (opcional, 10-50, default: 20) - % de errores para alertar
297+
- ✅ Documentación de validación de threshold (mínimo 10%, máximo 50%)
298+
- ✅ Actualización de embed de confirmación mostrando canal de alertas y threshold
299+
- ✅ Ejemplo: `/setup ... channel_alerts:#alertas alert_threshold:25`
300+
301+
### 📄 `src/commands/verify.ts`
302+
**Sistema de verificación por email**
303+
- ✅ Documentación actualizada con mejoras de producción
304+
- ✅ Rate limiting: 30s cooldown entre intentos de `/verify start`
305+
- ✅ Retry logic del mailer: 3 intentos con delays de 2s, 4s, 8s
306+
- ✅ Request IDs únicos para rastrear flujo completo de verificación
307+
- ✅ Logging estructurado en cada paso (start, email send, code verify, role assign)
308+
- ✅ OTP persistente en caso de error de email (permite reintentos)
309+
- ✅ Defer automático vía handler global (elimina race conditions)
310+
- ✅ Validación de estado de interacción antes de cada respuesta
311+
- ✅ Propagación de requestId a mailer y store para logging correlacionado
312+
- ✅ Ejemplo de flujo: User → /verify start → Rate limit check → Generate OTP → Send email (retry 3x) → Log success
313+
204314
### 📄 `src/config/store.ts`
205315
**Sistema de almacenamiento**
206316
- ✅ Descripción completa del sistema
207317
- ✅ JSDoc para interface `GuildConfig`
318+
- ✅ Nuevo campo `channels.alerts` - ID del canal de alertas administrativas
319+
- ✅ Nuevo campo `alertThreshold` - Porcentaje de error rate (0-100) para alertas automáticas
208320
- ✅ JSDoc para interface `ConfigFile`
209321
- ✅ Documentación de variables de AWS S3
210322
- ✅ JSDoc para función `streamToString()`
211-
- ✅ JSDoc para función `loadFromBucket()`
212-
- ✅ JSDoc para función `saveToBucket()`
323+
- ✅ JSDoc actualizado para función `loadFromBucket()` - Incluye logging con métricas de latencia
324+
- ✅ JSDoc actualizado para función `saveToBucket()` - Incluye logging con métricas de latencia
213325
- ✅ JSDoc para función `getGuildConfig()`
214-
- ✅ JSDoc para función `upsertGuildConfig()`
326+
- ✅ JSDoc actualizado para función `upsertGuildConfig(config, requestId?)` - Acepta requestId opcional
215327
- ✅ Explicación del sistema de caché
328+
- ✅ Logging de operaciones S3 con códigos HTTP y duración
329+
330+
---
331+
332+
## 🎯 Eventos
333+
334+
### 📄 `src/events/interactionCreate.ts`
335+
**Handler principal de interacciones**
336+
- ✅ Documentación actualizada con sistema de monitoreo
337+
- ✅ Generación de requestId único para cada interacción
338+
- ✅ Inyección de requestId en objeto interaction
339+
- ✅ Logging estructurado con contexto completo (type, commandName, userId, guildId, requestId)
340+
- ✅ Contador de interacciones para verificar error threshold cada 10 interacciones
341+
- ✅ Sistema de alertas automáticas cuando error rate supera threshold configurado
342+
- ✅ Envío de alerta con métricas detalladas: error rate, requests, errors, top commands, window
343+
- ✅ Defer automático para comandos con flag `defer: true`
344+
- ✅ Logging de éxito/error de ejecución de comandos
345+
- ✅ Manejo robusto de errores con logging estructurado
346+
- ✅ Nivel CRITICAL para errores en el handler principal

src/commands/metrics.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @file metrics.ts
3+
* @description Comando para visualizar métricas y estadísticas del bot en tiempo real.
4+
* Muestra error rate, requests totales, comandos más usados, uptime, y más.
5+
* Solo accesible para administradores.
6+
*/
7+
8+
import { SlashCommandBuilder, PermissionsBitField } from 'discord.js';
9+
import { buildEmbed } from '../utils/embed';
10+
import { getGuildConfig } from '../config/store';
11+
import { getErrorMetrics } from '../utils/logger';
12+
import { getRateLimitSize } from '../utils/rateLimit';
13+
14+
const data = new SlashCommandBuilder()
15+
.setName('metrics')
16+
.setDescription('Muestra métricas y estadísticas del bot');
17+
18+
/**
19+
* Formatea duración en ms a string legible
20+
* @param {number} ms - Milisegundos
21+
* @returns {string} Duración formateada
22+
*/
23+
function formatDuration(ms: number): string {
24+
const seconds = Math.floor(ms / 1000);
25+
const minutes = Math.floor(seconds / 60);
26+
const hours = Math.floor(minutes / 60);
27+
const days = Math.floor(hours / 24);
28+
29+
if (days > 0) {
30+
return `${days}d ${hours % 24}h ${minutes % 60}m`;
31+
} else if (hours > 0) {
32+
return `${hours}h ${minutes % 60}m`;
33+
} else if (minutes > 0) {
34+
return `${minutes}m ${seconds % 60}s`;
35+
} else {
36+
return `${seconds}s`;
37+
}
38+
}
39+
40+
/**
41+
* Ejecuta el comando metrics
42+
* @param {any} interaction - La interacción de Discord
43+
* @returns {Promise<void>}
44+
*/
45+
async function execute(interaction: any) {
46+
const config = getGuildConfig(interaction.guildId);
47+
const adminRoleId = config?.roles.admin;
48+
const juntaRoleId = config?.roles.junta;
49+
const member = interaction.member;
50+
51+
// Verificar permisos
52+
const isAdmin =
53+
member.permissions.has(PermissionsBitField.Flags.Administrator) ||
54+
(adminRoleId && member.roles.cache.has(adminRoleId)) ||
55+
(juntaRoleId && member.roles.cache.has(juntaRoleId));
56+
57+
if (!isAdmin) {
58+
return interaction.reply({
59+
content: 'Solo administradores pueden ver las métricas del bot.',
60+
flags: 1 << 6,
61+
});
62+
}
63+
64+
// Obtener métricas
65+
const metrics = getErrorMetrics();
66+
const uptime = interaction.client.readyTimestamp
67+
? Date.now() - interaction.client.readyTimestamp
68+
: 0;
69+
const rateLimitSize = getRateLimitSize();
70+
71+
// Formatear error rate con emoji
72+
const errorRateText = `${metrics.errorRate.toFixed(1)}%`;
73+
const errorRateEmoji = metrics.errorRate > 20 ? '🔴' : metrics.errorRate > 10 ? '🟡' : '🟢';
74+
75+
// Top comandos más usados (basado en errores, en producción sería bueno trackear todos los usos)
76+
const topCommandsText =
77+
metrics.topErrorCommands.length > 0
78+
? metrics.topErrorCommands.map((c, i) => `${i + 1}. **${c.command}**: ${c.count}`).join('\n')
79+
: 'No hay datos disponibles';
80+
81+
// Top tipos de errores
82+
const topErrorTypesText =
83+
metrics.topErrorTypes.length > 0
84+
? metrics.topErrorTypes.map((t, i) => `${i + 1}. **${t.type}**: ${t.count}`).join('\n')
85+
: 'No hay errores registrados';
86+
87+
// Construir embed
88+
const embed = buildEmbed({
89+
title: '📊 Métricas del Bot',
90+
description: `Estadísticas de los últimos ${metrics.windowMinutes} minutos`,
91+
color: '#5865F2',
92+
fields: [
93+
{
94+
name: `${errorRateEmoji} Error Rate`,
95+
value: errorRateText,
96+
inline: true,
97+
},
98+
{
99+
name: '📈 Total Requests',
100+
value: metrics.totalRequests.toString(),
101+
inline: true,
102+
},
103+
{
104+
name: '❌ Total Errors',
105+
value: metrics.totalErrors.toString(),
106+
inline: true,
107+
},
108+
{
109+
name: '⏰ Uptime',
110+
value: formatDuration(uptime),
111+
inline: true,
112+
},
113+
{
114+
name: '🚦 Rate Limits Active',
115+
value: rateLimitSize.toString(),
116+
inline: true,
117+
},
118+
{
119+
name: '📍 Ping',
120+
value: `${interaction.client.ws.ping}ms`,
121+
inline: true,
122+
},
123+
{
124+
name: '🔝 Comandos con Errores',
125+
value: topCommandsText,
126+
inline: false,
127+
},
128+
{
129+
name: '⚠️ Tipos de Errores',
130+
value: topErrorTypesText,
131+
inline: false,
132+
},
133+
],
134+
footer: `Generado el ${new Date().toLocaleString()}`,
135+
});
136+
137+
return interaction.reply({ embeds: [embed], flags: 1 << 6 });
138+
}
139+
140+
export default { data, execute, defer: true };

src/commands/setup.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ const data = new SlashCommandBuilder()
4646
.setName('role_event_ping')
4747
.setDescription('Rol para avisos de eventos/anuncios')
4848
.setRequired(false),
49+
)
50+
.addStringOption((opt) =>
51+
opt
52+
.setName('channel_alerts')
53+
.setDescription('Canal para alertas del sistema (opcional)')
54+
.setRequired(false),
55+
)
56+
.addIntegerOption((opt) =>
57+
opt
58+
.setName('alert_threshold')
59+
.setDescription('% de errores para alertar (10-50, default: 20)')
60+
.setRequired(false)
61+
.setMinValue(10)
62+
.setMaxValue(50),
4963
);
5064

5165
/**
@@ -76,7 +90,9 @@ async function execute(interaction: any) {
7690
interaction.options.getString('channel_vc2', true),
7791
],
7892
voiceCategory: interaction.options.getString('category_voice', true),
93+
alerts: interaction.options.getString('channel_alerts') ?? undefined,
7994
},
95+
alertThreshold: interaction.options.getInteger('alert_threshold') ?? 20,
8096
};
8197
upsertGuildConfig(config);
8298
const embed = buildEmbed({
@@ -101,6 +117,16 @@ async function execute(interaction: any) {
101117
inline: false,
102118
},
103119
{ name: 'Categoría Voz', value: `<#${config.channels.voiceCategory}>`, inline: true },
120+
{
121+
name: 'Canal Alertas',
122+
value: config.channels.alerts ? `<#${config.channels.alerts}>` : 'No configurado',
123+
inline: true,
124+
},
125+
{
126+
name: 'Threshold Alertas',
127+
value: `${config.alertThreshold}%`,
128+
inline: true,
129+
},
104130
],
105131
});
106132
await interaction.reply({ embeds: [embed] });

0 commit comments

Comments
 (0)