Fase 3: Notificaciones desktop (notify-send)

- Nuevo módulo notify.zig con funciones send(), sendError(), sendRecovery()
- Opción --notify/-n para activar notificaciones
- Solo notifica cuando hay errores (evita spam)
- Notificación crítica con lista de servicios caídos
- Usa notify-send (libnotify) disponible en Linux

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-07 21:15:17 +01:00
parent dfcfd31ec3
commit 5a17d74680
2 changed files with 136 additions and 4 deletions

View file

@ -9,12 +9,14 @@
//! zig build run -- --watch - Modo continuo (cada 60s por defecto) //! zig build run -- --watch - Modo continuo (cada 60s por defecto)
//! zig build run -- --watch -i 30 - Modo continuo cada 30 segundos //! zig build run -- --watch -i 30 - Modo continuo cada 30 segundos
//! zig build run -- --log - Guardar log a archivo //! zig build run -- --log - Guardar log a archivo
//! zig build run -- --notify - Notificaciones desktop en errores
//! zig build run -- --help - Mostrar ayuda //! zig build run -- --help - Mostrar ayuda
const std = @import("std"); const std = @import("std");
const http = @import("http.zig"); const http = @import("http.zig");
const tcp = @import("tcp.zig"); const tcp = @import("tcp.zig");
const config = @import("config.zig"); const config = @import("config.zig");
const notify = @import("notify.zig");
/// Nombre del archivo de log por defecto. /// Nombre del archivo de log por defecto.
const DEFAULT_LOG_FILE = "service-monitor.log"; const DEFAULT_LOG_FILE = "service-monitor.log";
@ -29,6 +31,8 @@ const Options = struct {
log_to_file: bool = false, log_to_file: bool = false,
/// Ruta del archivo de log. /// Ruta del archivo de log.
log_file: []const u8 = DEFAULT_LOG_FILE, log_file: []const u8 = DEFAULT_LOG_FILE,
/// Activar notificaciones desktop (notify-send).
notify: bool = false,
/// Mostrar ayuda y salir. /// Mostrar ayuda y salir.
help: bool = false, help: bool = false,
}; };
@ -68,13 +72,17 @@ pub fn main() !void {
try stdout.print("Log: {s}\n", .{options.log_file}); try stdout.print("Log: {s}\n", .{options.log_file});
} }
if (options.notify) {
try stdout.print("Notificaciones: activadas\n", .{});
}
if (options.watch) { if (options.watch) {
// Modo watch: loop infinito // Modo watch: loop infinito
try stdout.print("\n=== Service Monitor (watch mode, interval: {d}s) ===\n", .{options.interval_seconds}); 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", .{}); try stdout.print("Presiona Ctrl+C para salir\n\n", .{});
while (true) { 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 _ = had_errors; // En modo watch no salimos por errores
std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s); std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s);
@ -82,7 +90,7 @@ pub fn main() !void {
} else { } else {
// Modo único // Modo único
try stdout.print("\n=== Service Monitor ===\n\n", .{}); 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) { if (had_errors) {
std.process.exit(1); std.process.exit(1);
@ -93,10 +101,18 @@ pub fn main() !void {
/// Ejecuta verificación de todos los servicios. /// Ejecuta verificación de todos los servicios.
/// ///
/// Escribe resultados a stdout y opcionalmente a archivo de log. /// 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. /// 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(); const services = config.getServices();
var had_errors = false; var had_errors = false;
var error_count: u32 = 0;
var error_services: [16][]const u8 = undefined;
// Timestamp // Timestamp
const timestamp = std.time.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| { } else |err| {
had_errors = true; 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 }); try stdout.print("\x1b[31m✗\x1b[0m {s} - ERROR: {}\n", .{ service.name, err });
if (log_writer) |lw| { if (log_writer) |lw| {
try lw.print("ERROR {s} - {}\n", .{ service.name, err }); 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", .{}); 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; return had_errors;
} }
@ -178,6 +227,8 @@ fn parseArgs() !Options {
options.log_file = next_arg; 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")) { } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
options.help = true; options.help = true;
} }
@ -199,6 +250,7 @@ fn printUsage(stdout: anytype) !void {
\\ --watch, -w Modo continuo (ejecuta checks en loop) \\ --watch, -w Modo continuo (ejecuta checks en loop)
\\ --interval, -i <N> Intervalo en segundos entre checks (default: 60) \\ --interval, -i <N> Intervalo en segundos entre checks (default: 60)
\\ --log, -l [archivo] Guardar log a archivo (default: service-monitor.log) \\ --log, -l [archivo] Guardar log a archivo (default: service-monitor.log)
\\ --notify, -n Notificaciones desktop cuando hay errores (Linux)
\\ --help, -h Muestra esta ayuda \\ --help, -h Muestra esta ayuda
\\ \\
\\EJEMPLOS: \\EJEMPLOS:
@ -206,7 +258,8 @@ fn printUsage(stdout: anytype) !void {
\\ service-monitor --watch Verificar cada 60 segundos \\ service-monitor --watch Verificar cada 60 segundos
\\ service-monitor -w -i 30 Verificar cada 30 segundos \\ service-monitor -w -i 30 Verificar cada 30 segundos
\\ service-monitor --log Guardar a service-monitor.log \\ 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
\\ \\
\\ \\
, .{}); , .{});

79
src/notify.zig Normal file
View file

@ -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");
}