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:
commit
e2e19da32f
8 changed files with 527 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal 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
165
CLAUDE.md
Normal 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
37
build.zig
Normal 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
53
docs/DIARIO_DESARROLLO.md
Normal 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
79
src/config.zig
Normal 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
75
src/http.zig
Normal 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
47
src/main.zig
Normal 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
60
src/tcp.zig
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue