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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-07 20:43:34 +01:00
commit e2e19da32f
8 changed files with 527 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Zig build artifacts
zig-out/
zig-cache/
.zig-cache/
# Binary
service-monitor
# OS
.DS_Store
Thumbs.db

165
CLAUDE.md Normal file
View file

@ -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)

37
build.zig Normal file
View file

@ -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);
}

53
docs/DIARIO_DESARROLLO.md Normal file
View file

@ -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
```
---

79
src/config.zig Normal file
View file

@ -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;
}

75
src/http.zig Normal file
View file

@ -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;
}

47
src/main.zig Normal file
View file

@ -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);
}
}

60
src/tcp.zig Normal file
View file

@ -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;
}