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:
parent
dfcfd31ec3
commit
5a17d74680
2 changed files with 136 additions and 4 deletions
61
src/main.zig
61
src/main.zig
|
|
@ -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
79
src/notify.zig
Normal 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");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue