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
|
//! Configuración de servicios a monitorear
|
||||||
//!
|
//!
|
||||||
//! Define la lista de servicios del servidor Simba (Hetzner) que deben
|
//! Soporta configuración desde archivo externo o valores por defecto.
|
||||||
//! ser verificados periódicamente.
|
//! 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");
|
const std = @import("std");
|
||||||
|
|
||||||
|
|
@ -20,60 +34,199 @@ pub const Service = struct {
|
||||||
/// Tipo de verificación a realizar.
|
/// Tipo de verificación a realizar.
|
||||||
check_type: CheckType,
|
check_type: CheckType,
|
||||||
/// URL completa para verificaciones HTTP (ej: "https://example.com").
|
/// URL completa para verificaciones HTTP (ej: "https://example.com").
|
||||||
/// Solo usado cuando check_type == .http
|
|
||||||
url: []const u8 = "",
|
url: []const u8 = "",
|
||||||
/// Hostname para verificaciones TCP.
|
/// Hostname para verificaciones TCP.
|
||||||
/// Solo usado cuando check_type == .tcp
|
|
||||||
host: []const u8 = "",
|
host: []const u8 = "",
|
||||||
/// Puerto para verificaciones TCP.
|
/// Puerto para verificaciones TCP.
|
||||||
/// Solo usado cuando check_type == .tcp
|
|
||||||
port: u16 = 0,
|
port: u16 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Lista de servicios del servidor Simba (188.245.244.244) a monitorear.
|
/// Configuración SMTP para envío de emails.
|
||||||
///
|
pub const SmtpConfig = struct {
|
||||||
/// Servicios actuales:
|
host: []const u8 = "",
|
||||||
/// - Forgejo (Git): HTTP en git.reugenio.com + SSH en puerto 2222
|
port: u16 = 587,
|
||||||
/// - Simifactu API: HTTPS en simifactu.com
|
username: []const u8 = "",
|
||||||
/// - Mundisofa: HTTPS en mundisofa.com
|
password: []const u8 = "",
|
||||||
/// - Menzuri: HTTPS en menzuri.com
|
from: []const u8 = "",
|
||||||
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",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 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
|
/// Si el archivo no existe, retorna la configuración por defecto.
|
||||||
/// para permitir configuración dinámica sin recompilar.
|
pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Config {
|
||||||
pub fn getServices() []const Service {
|
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
||||||
return &services;
|
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
|
//! Service Monitor - Monitor de servicios HTTP y TCP
|
||||||
//!
|
//!
|
||||||
//! Herramienta para verificar que los servicios en el servidor Hetzner (Simba)
|
//! Herramienta para verificar que los servicios estén funcionando correctamente.
|
||||||
//! estén funcionando correctamente. Diseñado para ser simple, ligero y sin
|
//! Diseñado para ser simple, ligero y sin dependencias externas.
|
||||||
//! dependencias externas.
|
|
||||||
//!
|
//!
|
||||||
//! Uso:
|
//! Uso:
|
||||||
//! zig build run - Verificar todos los servicios una vez
|
//! service-monitor - Verificar una vez
|
||||||
//! zig build run -- --watch - Modo continuo (cada 60s por defecto)
|
//! service-monitor --watch - Modo continuo (cada 60s)
|
||||||
//! zig build run -- --watch -i 30 - Modo continuo cada 30 segundos
|
//! service-monitor --daemon - Ejecutar como daemon en background
|
||||||
//! zig build run -- --log - Guardar log a archivo
|
//! service-monitor --config FILE - Usar archivo de configuración
|
||||||
//! zig build run -- --notify - Notificaciones desktop en errores
|
//! service-monitor --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");
|
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";
|
const DEFAULT_LOG_FILE = "service-monitor.log";
|
||||||
|
|
||||||
|
/// Archivo PID para daemon.
|
||||||
|
const DEFAULT_PID_FILE = "service-monitor.pid";
|
||||||
|
|
||||||
/// Opciones de línea de comandos.
|
/// Opciones de línea de comandos.
|
||||||
const Options = struct {
|
const Options = struct {
|
||||||
/// Modo watch: ejecutar continuamente.
|
/// Modo watch: ejecutar continuamente.
|
||||||
watch: bool = false,
|
watch: bool = false,
|
||||||
/// Intervalo entre checks en segundos (solo en modo watch).
|
/// Intervalo entre checks en segundos.
|
||||||
interval_seconds: u32 = 60,
|
interval_seconds: u32 = 60,
|
||||||
/// Guardar log a archivo.
|
/// Guardar log a archivo.
|
||||||
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).
|
/// Activar notificaciones desktop.
|
||||||
notify: bool = false,
|
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,
|
help: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,41 +62,78 @@ pub fn main() !void {
|
||||||
return;
|
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;
|
var log_file: ?std.fs.File = null;
|
||||||
defer if (log_file) |f| f.close();
|
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, .{
|
log_file = std.fs.cwd().createFile(options.log_file, .{
|
||||||
.truncate = false,
|
.truncate = false,
|
||||||
}) catch |err| {
|
}) 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);
|
std.process.exit(1);
|
||||||
};
|
};
|
||||||
// Posicionar al final para append
|
|
||||||
log_file.?.seekFromEnd(0) catch {};
|
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", .{});
|
try stdout.print("Notificaciones: activadas\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.watch) {
|
// Obtener writer de salida (null si daemon)
|
||||||
// Modo watch: loop infinito
|
const output_writer = if (options.daemonize) null else stdout;
|
||||||
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", .{});
|
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) {
|
while (true) {
|
||||||
const had_errors = try runChecks(allocator, stdout, log_file, options.notify);
|
_ = try runChecks(allocator, output_writer, log_file, options.notify, cfg.services);
|
||||||
_ = 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);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 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, options.notify);
|
const had_errors = try runChecks(allocator, stdout, log_file, options.notify, cfg.services);
|
||||||
|
|
||||||
if (had_errors) {
|
if (had_errors) {
|
||||||
std.process.exit(1);
|
std.process.exit(1);
|
||||||
|
|
@ -99,17 +142,13 @@ 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.
|
|
||||||
/// Envía notificaciones desktop si está habilitado y hay errores.
|
|
||||||
/// Retorna true si hubo algún error, false si todos OK.
|
|
||||||
fn runChecks(
|
fn runChecks(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
stdout: anytype,
|
stdout: ?std.fs.File.Writer,
|
||||||
log_file: ?std.fs.File,
|
log_file: ?std.fs.File,
|
||||||
notify_enabled: bool,
|
notify_enabled: bool,
|
||||||
|
services: []const config.Service,
|
||||||
) !bool {
|
) !bool {
|
||||||
const services = config.getServices();
|
|
||||||
var had_errors = false;
|
var had_errors = false;
|
||||||
var error_count: u32 = 0;
|
var error_count: u32 = 0;
|
||||||
var error_services: [16][]const u8 = undefined;
|
var error_services: [16][]const u8 = undefined;
|
||||||
|
|
@ -135,9 +174,10 @@ fn runChecks(
|
||||||
seconds,
|
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;
|
const log_writer = if (log_file) |f| f.writer() else null;
|
||||||
if (log_writer) |lw| {
|
if (log_writer) |lw| {
|
||||||
try lw.print(timestamp_str ++ "\n", timestamp_args);
|
try lw.print(timestamp_str ++ "\n", timestamp_args);
|
||||||
|
|
@ -150,7 +190,9 @@ fn runChecks(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result) |time_ms| {
|
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| {
|
if (log_writer) |lw| {
|
||||||
try lw.print("OK {s} ({d}ms)\n", .{ service.name, time_ms });
|
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_services[error_count] = service.name;
|
||||||
error_count += 1;
|
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| {
|
if (log_writer) |lw| {
|
||||||
try lw.print("ERROR {s} - {}\n", .{ service.name, err });
|
try lw.print("ERROR {s} - {}\n", .{ service.name, err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try stdout.print("\n", .{});
|
if (stdout) |out| {
|
||||||
|
try out.print("\n", .{});
|
||||||
|
}
|
||||||
if (log_writer) |lw| {
|
if (log_writer) |lw| {
|
||||||
try lw.print("\n", .{});
|
try lw.print("\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enviar notificación si hay errores
|
// Notificación desktop si hay errores
|
||||||
if (notify_enabled and had_errors) {
|
if (notify_enabled and had_errors) {
|
||||||
// Construir mensaje con servicios fallidos
|
|
||||||
var body_buf: [512]u8 = undefined;
|
var body_buf: [512]u8 = undefined;
|
||||||
var body_len: usize = 0;
|
var body_len: usize = 0;
|
||||||
|
|
||||||
for (error_services[0..error_count]) |svc_name| {
|
for (error_services[0..error_count]) |svc_name| {
|
||||||
if (body_len > 0) {
|
if (body_len > 0 and body_len + 1 < body_buf.len) {
|
||||||
if (body_len + 2 < body_buf.len) {
|
body_buf[body_len] = '\n';
|
||||||
body_buf[body_len] = '\n';
|
body_len += 1;
|
||||||
body_len += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const remaining = body_buf.len - body_len;
|
const remaining = body_buf.len - body_len;
|
||||||
const to_copy = @min(svc_name.len, remaining);
|
const to_copy = @min(svc_name.len, remaining);
|
||||||
|
|
@ -191,14 +234,7 @@ fn runChecks(
|
||||||
body_len += to_copy;
|
body_len += to_copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
notify.send(
|
notify.send(allocator, "⚠️ Servicios caídos", body_buf[0..body_len], "critical") catch {};
|
||||||
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;
|
||||||
|
|
@ -207,28 +243,28 @@ fn runChecks(
|
||||||
/// Parsea los argumentos de línea de comandos.
|
/// Parsea los argumentos de línea de comandos.
|
||||||
fn parseArgs() !Options {
|
fn parseArgs() !Options {
|
||||||
var options = Options{};
|
var options = Options{};
|
||||||
|
|
||||||
var args = std.process.args();
|
var args = std.process.args();
|
||||||
_ = args.skip(); // Saltar nombre del programa
|
_ = args.skip();
|
||||||
|
|
||||||
while (args.next()) |arg| {
|
while (args.next()) |arg| {
|
||||||
if (std.mem.eql(u8, arg, "--watch") or std.mem.eql(u8, arg, "-w")) {
|
if (std.mem.eql(u8, arg, "--watch") or std.mem.eql(u8, arg, "-w")) {
|
||||||
options.watch = true;
|
options.watch = true;
|
||||||
} else if (std.mem.eql(u8, arg, "--interval") or std.mem.eql(u8, arg, "-i")) {
|
} else if (std.mem.eql(u8, arg, "--interval") or std.mem.eql(u8, arg, "-i")) {
|
||||||
const interval_str = args.next() orelse return error.MissingIntervalValue;
|
const val = args.next() orelse return error.MissingIntervalValue;
|
||||||
options.interval_seconds = std.fmt.parseInt(u32, interval_str, 10) catch {
|
options.interval_seconds = std.fmt.parseInt(u32, val, 10) catch return error.InvalidIntervalValue;
|
||||||
return error.InvalidIntervalValue;
|
|
||||||
};
|
|
||||||
} else if (std.mem.eql(u8, arg, "--log") or std.mem.eql(u8, arg, "-l")) {
|
} else if (std.mem.eql(u8, arg, "--log") or std.mem.eql(u8, arg, "-l")) {
|
||||||
options.log_to_file = true;
|
options.log_to_file = true;
|
||||||
// Comprobar si el siguiente arg es una ruta (no empieza con -)
|
if (args.next()) |next| {
|
||||||
if (args.next()) |next_arg| {
|
if (next.len > 0 and next[0] != '-') {
|
||||||
if (next_arg.len > 0 and next_arg[0] != '-') {
|
options.log_file = next;
|
||||||
options.log_file = next_arg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (std.mem.eql(u8, arg, "--notify") or std.mem.eql(u8, arg, "-n")) {
|
} else if (std.mem.eql(u8, arg, "--notify") or std.mem.eql(u8, arg, "-n")) {
|
||||||
options.notify = true;
|
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")) {
|
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
options.help = true;
|
options.help = true;
|
||||||
}
|
}
|
||||||
|
|
@ -241,25 +277,39 @@ fn parseArgs() !Options {
|
||||||
fn printUsage(stdout: anytype) !void {
|
fn printUsage(stdout: anytype) !void {
|
||||||
try stdout.print(
|
try stdout.print(
|
||||||
\\
|
\\
|
||||||
\\Service Monitor - Verifica servicios HTTP y TCP
|
\\Service Monitor - Verifica disponibilidad de servicios HTTP y TCP
|
||||||
\\
|
\\
|
||||||
\\USO:
|
\\USO:
|
||||||
\\ service-monitor [opciones]
|
\\ service-monitor [opciones]
|
||||||
\\
|
\\
|
||||||
\\OPCIONES:
|
\\OPCIONES:
|
||||||
\\ --watch, -w Modo continuo (ejecuta checks en loop)
|
\\ -w, --watch Modo continuo (ejecuta checks en loop)
|
||||||
\\ --interval, -i <N> Intervalo en segundos entre checks (default: 60)
|
\\ -i, --interval <N> Intervalo en segundos entre checks (default: 60)
|
||||||
\\ --log, -l [archivo] Guardar log a archivo (default: service-monitor.log)
|
\\ -l, --log [archivo] Guardar log a archivo (default: service-monitor.log)
|
||||||
\\ --notify, -n Notificaciones desktop cuando hay errores (Linux)
|
\\ -n, --notify Notificaciones desktop cuando hay errores
|
||||||
\\ --help, -h Muestra esta ayuda
|
\\ -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:
|
\\EJEMPLOS:
|
||||||
\\ service-monitor Verificar una vez
|
\\ service-monitor Verificar una vez
|
||||||
\\ service-monitor --watch Verificar cada 60 segundos
|
\\ service-monitor -w 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 -w -d Daemon en background
|
||||||
\\ service-monitor -w -n Watch + notificaciones en errores
|
\\ service-monitor -w -d -l -n Daemon + log + notificaciones
|
||||||
\\ service-monitor -w -l -n Watch + log + notificaciones
|
\\ service-monitor -c mi-config.conf Usar config personalizado
|
||||||
\\
|
\\
|
||||||
\\
|
\\
|
||||||
, .{});
|
, .{});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue