diff --git a/service-monitor.log b/service-monitor.log new file mode 100644 index 0000000..04893fb --- /dev/null +++ b/service-monitor.log @@ -0,0 +1,7 @@ +[2025-12-07 20:00:34] +OK Forgejo (HTTP) (387ms) +OK Forgejo (SSH) (57ms) +OK Simifactu API (363ms) +OK Mundisofa (363ms) +OK Menzuri (363ms) + diff --git a/src/main.zig b/src/main.zig index a5c0a96..10c7b73 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,7 @@ //! zig build run - Verificar todos los servicios una vez //! 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 -- --help - Mostrar ayuda const std = @import("std"); @@ -15,12 +16,19 @@ const http = @import("http.zig"); const tcp = @import("tcp.zig"); const config = @import("config.zig"); +/// Nombre del archivo de log por defecto. +const DEFAULT_LOG_FILE = "service-monitor.log"; + /// Opciones de línea de comandos. const Options = struct { /// Modo watch: ejecutar continuamente. watch: bool = false, /// Intervalo entre checks en segundos (solo en modo watch). interval_seconds: u32 = 60, + /// Guardar log a archivo. + log_to_file: bool = false, + /// Ruta del archivo de log. + log_file: []const u8 = DEFAULT_LOG_FILE, /// Mostrar ayuda y salir. help: bool = false, }; @@ -44,13 +52,29 @@ pub fn main() !void { return; } + // Abrir archivo de log si está habilitado + var log_file: ?std.fs.File = null; + defer if (log_file) |f| f.close(); + + if (options.log_to_file) { + log_file = std.fs.cwd().createFile(options.log_file, .{ + .truncate = false, + }) catch |err| { + try stdout.print("Error abriendo archivo de log '{s}': {}\n", .{ options.log_file, err }); + std.process.exit(1); + }; + // Posicionar al final para append + log_file.?.seekFromEnd(0) catch {}; + try stdout.print("Log: {s}\n", .{options.log_file}); + } + 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); + const had_errors = try runChecks(allocator, stdout, log_file); _ = had_errors; // En modo watch no salimos por errores std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s); @@ -58,7 +82,7 @@ pub fn main() !void { } else { // Modo único try stdout.print("\n=== Service Monitor ===\n\n", .{}); - const had_errors = try runChecks(allocator, stdout); + const had_errors = try runChecks(allocator, stdout, log_file); if (had_errors) { std.process.exit(1); @@ -68,8 +92,9 @@ pub fn main() !void { /// Ejecuta verificación de todos los servicios. /// +/// Escribe resultados a stdout y opcionalmente a archivo de log. /// Retorna true si hubo algún error, false si todos OK. -fn runChecks(allocator: std.mem.Allocator, stdout: anytype) !bool { +fn runChecks(allocator: std.mem.Allocator, stdout: anytype, log_file: ?std.fs.File) !bool { const services = config.getServices(); var had_errors = false; @@ -77,12 +102,30 @@ fn runChecks(allocator: std.mem.Allocator, stdout: anytype) !bool { const timestamp = std.time.timestamp(); const epoch_seconds: u64 = @intCast(timestamp); const epoch = std.time.epoch.EpochSeconds{ .secs = epoch_seconds }; + const year_day = epoch.getEpochDay().calculateYearDay(); + const month_day = year_day.calculateMonthDay(); const day_seconds = epoch.getDaySeconds(); const hours = day_seconds.getHoursIntoDay(); const minutes = day_seconds.getMinutesIntoHour(); const seconds = day_seconds.getSecondsIntoMinute(); - try stdout.print("[{d:0>2}:{d:0>2}:{d:0>2}]\n", .{ hours, minutes, seconds }); + const timestamp_str = "[{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}]"; + const timestamp_args = .{ + year_day.year, + @intFromEnum(month_day.month), + month_day.day_index + 1, + hours, + minutes, + seconds, + }; + + try stdout.print(timestamp_str ++ "\n", timestamp_args); + + // Log writer opcional + const log_writer = if (log_file) |f| f.writer() else null; + if (log_writer) |lw| { + try lw.print(timestamp_str ++ "\n", timestamp_args); + } for (services) |service| { const result = switch (service.check_type) { @@ -92,13 +135,23 @@ fn runChecks(allocator: std.mem.Allocator, stdout: anytype) !bool { if (result) |time_ms| { try stdout.print("\x1b[32m✓\x1b[0m {s} - OK ({d}ms)\n", .{ service.name, time_ms }); + if (log_writer) |lw| { + try lw.print("OK {s} ({d}ms)\n", .{ service.name, time_ms }); + } } else |err| { had_errors = true; 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 }); + } } } try stdout.print("\n", .{}); + if (log_writer) |lw| { + try lw.print("\n", .{}); + } + return had_errors; } @@ -117,6 +170,14 @@ fn parseArgs() !Options { options.interval_seconds = std.fmt.parseInt(u32, interval_str, 10) catch { return error.InvalidIntervalValue; }; + } else if (std.mem.eql(u8, arg, "--log") or std.mem.eql(u8, arg, "-l")) { + options.log_to_file = true; + // Comprobar si el siguiente arg es una ruta (no empieza con -) + if (args.next()) |next_arg| { + if (next_arg.len > 0 and next_arg[0] != '-') { + options.log_file = next_arg; + } + } } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { options.help = true; } @@ -137,12 +198,15 @@ fn printUsage(stdout: anytype) !void { \\OPCIONES: \\ --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) \\ --help, -h Muestra esta ayuda \\ \\EJEMPLOS: - \\ service-monitor Verificar una vez - \\ service-monitor --watch Verificar cada 60 segundos - \\ service-monitor -w -i 30 Verificar cada 30 segundos + \\ service-monitor Verificar una vez + \\ 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 \\ \\ , .{});