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:
parent
5a17d74680
commit
655dcb81e9
4 changed files with 473 additions and 124 deletions
46
services.conf.example
Normal file
46
services.conf.example
Normal 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
|
||||
249
src/config.zig
249
src/config.zig
|
|
@ -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
100
src/daemon.zig
Normal 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 {};
|
||||
}
|
||||
202
src/main.zig
202
src/main.zig
|
|
@ -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
|
||||
\\
|
||||
\\
|
||||
, .{});
|
||||
|
|
|
|||
Loading…
Reference in a new issue