Daemon mode + configuración externa

- Nuevo módulo daemon.zig: fork() + setsid() + /dev/null
- Opción --daemon/-d para ejecutar en background
- Archivo PID en service-monitor.pid
- Config externo desde archivo (--config/-c)
- Formato CSV simple: http,nombre,url / tcp,nombre,host,puerto
- Soporte para email y telegram en config (preparado)
- services.conf.example con documentación
- Ayuda actualizada con todas las opciones

🤖 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 23:45:07 +01:00
parent 5a17d74680
commit 655dcb81e9
4 changed files with 473 additions and 124 deletions

46
services.conf.example Normal file
View file

@ -0,0 +1,46 @@
# Service Monitor - Archivo de configuración
# ============================================
#
# Formato: tipo,parámetros separados por comas
# Las líneas que empiezan con # son comentarios
#
# SERVICIOS
# ---------
# http,Nombre,URL
# tcp,Nombre,host,puerto
# Servicios HTTP/HTTPS
http,Forgejo (HTTP),https://git.reugenio.com
http,Simifactu API,https://simifactu.com
http,Mundisofa,https://mundisofa.com
http,Menzuri,https://menzuri.com
# Servicios TCP
tcp,Forgejo (SSH),git.reugenio.com,2222
# NOTIFICACIONES EMAIL
# --------------------
# email,destinatario@ejemplo.com
# email_smtp,servidor,puerto,usuario,password,remitente
# Destinatarios (uno por línea)
#email,admin@ejemplo.com
#email,alertas@ejemplo.com
# Servidor SMTP
#email_smtp,smtp.gmail.com,587,usuario@gmail.com,app_password,alertas@ejemplo.com
# NOTIFICACIONES TELEGRAM
# -----------------------
# telegram,bot_token,chat_id
#
# Para obtener el bot_token:
# 1. Habla con @BotFather en Telegram
# 2. Usa /newbot y sigue las instrucciones
# 3. Copia el token que te da
#
# Para obtener tu chat_id:
# 1. Habla con @userinfobot en Telegram
# 2. Te dirá tu chat_id
#telegram,123456789:ABCdefGHIjklMNOpqrsTUVwxyz,987654321

View file

@ -1,7 +1,21 @@
//! Configuración de servicios a monitorear
//!
//! Define la lista de servicios del servidor Simba (Hetzner) que deben
//! ser verificados periódicamente.
//! Soporta configuración desde archivo externo o valores por defecto.
//! Formato del archivo de configuración:
//!
//! ```
//! # Comentarios empiezan con #
//! # Servicios HTTP/HTTPS
//! http,Nombre Servicio,https://ejemplo.com
//!
//! # Servicios TCP
//! tcp,Nombre Servicio,host.ejemplo.com,puerto
//!
//! # Configuración de notificaciones
//! email,destinatario@ejemplo.com
//! email_smtp,smtp.ejemplo.com,587,usuario,password
//! telegram,bot_token,chat_id
//! ```
const std = @import("std");
@ -20,60 +34,199 @@ pub const Service = struct {
/// Tipo de verificación a realizar.
check_type: CheckType,
/// URL completa para verificaciones HTTP (ej: "https://example.com").
/// Solo usado cuando check_type == .http
url: []const u8 = "",
/// Hostname para verificaciones TCP.
/// Solo usado cuando check_type == .tcp
host: []const u8 = "",
/// Puerto para verificaciones TCP.
/// Solo usado cuando check_type == .tcp
port: u16 = 0,
};
/// Lista de servicios del servidor Simba (188.245.244.244) a monitorear.
///
/// Servicios actuales:
/// - Forgejo (Git): HTTP en git.reugenio.com + SSH en puerto 2222
/// - Simifactu API: HTTPS en simifactu.com
/// - Mundisofa: HTTPS en mundisofa.com
/// - Menzuri: HTTPS en menzuri.com
const services = [_]Service{
// Forgejo - Servidor Git
.{
.name = "Forgejo (HTTP)",
.check_type = .http,
.url = "https://git.reugenio.com",
},
.{
.name = "Forgejo (SSH)",
.check_type = .tcp,
.host = "git.reugenio.com",
.port = 2222,
},
// Simifactu - API facturación
.{
.name = "Simifactu API",
.check_type = .http,
.url = "https://simifactu.com",
},
// Mundisofa - Tienda online
.{
.name = "Mundisofa",
.check_type = .http,
.url = "https://mundisofa.com",
},
// Menzuri - Web
.{
.name = "Menzuri",
.check_type = .http,
.url = "https://menzuri.com",
},
/// Configuración SMTP para envío de emails.
pub const SmtpConfig = struct {
host: []const u8 = "",
port: u16 = 587,
username: []const u8 = "",
password: []const u8 = "",
from: []const u8 = "",
};
/// Retorna la lista de servicios a monitorear.
/// Configuración de Telegram.
pub const TelegramConfig = struct {
bot_token: []const u8 = "",
chat_id: []const u8 = "",
};
/// Configuración completa cargada desde archivo.
pub const Config = struct {
/// Lista de servicios a monitorear.
services: []Service,
/// Emails destinatarios para alertas.
email_recipients: [][]const u8,
/// Configuración SMTP.
smtp: SmtpConfig,
/// Configuración Telegram.
telegram: TelegramConfig,
/// Allocator usado (para liberar memoria).
allocator: std.mem.Allocator,
/// Libera toda la memoria de la configuración.
pub fn deinit(self: *Config) void {
for (self.services) |service| {
self.allocator.free(service.name);
if (service.url.len > 0) self.allocator.free(service.url);
if (service.host.len > 0) self.allocator.free(service.host);
}
self.allocator.free(self.services);
for (self.email_recipients) |email| {
self.allocator.free(email);
}
self.allocator.free(self.email_recipients);
if (self.smtp.host.len > 0) self.allocator.free(self.smtp.host);
if (self.smtp.username.len > 0) self.allocator.free(self.smtp.username);
if (self.smtp.password.len > 0) self.allocator.free(self.smtp.password);
if (self.smtp.from.len > 0) self.allocator.free(self.smtp.from);
if (self.telegram.bot_token.len > 0) self.allocator.free(self.telegram.bot_token);
if (self.telegram.chat_id.len > 0) self.allocator.free(self.telegram.chat_id);
}
};
/// Nombre del archivo de configuración por defecto.
pub const DEFAULT_CONFIG_FILE = "services.conf";
/// Carga la configuración desde un archivo.
///
/// En el futuro, esta función podría leer desde un archivo JSON
/// para permitir configuración dinámica sin recompilar.
pub fn getServices() []const Service {
return &services;
/// Si el archivo no existe, retorna la configuración por defecto.
pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Config {
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
if (err == error.FileNotFound) {
return getDefaultConfig(allocator);
}
return err;
};
defer file.close();
return parseConfigFile(allocator, file);
}
/// Parsea el contenido de un archivo de configuración.
fn parseConfigFile(allocator: std.mem.Allocator, file: std.fs.File) !Config {
var services = std.ArrayList(Service).init(allocator);
var emails = std.ArrayList([]const u8).init(allocator);
var smtp = SmtpConfig{};
var telegram = TelegramConfig{};
const reader = file.reader();
var buf: [1024]u8 = undefined;
while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| {
if (maybe_line) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
// Ignorar líneas vacías y comentarios
if (trimmed.len == 0 or trimmed[0] == '#') continue;
// Parsear línea
var parts = std.mem.splitScalar(u8, trimmed, ',');
const tipo = parts.next() orelse continue;
if (std.mem.eql(u8, tipo, "http")) {
const name = parts.next() orelse continue;
const url = parts.next() orelse continue;
try services.append(.{
.name = try allocator.dupe(u8, name),
.check_type = .http,
.url = try allocator.dupe(u8, url),
});
} else if (std.mem.eql(u8, tipo, "tcp")) {
const name = parts.next() orelse continue;
const host = parts.next() orelse continue;
const port_str = parts.next() orelse continue;
const port = std.fmt.parseInt(u16, port_str, 10) catch continue;
try services.append(.{
.name = try allocator.dupe(u8, name),
.check_type = .tcp,
.host = try allocator.dupe(u8, host),
.port = port,
});
} else if (std.mem.eql(u8, tipo, "email")) {
const email = parts.next() orelse continue;
try emails.append(try allocator.dupe(u8, email));
} else if (std.mem.eql(u8, tipo, "email_smtp")) {
smtp.host = try allocator.dupe(u8, parts.next() orelse continue);
const port_str = parts.next() orelse "587";
smtp.port = std.fmt.parseInt(u16, port_str, 10) catch 587;
if (parts.next()) |user| smtp.username = try allocator.dupe(u8, user);
if (parts.next()) |pass| smtp.password = try allocator.dupe(u8, pass);
if (parts.next()) |from| smtp.from = try allocator.dupe(u8, from);
} else if (std.mem.eql(u8, tipo, "telegram")) {
telegram.bot_token = try allocator.dupe(u8, parts.next() orelse continue);
telegram.chat_id = try allocator.dupe(u8, parts.next() orelse continue);
}
} else {
break;
}
} else |_| {}
return Config{
.services = try services.toOwnedSlice(),
.email_recipients = try emails.toOwnedSlice(),
.smtp = smtp,
.telegram = telegram,
.allocator = allocator,
};
}
/// Retorna la configuración por defecto (hardcoded).
pub fn getDefaultConfig(allocator: std.mem.Allocator) !Config {
const builtin_services = [_]Service{
.{ .name = "Forgejo (HTTP)", .check_type = .http, .url = "https://git.reugenio.com" },
.{ .name = "Forgejo (SSH)", .check_type = .tcp, .host = "git.reugenio.com", .port = 2222 },
.{ .name = "Simifactu API", .check_type = .http, .url = "https://simifactu.com" },
.{ .name = "Mundisofa", .check_type = .http, .url = "https://mundisofa.com" },
.{ .name = "Menzuri", .check_type = .http, .url = "https://menzuri.com" },
};
var services = try allocator.alloc(Service, builtin_services.len);
for (builtin_services, 0..) |svc, i| {
services[i] = .{
.name = try allocator.dupe(u8, svc.name),
.check_type = svc.check_type,
.url = if (svc.url.len > 0) try allocator.dupe(u8, svc.url) else "",
.host = if (svc.host.len > 0) try allocator.dupe(u8, svc.host) else "",
.port = svc.port,
};
}
const empty_emails = try allocator.alloc([]const u8, 0);
return Config{
.services = services,
.email_recipients = empty_emails,
.smtp = SmtpConfig{},
.telegram = TelegramConfig{},
.allocator = allocator,
};
}
// --- Compatibilidad con código existente ---
/// Lista de servicios por defecto (hardcoded).
const default_services = [_]Service{
.{ .name = "Forgejo (HTTP)", .check_type = .http, .url = "https://git.reugenio.com" },
.{ .name = "Forgejo (SSH)", .check_type = .tcp, .host = "git.reugenio.com", .port = 2222 },
.{ .name = "Simifactu API", .check_type = .http, .url = "https://simifactu.com" },
.{ .name = "Mundisofa", .check_type = .http, .url = "https://mundisofa.com" },
.{ .name = "Menzuri", .check_type = .http, .url = "https://menzuri.com" },
};
/// Retorna la lista de servicios por defecto (para compatibilidad).
pub fn getServices() []const Service {
return &default_services;
}

