diff --git a/src/main.zig b/src/main.zig index 10c7b73..6c9d1c4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,12 +9,14 @@ //! zig build run -- --watch - Modo continuo (cada 60s por defecto) //! zig build run -- --watch -i 30 - Modo continuo cada 30 segundos //! zig build run -- --log - Guardar log a archivo +//! zig build run -- --notify - Notificaciones desktop en errores //! zig build run -- --help - Mostrar ayuda const std = @import("std"); const http = @import("http.zig"); const tcp = @import("tcp.zig"); const config = @import("config.zig"); +const notify = @import("notify.zig"); /// Nombre del archivo de log por defecto. const DEFAULT_LOG_FILE = "service-monitor.log"; @@ -29,6 +31,8 @@ const Options = struct { log_to_file: bool = false, /// Ruta del archivo de log. log_file: []const u8 = DEFAULT_LOG_FILE, + /// Activar notificaciones desktop (notify-send). + notify: bool = false, /// Mostrar ayuda y salir. help: bool = false, }; @@ -68,13 +72,17 @@ pub fn main() !void { try stdout.print("Log: {s}\n", .{options.log_file}); } + if (options.notify) { + try stdout.print("Notificaciones: activadas\n", .{}); + } + if (options.watch) { // Modo watch: loop infinito try stdout.print("\n=== Service Monitor (watch mode, interval: {d}s) ===\n", .{options.interval_seconds}); try stdout.print("Presiona Ctrl+C para salir\n\n", .{}); while (true) { - const had_errors = try runChecks(allocator, stdout, log_file); + const had_errors = try runChecks(allocator, stdout, log_file, options.notify); _ = had_errors; // En modo watch no salimos por errores std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s); @@ -82,7 +90,7 @@ pub fn main() !void { } else { // Modo único try stdout.print("\n=== Service Monitor ===\n\n", .{}); - const had_errors = try runChecks(allocator, stdout, log_file); + const had_errors = try runChecks(allocator, stdout, log_file, options.notify); if (had_errors) { std.process.exit(1); @@ -93,10 +101,18 @@ pub fn main() !void { /// Ejecuta verificación de todos los servicios. /// /// Escribe resultados a stdout y opcionalmente a archivo de log. +/// Envía notificaciones desktop si está habilitado y hay errores. /// Retorna true si hubo algún error, false si todos OK. -fn runChecks(allocator: std.mem.Allocator, stdout: anytype, log_file: ?std.fs.File) !bool { +fn runChecks( + allocator: std.mem.Allocator, + stdout: anytype, + log_file: ?std.fs.File, + notify_enabled: bool, +) !bool { const services = config.getServices(); var had_errors = false; + var error_count: u32 = 0; + var error_services: [16][]const u8 = undefined; // Timestamp const timestamp = std.time.timestamp(); @@ -140,6 +156,10 @@ fn runChecks(allocator: std.mem.Allocator, stdout: anytype, log_file: ?std.fs.Fi } } else |err| { had_errors = true; + if (error_count < error_services.len) { + error_services[error_count] = service.name; + error_count += 1; + } try stdout.print("\x1b[31m✗\x1b[0m {s} - ERROR: {}\n", .{ service.name, err }); if (log_writer) |lw| { try lw.print("ERROR {s} - {}\n", .{ service.name, err }); @@ -152,6 +172,35 @@ fn runChecks(allocator: std.mem.Allocator, stdout: anytype, log_file: ?std.fs.Fi try lw.print("\n", .{}); } + // Enviar notificación si hay errores + if (notify_enabled and had_errors) { + // Construir mensaje con servicios fallidos + var body_buf: [512]u8 = undefined; + var body_len: usize = 0; + + for (error_services[0..error_count]) |svc_name| { + if (body_len > 0) { + if (body_len + 2 < body_buf.len) { + body_buf[body_len] = '\n'; + body_len += 1; + } + } + const remaining = body_buf.len - body_len; + const to_copy = @min(svc_name.len, remaining); + @memcpy(body_buf[body_len..][0..to_copy], svc_name[0..to_copy]); + body_len += to_copy; + } + + notify.send( + allocator, + "⚠️ Servicios caídos", + body_buf[0..body_len], + "critical", + ) catch { + // Ignorar errores de notificación, no son críticos + }; + } + return had_errors; } @@ -178,6 +227,8 @@ fn parseArgs() !Options { options.log_file = next_arg; } } + } else if (std.mem.eql(u8, arg, "--notify") or std.mem.eql(u8, arg, "-n")) { + options.notify = true; } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { options.help = true; } @@ -199,6 +250,7 @@ fn printUsage(stdout: anytype) !void { \\ --watch, -w Modo continuo (ejecuta checks en loop) \\ --interval, -i Intervalo en segundos entre checks (default: 60) \\ --log, -l [archivo] Guardar log a archivo (default: service-monitor.log) + \\ --notify, -n Notificaciones desktop cuando hay errores (Linux) \\ --help, -h Muestra esta ayuda \\ \\EJEMPLOS: @@ -206,7 +258,8 @@ fn printUsage(stdout: anytype) !void { \\ service-monitor --watch Verificar cada 60 segundos \\ service-monitor -w -i 30 Verificar cada 30 segundos \\ service-monitor --log Guardar a service-monitor.log - \\ service-monitor -w -l monitor.log Watch + log a archivo custom + \\ service-monitor -w -n Watch + notificaciones en errores + \\ service-monitor -w -l -n Watch + log + notificaciones \\ \\ , .{}); diff --git a/src/notify.zig b/src/notify.zig new file mode 100644 index 0000000..65c9090 --- /dev/null +++ b/src/notify.zig @@ -0,0 +1,79 @@ +//! Módulo de notificaciones desktop +//! +//! Envía notificaciones usando notify-send (libnotify) en Linux. +//! Las notificaciones solo se envían cuando hay errores para evitar spam. + +const std = @import("std"); + +/// Errores posibles durante el envío de notificaciones. +pub const NotifyError = error{ + /// No se pudo ejecutar notify-send. + CommandFailed, + /// notify-send no está instalado. + NotifyNotAvailable, +}; + +/// Envía una notificación desktop usando notify-send. +/// +/// Parámetros: +/// - allocator: Allocator para el proceso hijo. +/// - title: Título de la notificación. +/// - body: Cuerpo del mensaje. +/// - urgency: Nivel de urgencia ("low", "normal", "critical"). +/// +/// Ejemplo: +/// ```zig +/// try notify.send(allocator, "Service Monitor", "Forgejo está caído!", "critical"); +/// ``` +pub fn send( + allocator: std.mem.Allocator, + title: []const u8, + body: []const u8, + urgency: []const u8, +) NotifyError!void { + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "notify-send", + "--urgency", + urgency, + "--app-name", + "Service Monitor", + title, + body, + }, + }) catch { + return NotifyError.CommandFailed; + }; + + allocator.free(result.stdout); + allocator.free(result.stderr); + + if (result.term.Exited != 0) { + return NotifyError.CommandFailed; + } +} + +/// Envía una notificación de error crítico. +/// +/// Wrapper conveniente para notificaciones de servicios caídos. +pub fn sendError(allocator: std.mem.Allocator, service_name: []const u8) NotifyError!void { + var body_buf: [256]u8 = undefined; + const body = std.fmt.bufPrint(&body_buf, "❌ {s} no responde", .{service_name}) catch { + return NotifyError.CommandFailed; + }; + + return send(allocator, "⚠️ Servicio caído", body, "critical"); +} + +/// Envía una notificación de recuperación. +/// +/// Para notificar cuando un servicio que estaba caído vuelve a funcionar. +pub fn sendRecovery(allocator: std.mem.Allocator, service_name: []const u8) NotifyError!void { + var body_buf: [256]u8 = undefined; + const body = std.fmt.bufPrint(&body_buf, "✅ {s} está funcionando de nuevo", .{service_name}) catch { + return NotifyError.CommandFailed; + }; + + return send(allocator, "Servicio recuperado", body, "normal"); +}