commit e2e19da32fc05a5229586f02c577afab2859e9b0 Author: reugenio Date: Sun Dec 7 20:43:34 2025 +0100 Fase 1: Monitor básico HTTP/TCP funcionando - Verificación HTTP/HTTPS con std.http.Client - Verificación TCP con resolución DNS (tcpConnectToHost) - 5 servicios configurados: Forgejo (HTTP+SSH), Simifactu, Mundisofa, Menzuri - Output terminal con colores y tiempos de respuesta - Doc comments en todas las funciones públicas (estándar open source) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cbf470 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Zig build artifacts +zig-out/ +zig-cache/ +.zig-cache/ + +# Binary +service-monitor + +# OS +.DS_Store +Thumbs.db diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f25d283 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,165 @@ +# service-monitor - Monitor de Servicios en Zig + +> **Última actualización**: 2025-12-07 + +## Descripción del Proyecto + +Monitor que verifica periódicamente que los servicios en nuestro servidor Hetzner (Simba) estén funcionando correctamente, con notificaciones si algo falla. + +**Lenguaje**: Zig 0.13.0 + +**Objetivo**: Herramienta de monitoreo simple y ligera, sin dependencias de servicios externos como UptimeRobot. + +## Servicios a Monitorear + +### Servidor Simba (188.245.244.244) + +| Servicio | URL/Puerto | Verificación | +|----------|------------|--------------| +| **Forgejo** | https://git.reugenio.com | HTTP 200 | +| **Forgejo SSH** | git.reugenio.com:2222 | Conexión TCP | +| **Simifactu API** | https://simifactu.com | HTTP 200 | +| **Mundisofa** | https://mundisofa.com | HTTP 200 | +| **Menzuri** | https://menzuri.com | HTTP 200 | + +## Funcionalidades Planificadas + +### Fase 1 - Básico +- [ ] Verificar HTTP status de URLs +- [ ] Verificar puertos TCP abiertos +- [ ] Output en terminal (estado actual) + +### Fase 2 - Monitoreo Continuo +- [ ] Modo daemon (corre en background) +- [ ] Intervalo configurable +- [ ] Log de eventos + +### Fase 3 - Notificaciones +- [ ] Notificación desktop (libnotify) +- [ ] Log a archivo +- [ ] Opcional: webhook/email + +## Stack Técnico + +- **Zig 0.13.0** - Lenguaje principal +- **HTTP Client** - std.http +- **TCP Sockets** - std.net +- **Sin dependencias externas** - Solo stdlib de Zig + +## Filosofía del Proyecto + +- Binario único, pequeño, portable +- Puede correr en el servidor o en máquina local +- Sin dependencias de servicios cloud externos +- Consumo mínimo de recursos + +--- + +## Equipo y Metodología + +### Quiénes Somos +- **Usuario**: Desarrollador independiente, proyectos comerciales propios +- **Claude**: Asistente de programación (Claude Code) + +### Normas de Trabajo Centralizadas + +**IMPORTANTE**: Todas las normas de trabajo están en: +``` +/mnt/cello2/arno/re/recode/TEAM_STANDARDS/ +``` + +**Archivos clave a leer**: +- `LAST_UPDATE.md` - **LEER PRIMERO** - Cambios recientes en normas +- `NORMAS_TRABAJO_CONSENSUADAS.md` - Metodología fundamental +- `QUICK_REFERENCE.md` - Cheat sheet rápido +- `INFRASTRUCTURE/` - Documentación de servidores (IPs, puertos, servicios) + +### Protocolo de Comunicación Entre Proyectos + +1. **Al iniciar conversación**: Leer `TEAM_STANDARDS/LAST_UPDATE.md` +2. **Si modificas normas**: Actualizar `LAST_UPDATE.md` con fecha y cambios +3. **Commits**: Siempre a Forgejo (git.reugenio.com), nunca a GitHub + +### Control de Versiones + +```bash +# Este proyecto +git remote: git@git.reugenio.com:reugenio/service-monitor.git (pendiente crear) + +# Crear repo en Forgejo cuando esté listo el código inicial +``` + +### Documentación del Sistema Local + +El ordenador de trabajo está documentado en: +``` +/home/re/cello/claude/claude.md +/home/re/cello/claude/docs/ +``` + +### Documentación del Servidor + +Información detallada del servidor Simba: +``` +/mnt/cello2/arno/re/recode/TEAM_STANDARDS/INFRASTRUCTURE/SERVIDOR_HETZNER.md +``` + +--- + +## Estructura del Proyecto (Planificada) + +``` +service-monitor/ +├── CLAUDE.md # Este archivo +├── src/ +│ ├── main.zig # Punto de entrada +│ ├── checker.zig # Lógica de verificación +│ ├── http.zig # Cliente HTTP +│ ├── tcp.zig # Verificación TCP +│ ├── config.zig # Configuración de servicios +│ └── notify.zig # Sistema de notificaciones +├── build.zig # Sistema de build de Zig +├── services.json # Lista de servicios a monitorear (configurable) +└── README.md # Documentación de uso +``` + +## Comandos Útiles + +```bash +# Compilar +zig build + +# Ejecutar verificación única +zig build run + +# Modo daemon +zig build run -- --daemon --interval 60 + +# Release para servidor +zig build -Doptimize=ReleaseFast -Dtarget=x86_64-linux +``` + +## Ejemplo de Uso (Planificado) + +```bash +# Verificar todos los servicios una vez +$ service-monitor check +✅ git.reugenio.com - 200 OK (45ms) +✅ git.reugenio.com:2222 - TCP OK (12ms) +✅ simifactu.com - 200 OK (89ms) +✅ mundisofa.com - 200 OK (67ms) +✅ menzuri.com - 200 OK (72ms) + +# Modo watch (cada 60 segundos) +$ service-monitor watch --interval 60 +``` + +--- + +## Notas de Desarrollo + +*Esta sección se irá llenando conforme avance el proyecto* + +--- + +**Estado**: Proyecto nuevo, pendiente iniciar desarrollo (después de forgejo-cli) diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a03c75c --- /dev/null +++ b/build.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "service-monitor", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); + + // Comando: zig build run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Ejecutar service-monitor"); + run_step.dependOn(&run_cmd.step); + + // Tests + const unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Ejecutar tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/docs/DIARIO_DESARROLLO.md b/docs/DIARIO_DESARROLLO.md new file mode 100644 index 0000000..97a3460 --- /dev/null +++ b/docs/DIARIO_DESARROLLO.md @@ -0,0 +1,53 @@ +# Diario de Desarrollo - service-monitor + +## 2025-12-07 - Sesión 1: Inicio proyecto + +### Contexto +- Proyecto nuevo: Monitor de servicios para servidor Hetzner (Simba) +- Lenguaje: Zig 0.13.0 +- Objetivo: Herramienta ligera, sin dependencias externas, potencialmente open source + +### Implementado - Fase 1 Básica + +**Estructura creada:** +``` +service-monitor/ +├── build.zig # Sistema build Zig +├── CLAUDE.md # Especificación proyecto +├── docs/ # Documentación +│ └── DIARIO_DESARROLLO.md +└── src/ + ├── main.zig # Entry point + output terminal + ├── http.zig # Verificación HTTP/HTTPS (std.http.Client) + ├── tcp.zig # Verificación TCP (std.net) + └── config.zig # Lista servicios hardcoded +``` + +**Servicios configurados:** +1. Forgejo HTTP - https://git.reugenio.com +2. Forgejo SSH - git.reugenio.com:2222 +3. Simifactu API - https://simifactu.com +4. Mundisofa - https://mundisofa.com +5. Menzuri - https://menzuri.com + +**Detalles técnicos Zig 0.13:** +- `std.http.Client.open()` requiere `server_header_buffer` obligatorio +- `std.time.Timer` debe ser `var` (no `const`) para llamar `.read()` +- Output con colores ANSI: `\x1b[32m` verde, `\x1b[31m` rojo, `\x1b[0m` reset + +**Normas aplicadas:** +- Doc comments (`///`) en todas las funciones públicas +- Código idiomático Zig (snake_case, error handling explícito) +- Preparado para open source + +### Pendiente - Fases 2 y 3 +- [ ] Fase 2: Modo daemon + intervalo configurable + logs +- [ ] Fase 3: Notificaciones (libnotify, webhook, email) + +### Comandos +```bash +zig build # Compilar +zig build run # Ejecutar verificación +``` + +--- diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..e84ca0a --- /dev/null +++ b/src/config.zig @@ -0,0 +1,79 @@ +//! Configuración de servicios a monitorear +//! +//! Define la lista de servicios del servidor Simba (Hetzner) que deben +//! ser verificados periódicamente. + +const std = @import("std"); + +/// Tipo de verificación a realizar. +pub const CheckType = enum { + /// Verificación HTTP/HTTPS (espera status 200 OK). + http, + /// Verificación TCP (conexión a puerto). + tcp, +}; + +/// Definición de un servicio a monitorear. +pub const Service = struct { + /// Nombre descriptivo del servicio (para mostrar en output). + name: []const u8, + /// 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", + }, +}; + +/// Retorna la lista de servicios a monitorear. +/// +/// 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; +} diff --git a/src/http.zig b/src/http.zig new file mode 100644 index 0000000..942c89a --- /dev/null +++ b/src/http.zig @@ -0,0 +1,75 @@ +//! Módulo de verificación HTTP +//! +//! Realiza peticiones HTTP/HTTPS y verifica que los servicios respondan +//! correctamente con status 200 OK. + +const std = @import("std"); + +/// Errores posibles durante la verificación HTTP. +pub const HttpCheckError = error{ + /// No se pudo resolver el hostname. + DnsResolutionFailed, + /// No se pudo establecer conexión con el servidor. + ConnectionFailed, + /// El servidor no respondió a tiempo. + Timeout, + /// El servidor respondió con un status code diferente a 200. + UnexpectedStatus, + /// Error al procesar la respuesta HTTP. + InvalidResponse, + /// Error de TLS/SSL. + TlsError, + /// Error de red genérico. + NetworkError, +}; + +/// Verifica que una URL HTTP/HTTPS responda con status 200 OK. +/// +/// Realiza una petición GET a la URL especificada y mide el tiempo de respuesta. +/// Soporta tanto HTTP como HTTPS (TLS). +/// +/// Parámetros: +/// - allocator: Allocator para memoria temporal. +/// - url: URL completa a verificar (ej: "https://example.com"). +/// +/// Retorna: +/// - El tiempo de respuesta en milisegundos si el status es 200. +/// - Un error HttpCheckError si algo falla. +/// +/// Ejemplo: +/// ```zig +/// const time_ms = try http.check(allocator, "https://git.reugenio.com"); +/// std.debug.print("Respuesta en {d}ms\n", .{time_ms}); +/// ``` +pub fn check(allocator: std.mem.Allocator, url: []const u8) HttpCheckError!u64 { + var timer = std.time.Timer.start() catch return HttpCheckError.NetworkError; + + // Parsear la URL + const uri = std.Uri.parse(url) catch return HttpCheckError.InvalidResponse; + + // Crear cliente HTTP + var client = std.http.Client{ .allocator = allocator }; + defer client.deinit(); + + // Buffer para headers de respuesta (requerido en Zig 0.13) + var header_buffer: [8192]u8 = undefined; + + // Realizar la petición - API Zig 0.13 + var request = client.open(.GET, uri, .{ + .server_header_buffer = &header_buffer, + }) catch return HttpCheckError.ConnectionFailed; + defer request.deinit(); + + request.send() catch return HttpCheckError.NetworkError; + request.wait() catch return HttpCheckError.Timeout; + + // Verificar status + if (request.response.status != .ok) { + return HttpCheckError.UnexpectedStatus; + } + + const elapsed_ns = timer.read(); + const elapsed_ms = elapsed_ns / std.time.ns_per_ms; + + return elapsed_ms; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..b56f229 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,47 @@ +//! 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. +//! +//! Uso: +//! zig build run - Verificar todos los servicios una vez +//! zig build run -- --help - Mostrar ayuda + +const std = @import("std"); +const http = @import("http.zig"); +const tcp = @import("tcp.zig"); +const config = @import("config.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const stdout = std.io.getStdOut().writer(); + + try stdout.print("\n=== Service Monitor ===\n\n", .{}); + + const services = config.getServices(); + var all_ok = true; + + for (services) |service| { + const result = switch (service.check_type) { + .http => http.check(allocator, service.url), + .tcp => tcp.check(allocator, service.host, service.port), + }; + + if (result) |time_ms| { + try stdout.print("\x1b[32m✓\x1b[0m {s} - OK ({d}ms)\n", .{ service.name, time_ms }); + } else |err| { + all_ok = false; + try stdout.print("\x1b[31m✗\x1b[0m {s} - ERROR: {}\n", .{ service.name, err }); + } + } + + try stdout.print("\n", .{}); + + if (!all_ok) { + std.process.exit(1); + } +} diff --git a/src/tcp.zig b/src/tcp.zig new file mode 100644 index 0000000..c5fb1e7 --- /dev/null +++ b/src/tcp.zig @@ -0,0 +1,60 @@ +//! Módulo de verificación TCP +//! +//! Verifica que un puerto TCP esté abierto y aceptando conexiones. +//! Útil para servicios como SSH, bases de datos, etc. + +const std = @import("std"); + +/// Errores posibles durante la verificación TCP. +pub const TcpCheckError = error{ + /// No se pudo resolver el hostname. + DnsResolutionFailed, + /// No se pudo establecer conexión con el puerto. + ConnectionRefused, + /// El servidor no respondió a tiempo. + Timeout, + /// Error de red genérico. + NetworkError, +}; + +/// Timeout por defecto para conexiones TCP (5 segundos). +const DEFAULT_TIMEOUT_MS = 5000; + +/// Verifica que un puerto TCP esté abierto y aceptando conexiones. +/// +/// Intenta establecer una conexión TCP al host:puerto especificado. +/// Si la conexión se establece correctamente, el servicio se considera activo. +/// +/// Parámetros: +/// - allocator: Allocator para memoria temporal (resolución DNS). +/// - host: Hostname o IP del servidor (ej: "git.reugenio.com"). +/// - port: Puerto TCP a verificar (ej: 2222 para SSH). +/// +/// Retorna: +/// - El tiempo de conexión en milisegundos si el puerto está abierto. +/// - Un error TcpCheckError si la conexión falla. +/// +/// Ejemplo: +/// ```zig +/// const time_ms = try tcp.check(allocator, "git.reugenio.com", 2222); +/// std.debug.print("Conexión TCP en {d}ms\n", .{time_ms}); +/// ``` +pub fn check(allocator: std.mem.Allocator, host: []const u8, port: u16) TcpCheckError!u64 { + var timer = std.time.Timer.start() catch return TcpCheckError.NetworkError; + + // Intentar conexión TCP con resolución DNS + const stream = std.net.tcpConnectToHost(allocator, host, port) catch |err| { + return switch (err) { + error.ConnectionRefused => TcpCheckError.ConnectionRefused, + error.ConnectionTimedOut => TcpCheckError.Timeout, + error.UnknownHostName => TcpCheckError.DnsResolutionFailed, + else => TcpCheckError.NetworkError, + }; + }; + defer stream.close(); + + const elapsed_ns = timer.read(); + const elapsed_ms = elapsed_ns / std.time.ns_per_ms; + + return elapsed_ms; +}