100
src/daemon.zig Normal file
View file

@ -0,0 +1,100 @@
//! Módulo para daemonización del proceso
//!
//! Permite ejecutar el monitor como un daemon en background,
//! desvinculándolo de la terminal.
const std = @import("std");
const linux = std.os.linux;
/// Errores posibles durante la daemonización.
pub const DaemonError = error{
/// Error al hacer fork del proceso.
ForkFailed,
/// Error al crear nueva sesión.
SetsidFailed,
/// Error al cambiar directorio.
ChdirFailed,
};
/// Convierte el proceso actual en un daemon.
///
/// El proceso se desvincula de la terminal, crea una nueva sesión,
/// y redirige stdin/stdout/stderr a /dev/null.
///
/// Retorna:
/// - true si somos el proceso hijo (daemon) y debemos continuar
/// - false si somos el proceso padre y debemos salir
/// - error si algo falla
pub fn daemonize() DaemonError!bool {
// Primer fork: separarse del proceso padre
const pid1 = std.posix.fork() catch return DaemonError.ForkFailed;
if (pid1 != 0) {
// Proceso padre: salir
return false;
}
// Proceso hijo: crear nueva sesión usando syscall directo
const ret = linux.syscall0(.setsid);
if (@as(isize, @bitCast(ret)) < 0) {
return DaemonError.SetsidFailed;
}
// Segundo fork: evitar reconectar a terminal
const pid2 = std.posix.fork() catch return DaemonError.ForkFailed;
if (pid2 != 0) {
// Primer hijo: salir
std.posix.exit(0);
}
// Daemon real: cambiar a directorio raíz para no bloquear unmounts
std.posix.chdir("/") catch return DaemonError.ChdirFailed;
// Cerrar descriptores estándar y redirigir a /dev/null
const dev_null = std.fs.openFileAbsolute("/dev/null", .{ .mode = .read_write }) catch {
return true;
};
defer dev_null.close();
const null_fd = dev_null.handle;
// Redirigir stdin, stdout, stderr a /dev/null
std.posix.dup2(null_fd, 0) catch {};
std.posix.dup2(null_fd, 1) catch {};
std.posix.dup2(null_fd, 2) catch {};
return true;
}
/// Escribe el PID del daemon a un archivo.
pub fn writePidFile(path: []const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
const pid = linux.getpid();
var buf: [16]u8 = undefined;
const pid_str = std.fmt.bufPrint(&buf, "{d}\n", .{pid}) catch return;
_ = try file.write(pid_str);
}
/// Lee el PID de un archivo pidfile.
pub fn readPidFile(path: []const u8) !i32 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var buf: [16]u8 = undefined;
const bytes_read = try file.read(&buf);
var end: usize = bytes_read;
while (end > 0 and (buf[end - 1] == '\n' or buf[end - 1] == '\r')) {
end -= 1;
}
return std.fmt.parseInt(i32, buf[0..end], 10) catch error.InvalidPidFile;
}
/// Elimina el archivo pidfile.
pub fn removePidFile(path: []const u8) void {
std.fs.cwd().deleteFile(path) catch {};
}

View file

@ -1,39 +1,45 @@
//! Service Monitor - Monitor de servicios HTTP y TCP
//!
//! Herramienta para verificar que los servicios en el servidor Hetzner (Simba)
//! estén funcionando correctamente. Diseñado para ser simple, ligero y sin
//! dependencias externas.
//! Herramienta para verificar que los servicios estén funcionando correctamente.
//! Diseñado para ser simple, ligero y sin dependencias externas.
//!
//! Uso:
//! 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 -- --notify - Notificaciones desktop en errores
//! zig build run -- --help - Mostrar ayuda
//! service-monitor - Verificar una vez
//! service-monitor --watch - Modo continuo (cada 60s)
//! service-monitor --daemon - Ejecutar como daemon en background
//! service-monitor --config FILE - Usar archivo de configuración
//! service-monitor --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");
const daemon = @import("daemon.zig");
/// Nombre del archivo de log por defecto.
/// Archivo de log por defecto.
const DEFAULT_LOG_FILE = "service-monitor.log";
/// Archivo PID para daemon.
const DEFAULT_PID_FILE = "service-monitor.pid";
/// 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).
/// Intervalo entre checks en segundos.
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,
/// Activar notificaciones desktop (notify-send).
/// Activar notificaciones desktop.
notify: bool = false,
/// Mostrar ayuda y salir.
/// Ejecutar como daemon.
daemonize: bool = false,
/// Archivo de configuración.
config_file: []const u8 = config.DEFAULT_CONFIG_FILE,
/// Mostrar ayuda.
help: bool = false,
};
@ -56,41 +62,78 @@ pub fn main() !void {
return;
}
// Abrir archivo de log si está habilitado
// Cargar configuración
var cfg = config.loadFromFile(allocator, options.config_file) catch |err| {
try stdout.print("Error cargando configuración '{s}': {}\n", .{ options.config_file, err });
std.process.exit(1);
};
defer cfg.deinit();
// Daemonizar si se solicita
if (options.daemonize) {
if (!options.watch) {
try stdout.print("Error: --daemon requiere --watch\n", .{});
std.process.exit(1);
}
try stdout.print("Iniciando daemon (PID file: {s})...\n", .{DEFAULT_PID_FILE});
const is_daemon = daemon.daemonize() catch |err| {
try stdout.print("Error al daemonizar: {}\n", .{err});
std.process.exit(1);
};
if (!is_daemon) {
// Proceso padre: salir
try stdout.print("Daemon iniciado.\n", .{});
return;
}
// Proceso daemon: escribir PID
daemon.writePidFile(DEFAULT_PID_FILE) catch {};
}
// Abrir archivo de log
var log_file: ?std.fs.File = null;
defer if (log_file) |f| f.close();
if (options.log_to_file) {
// En modo daemon, siempre loguear a archivo
if (options.log_to_file or options.daemonize) {
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 });
if (!options.daemonize) {
try stdout.print("Error abriendo 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.daemonize) {
try stdout.print("Log: {s}\n", .{options.log_file});
}
}
if (options.notify) {
if (options.notify and !options.daemonize) {
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", .{});
// Obtener writer de salida (null si daemon)
const output_writer = if (options.daemonize) null else stdout;
if (options.watch or options.daemonize) {
if (!options.daemonize) {
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, options.notify);
_ = had_errors; // En modo watch no salimos por errores
_ = try runChecks(allocator, output_writer, log_file, options.notify, cfg.services);
std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s);
}
} else {
// Modo único
try stdout.print("\n=== Service Monitor ===\n\n", .{});
const had_errors = try runChecks(allocator, stdout, log_file, options.notify);
const had_errors = try runChecks(allocator, stdout, log_file, options.notify, cfg.services);
if (had_errors) {
std.process.exit(1);
@ -99,17 +142,13 @@ 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,
stdout: ?std.fs.File.Writer,
log_file: ?std.fs.File,
notify_enabled: bool,
services: []const config.Service,
) !bool {
const services = config.getServices();
var had_errors = false;
var error_count: u32 = 0;
var error_services: [16][]const u8 = undefined;
@ -135,9 +174,10 @@ fn runChecks(
seconds,
};
try stdout.print(timestamp_str ++ "\n", timestamp_args);
if (stdout) |out| {
try out.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);
@ -150,7 +190,9 @@ fn runChecks(
};
if (result) |time_ms| {
try stdout.print("\x1b[32m✓\x1b[0m {s} - OK ({d}ms)\n", .{ service.name, time_ms });
if (stdout) |out| {
try out.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 });
}
@ -160,30 +202,31 @@ fn runChecks(
error_services[error_count] = service.name;
error_count += 1;
}
try stdout.print("\x1b[31m✗\x1b[0m {s} - ERROR: {}\n", .{ service.name, err });
if (stdout) |out| {
try out.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 (stdout) |out| {
try out.print("\n", .{});
}
if (log_writer) |lw| {
try lw.print("\n", .{});
}
// Enviar notificación si hay errores
// Notificación desktop 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;
}
if (body_len > 0 and body_len + 1 < 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);
@ -191,14 +234,7 @@ fn runChecks(
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
};
notify.send(allocator, "⚠️ Servicios caídos", body_buf[0..body_len], "critical") catch {};
}
return had_errors;
@ -207,28 +243,28 @@ fn runChecks(
/// Parsea los argumentos de línea de comandos.
fn parseArgs() !Options {
var options = Options{};
var args = std.process.args();
_ = args.skip(); // Saltar nombre del programa
_ = args.skip();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--watch") or std.mem.eql(u8, arg, "-w")) {
options.watch = true;
} else if (std.mem.eql(u8, arg, "--interval") or std.mem.eql(u8, arg, "-i")) {
const interval_str = args.next() orelse return error.MissingIntervalValue;
options.interval_seconds = std.fmt.parseInt(u32, interval_str, 10) catch {
return error.InvalidIntervalValue;
};
const val = args.next() orelse return error.MissingIntervalValue;
options.interval_seconds = std.fmt.parseInt(u32, val, 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;
if (args.next()) |next| {
if (next.len > 0 and next[0] != '-') {
options.log_file = next;
}
}
} 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, "--daemon") or std.mem.eql(u8, arg, "-d")) {
options.daemonize = true;
} else if (std.mem.eql(u8, arg, "--config") or std.mem.eql(u8, arg, "-c")) {
options.config_file = args.next() orelse return error.MissingConfigFile;
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
options.help = true;
}
@ -241,25 +277,39 @@ fn parseArgs() !Options {
fn printUsage(stdout: anytype) !void {
try stdout.print(
\\
\\Service Monitor - Verifica servicios HTTP y TCP
\\Service Monitor - Verifica disponibilidad de servicios HTTP y TCP
\\
\\USO:
\\ service-monitor [opciones]
\\
\\OPCIONES:
\\ --watch, -w Modo continuo (ejecuta checks en loop)
\\ --interval, -i <N> 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
\\ -w, --watch Modo continuo (ejecuta checks en loop)
\\ -i, --interval <N> Intervalo en segundos entre checks (default: 60)
\\ -l, --log [archivo] Guardar log a archivo (default: service-monitor.log)
\\ -n, --notify Notificaciones desktop cuando hay errores
\\ -d, --daemon Ejecutar como daemon en background (requiere -w)
\\ -c, --config <archivo> Archivo de configuración (default: services.conf)
\\ -h, --help Muestra esta ayuda
\\
\\ARCHIVO DE CONFIGURACIÓN:
\\ Formato CSV simple. Ver services.conf.example para ejemplos.
\\
\\ # Servicios
\\ http,Nombre,https://url.com
\\ tcp,Nombre,host.com,puerto
\\
\\ # Notificaciones
\\ email,destinatario@email.com
\\ email_smtp,smtp.host.com,587,usuario,password,remitente
\\ telegram,bot_token,chat_id
\\
\\EJEMPLOS:
\\ 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 -n Watch + notificaciones en errores
\\ service-monitor -w -l -n Watch + log + notificaciones
\\ service-monitor Verificar una vez
\\ service-monitor -w Verificar cada 60 segundos
\\ service-monitor -w -i 30 Verificar cada 30 segundos
\\ service-monitor -w -d Daemon en background
\\ service-monitor -w -d -l -n Daemon + log + notificaciones
\\ service-monitor -c mi-config.conf Usar config personalizado
\\
\\
, .{});