Migración a Zig 0.15.2

Cambios principales:
- build.zig: root_source_file → root_module con b.createModule()
- stdout: std.io.getStdOut() → std.fs.File.stdout().deprecatedWriter()
- ArrayList: std.ArrayList → std.array_list.Managed
- file.reader(): deprecatedReader() para compatibilidad
- HTTP Client: client.open/send/wait → client.fetch()
- sleep: std.time.sleep → std.Thread.sleep

Código funciona correctamente con Zig 0.15.2.

🤖 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-08 01:03:43 +01:00
parent a011d9e552
commit f31ce95afe
6 changed files with 333 additions and 31 deletions

View file

@ -1,12 +1,12 @@
# service-monitor - Monitor de Servicios en Zig # service-monitor - Monitor de Servicios en Zig
> **Última actualización**: 2025-12-07 > **Última actualización**: 2025-12-08
## Descripción del Proyecto ## 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. 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 **Lenguaje**: Zig 0.15.2
**Objetivo**: Herramienta de monitoreo simple y ligera, sin dependencias de servicios externos como UptimeRobot. **Objetivo**: Herramienta de monitoreo simple y ligera, sin dependencias de servicios externos como UptimeRobot.

View file

@ -6,9 +6,11 @@ pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "service-monitor", .name = "service-monitor",
.root_source_file = b.path("src/main.zig"), .root_module = b.createModule(.{
.target = target, .root_source_file = b.path("src/main.zig"),
.optimize = optimize, .target = target,
.optimize = optimize,
}),
}); });
// Instalar en carpeta raíz del proyecto (no en zig-out/) // Instalar en carpeta raíz del proyecto (no en zig-out/)
@ -29,9 +31,11 @@ pub fn build(b: *std.Build) void {
// Tests // Tests
const unit_tests = b.addTest(.{ const unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"), .root_module = b.createModule(.{
.target = target, .root_source_file = b.path("src/main.zig"),
.optimize = optimize, .target = target,
.optimize = optimize,
}),
}); });
const run_unit_tests = b.addRunArtifact(unit_tests); const run_unit_tests = b.addRunArtifact(unit_tests);

308
docs/PROYECTO_COMPLETO.md Normal file
View file

@ -0,0 +1,308 @@
# Service Monitor - Documentación Completa
> **Fecha**: 2025-12-08
> **Versión**: 1.1
> **Lenguaje**: Zig 0.15.2
> **Repositorio**: https://git.reugenio.com/reugenio/service-monitor
## Descripción
Monitor de servicios HTTP y TCP para verificar disponibilidad del servidor Hetzner (Simba). Notifica por desktop, email y Telegram cuando hay servicios caídos.
## Características Implementadas
| Feature | Estado | Descripción |
|---------|--------|-------------|
| Verificación HTTP/HTTPS | ✅ | GET + status 200 + tiempo respuesta |
| Verificación TCP | ✅ | Conexión a puerto + DNS resolution |
| Modo watch | ✅ | Loop continuo con intervalo configurable |
| Modo daemon | ✅ | Background con fork() + setsid() |
| Log a archivo | ✅ | Append mode, timestamps ISO |
| Config externo | ✅ | Archivo CSV simple |
| Notificación desktop | ✅ | notify-send (Linux) |
| Notificación email | ✅ | SMTP con AUTH LOGIN (sin TLS) |
| Notificación Telegram | ✅ | Bot API via curl |
## Estructura del Proyecto
```
service-monitor/
├── build.zig # Sistema de build Zig
├── CLAUDE.md # Especificación del proyecto
├── services.conf # Configuración activa
├── services.conf.example # Ejemplo documentado
├── service-monitor # Binario compilado
├── service-monitor.log # Log de ejecución
├── service-monitor.pid # PID en modo daemon
├── src/
│ ├── main.zig # Entry point + CLI + lógica principal (317 líneas)
│ ├── http.zig # Cliente HTTP/HTTPS (77 líneas)
│ ├── tcp.zig # Verificación TCP (68 líneas)
│ ├── config.zig # Parser de configuración (233 líneas)
│ ├── notify.zig # Notificaciones desktop (77 líneas)
│ ├── daemon.zig # Daemonización (101 líneas)
│ ├── smtp.zig # Cliente SMTP (260 líneas)
│ └── telegram.zig # Cliente Telegram (107 líneas)
└── docs/
├── DIARIO_DESARROLLO.md
└── PROYECTO_COMPLETO.md # Este archivo
```
## Uso
### Comandos Básicos
```bash
# Compilar
zig build
# Verificar una vez
./service-monitor
# Modo watch (cada 60 segundos)
./service-monitor --watch
# Modo watch cada 30 segundos
./service-monitor -w -i 30
# Con log a archivo
./service-monitor -w -l
# Con notificaciones desktop
./service-monitor -w -n
# Daemon en background
./service-monitor -w -d
# Todo junto
./service-monitor -w -i 60 -d -l -n
# Config personalizado
./service-monitor -c /ruta/config.conf
# Ayuda
./service-monitor --help
```
### Opciones CLI
| Opción | Corto | Descripción | Default |
|--------|-------|-------------|---------|
| `--watch` | `-w` | Modo continuo | false |
| `--interval` | `-i` | Segundos entre checks | 60 |
| `--log` | `-l` | Log a archivo | service-monitor.log |
| `--notify` | `-n` | Notificaciones desktop | false |
| `--daemon` | `-d` | Ejecutar en background | false |
| `--config` | `-c` | Archivo de configuración | services.conf |
| `--help` | `-h` | Mostrar ayuda | - |
### Gestión del Daemon
```bash
# Iniciar daemon
./service-monitor -w -d -l -n
# Ver PID
cat service-monitor.pid
# Ver logs
tail -f service-monitor.log
# Detener daemon
kill $(cat service-monitor.pid)
```
## Archivo de Configuración
### Formato
```
# Comentarios con #
# tipo,parámetros...
# Servicios HTTP
http,Nombre Servicio,https://url.com
# Servicios TCP
tcp,Nombre Servicio,hostname,puerto
# Email (destinatarios)
email,destinatario@ejemplo.com
# Email (servidor SMTP)
email_smtp,smtp.servidor.com,puerto,usuario,password,from@email.com
# Telegram
telegram,bot_token,chat_id
```
### Ejemplo Real (services.conf)
```
# Servicios HTTP
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
# Telegram
telegram,8158165444:AAFxUjLChsuusgFD5B1gt2svt8NflvAm1M8,1481345275
```
## Servicios Monitoreados (Servidor Simba)
| Servicio | Tipo | Endpoint | Puerto |
|----------|------|----------|--------|
| Forgejo HTTP | HTTPS | git.reugenio.com | 443 |
| Forgejo SSH | TCP | git.reugenio.com | 2222 |
| Simifactu | HTTPS | simifactu.com | 443 |
| Mundisofa | HTTPS | mundisofa.com | 443 |
| Menzuri | HTTPS | menzuri.com | 443 |
## Output
### Terminal (con colores)
```
[2025-12-07 20:53:24]
✓ Forgejo (HTTP) - OK (768ms)
✓ Forgejo (SSH) - OK (63ms)
✓ Simifactu API - OK (481ms)
✓ Mundisofa - OK (444ms)
✓ Menzuri - OK (456ms)
```
### Log (sin colores)
```
[2025-12-07 20:53:24]
OK Forgejo (HTTP) (768ms)
OK Forgejo (SSH) (63ms)
OK Simifactu API (481ms)
OK Mundisofa (444ms)
OK Menzuri (456ms)
```
### Telegram (cuando hay errores)
```
⚠️ ALERTA: Servicios caídos
- Nombre Servicio 1
- Nombre Servicio 2
```
## Detalles Técnicos por Módulo
### main.zig
- Entry point de la aplicación
- Parser de argumentos CLI
- Lógica de loop watch/daemon
- Coordinación de todos los módulos
- Gestión de timestamps (UTC)
### http.zig
- Cliente HTTP usando `std.http.Client`
- Soporte HTTPS con TLS automático
- Buffer de headers: 8192 bytes
- Medición de tiempo de respuesta
- Errores tipados: DnsResolutionFailed, ConnectionFailed, Timeout, UnexpectedStatus
### tcp.zig
- Conexión TCP con `std.net.tcpConnectToHost`
- Resolución DNS automática
- Medición de tiempo de conexión
- Errores tipados: DnsResolutionFailed, ConnectionRefused, Timeout
### config.zig
- Parser CSV simple
- Soporte para comentarios (#)
- Estructuras: Service, SmtpConfig, TelegramConfig, Config
- Fallback a config por defecto si no existe archivo
- Gestión de memoria con allocator
### notify.zig
- Wrapper sobre `notify-send`
- Niveles de urgencia: low, normal, critical
- Funciones: send(), sendError(), sendRecovery()
### daemon.zig
- Double fork para daemonización correcta
- Syscall directo para setsid() (no disponible en std.posix Zig 0.13)
- Redirección de stdin/stdout/stderr a /dev/null
- Escritura de PID file
- Cambio a directorio raíz
### smtp.zig
- Protocolo SMTP completo (RFC 5321)
- Comandos: EHLO, AUTH LOGIN, MAIL FROM, RCPT TO, DATA, QUIT
- Autenticación Base64
- Múltiples destinatarios
- **Limitación**: No soporta STARTTLS (usar puerto 25 o servidores sin TLS)
### telegram.zig
- Bot API via curl (más fiable que std.http para POST)
- Funciones: sendMessage(), sendAlert()
- Formato de mensaje con lista de servicios
## Consumo de Recursos
| Recurso | Valor |
|---------|-------|
| RAM | ~2-5 MB |
| CPU (idle) | ~0% |
| CPU (check) | <1% por ~2s |
| Binario | ~8.4 MB (debug) |
| Red | ~10KB por ciclo |
## Commits Históricos
| Hash | Descripción |
|------|-------------|
| e2e19da | Fase 1: Monitor básico HTTP/TCP |
| 3946f83 | Fase 2: Modo watch + CLI + timestamps |
| dfcfd31 | Log a archivo |
| 5a17d74 | Fase 3: Notificaciones desktop |
| 655dcb8 | Daemon mode + config externo |
| a011d9e | SMTP y Telegram |
## Pendiente
- [ ] SMTP con STARTTLS (para Gmail/Outlook)
- [x] ~~Migración a Zig 0.15~~ (completado 2025-12-08)
- [ ] Notificación de recuperación (servicio vuelve a funcionar)
- [ ] Rate limiting de notificaciones (evitar spam)
- [ ] Métricas/estadísticas de uptime
## Notas de Migración a Zig 0.15
La migración a Zig 0.15.2 requirió los siguientes cambios:
| Cambio | Antes (0.13) | Después (0.15) |
|--------|--------------|----------------|
| Build.zig | `root_source_file` | `root_module` con `b.createModule()` |
| stdout | `std.io.getStdOut().writer()` | `std.fs.File.stdout().deprecatedWriter()` |
| ArrayList | `std.ArrayList(T).init(alloc)` | `std.array_list.Managed(T).init(alloc)` |
| file.reader() | sin args | requiere buffer, usar `deprecatedReader()` |
| HTTP Client | `client.open()` + `send()` + `wait()` | `client.fetch()` |
| sleep | `std.time.sleep()` | `std.Thread.sleep()` |
## Referencias
- Zig 0.15 stdlib: https://ziglang.org/documentation/0.15.0/std/
- SMTP RFC 5321: https://tools.ietf.org/html/rfc5321
- Telegram Bot API: https://core.telegram.org/bots/api
- Guía Zig 0.15: /mnt/cello2/arno/re/recode/TEAM_STANDARDS/INFRASTRUCTURE/ZIG_0.15_GUIA.md

View file

@ -113,12 +113,12 @@ pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Config {
/// Parsea el contenido de un archivo de configuración. /// Parsea el contenido de un archivo de configuración.
fn parseConfigFile(allocator: std.mem.Allocator, file: std.fs.File) !Config { fn parseConfigFile(allocator: std.mem.Allocator, file: std.fs.File) !Config {
var services = std.ArrayList(Service).init(allocator); var services = std.array_list.Managed(Service).init(allocator);
var emails = std.ArrayList([]const u8).init(allocator); var emails = std.array_list.Managed([]const u8).init(allocator);
var smtp = SmtpConfig{}; var smtp = SmtpConfig{};
var telegram = TelegramConfig{}; var telegram = TelegramConfig{};
const reader = file.reader(); const reader = file.deprecatedReader();
var buf: [1024]u8 = undefined; var buf: [1024]u8 = undefined;
while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| { while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| {

View file

@ -44,27 +44,17 @@ pub const HttpCheckError = error{
pub fn check(allocator: std.mem.Allocator, url: []const u8) HttpCheckError!u64 { pub fn check(allocator: std.mem.Allocator, url: []const u8) HttpCheckError!u64 {
var timer = std.time.Timer.start() catch return HttpCheckError.NetworkError; var timer = std.time.Timer.start() catch return HttpCheckError.NetworkError;
// Parsear la URL // Crear cliente HTTP (Zig 0.15 API)
const uri = std.Uri.parse(url) catch return HttpCheckError.InvalidResponse; var client: std.http.Client = .{ .allocator = allocator };
// Crear cliente HTTP
var client = std.http.Client{ .allocator = allocator };
defer client.deinit(); defer client.deinit();
// Buffer para headers de respuesta (requerido en Zig 0.13) // Realizar la petición usando fetch() - API Zig 0.15
var header_buffer: [8192]u8 = undefined; const result = client.fetch(.{
.location = .{ .url = url },
// Realizar la petición - API Zig 0.13
var request = client.open(.GET, uri, .{
.server_header_buffer = &header_buffer,
}) catch return HttpCheckError.ConnectionFailed; }) catch return HttpCheckError.ConnectionFailed;
defer request.deinit();
request.send() catch return HttpCheckError.NetworkError;
request.wait() catch return HttpCheckError.Timeout;
// Verificar status // Verificar status
if (request.response.status != .ok) { if (result.status != .ok) {
return HttpCheckError.UnexpectedStatus; return HttpCheckError.UnexpectedStatus;
} }

View file

@ -50,7 +50,7 @@ pub fn main() !void {
defer _ = gpa.deinit(); defer _ = gpa.deinit();
const allocator = gpa.allocator(); const allocator = gpa.allocator();
const stdout = std.io.getStdOut().writer(); const stdout = std.fs.File.stdout().deprecatedWriter();
// Parsear argumentos // Parsear argumentos
const options = parseArgs() catch |err| { const options = parseArgs() catch |err| {
@ -131,7 +131,7 @@ pub fn main() !void {
while (true) { while (true) {
_ = try runChecks(allocator, output_writer, log_file, options.notify, &cfg); _ = try runChecks(allocator, output_writer, log_file, options.notify, &cfg);
std.time.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s); std.Thread.sleep(@as(u64, options.interval_seconds) * std.time.ns_per_s);
} }
} else { } else {
try stdout.print("\n=== Service Monitor ===\n\n", .{}); try stdout.print("\n=== Service Monitor ===\n\n", .{});
@ -146,7 +146,7 @@ pub fn main() !void {
/// Ejecuta verificación de todos los servicios. /// Ejecuta verificación de todos los servicios.
fn runChecks( fn runChecks(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
stdout: ?std.fs.File.Writer, stdout: ?std.fs.File.DeprecatedWriter,
log_file: ?std.fs.File, log_file: ?std.fs.File,
notify_enabled: bool, notify_enabled: bool,
cfg: *const config.Config, cfg: *const config.Config,
@ -181,7 +181,7 @@ fn runChecks(
try out.print(timestamp_str ++ "\n", timestamp_args); try out.print(timestamp_str ++ "\n", timestamp_args);
} }
const log_writer = if (log_file) |f| f.writer() else null; const log_writer = if (log_file) |f| f.deprecatedWriter() 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);
} }