Los tres proyectos anteriores miran hacia dentro del servidor: su estado, sus logs, sus datos. Este mira hacia fuera. Cuando alguien en Veloz Envíos dice «la red va mal», normalmente quiere decir que la aplicación tarda, y el trabajo de operaciones es convertir esa frase en una medida. monitor-red.sh hace exactamente eso: define de forma medible qué significa que la red y el servicio están bien, lo comprueba en paralelo cada pocos minutos, alerta solo cuando algo cambia y guarda un histórico del que sale la disponibilidad del mes.
Contenido
- Qué significa «la red va bien», de forma medible
- Comprobación puntual frente a tendencia
- Diseño: catálogo de objetivos y una función por tipo
- Las comprobaciones, una a una
- El despachador y el resultado normalizado
- Paralelismo: por qué aquí sí compensa
- Reintentos con retroceso
- Estado y alertas solo en las transiciones
- Ventanas de mantenimiento
- Histórico y subcomando
resumen - Modo directo, timer y puesta en producción
- Qué significa «la red va bien», de forma medible
«Va bien» no es monitorizable. Estas cinco frases sí:
| Afirmación medible | Cómo se comprueba | Umbral en Veloz Envíos |
|---|---|---|
| El host responde | ping -c1 -W2 |
Pérdida 0 %, RTT < 50 ms |
| El nombre resuelve | dig +short |
Devuelve al menos una A, < 500 ms |
| El puerto está abierto | nc -z -w2 o /dev/tcp |
Conexión en < 2 s |
| La API responde correctamente | curl -w '%{http_code} %{time_total}' |
200 y < 800 ms |
| El certificado no ha caducado | openssl s_client |
Más de 15 días de margen |
Cada fila es una comprobación independiente, con su umbral y su criticidad. La distinción importa: un ping que responde no dice nada sobre si la API funciona, y una API que devuelve 200 en cuatro segundos está rota aunque el código sea correcto. Un servicio se declara sano por su respuesta útil, no por su alcanzabilidad.
- Comprobación puntual frente a tendencia
Son dos productos distintos que comparten datos:
- La comprobación puntual responde ahora: ¿está caído? Su salida es un estado y su consecuencia posible es una alerta.
- La tendencia responde a lo largo del mes: ¿cuánto ha estado disponible, cómo evoluciona la latencia? Su salida es un número de informe y su consecuencia es una decisión de arquitectura.
Confundirlas produce los dos fallos clásicos: alertar por una latencia que lleva subiendo tres semanas (tarde) o dibujar gráficas sin que nadie se entere de una caída (inútil). El diseño las separa: cada ejecución alerta si procede y añade una línea al histórico; el subcomando resumen solo lee el histórico.
- Diseño: catálogo de objetivos y una función por tipo
Igual que los perfiles de respaldo (09-03), los objetivos viven en un fichero:
# etc/monitor-red.d/objetivos.conf # nombre|tipo|destino|umbral|criticidad api-salud|http|http://localhost:8080/salud|800|critica api-envios|http|http://localhost:8080/envios?ciudad=Valencia|1500|alta db-puerto|puerto|10.0.0.20:5432|2000|critica dns-interno|dns|api.veloz.local|500|media srv-02|ping|srv-veloz-02|50|alta pasarela|ping|10.0.0.1|30|critica
El separador es | y no coma porque las URL llevan comas con facilidad y las barras verticales no. Se lee con IFS='|' read (03-06), ignorando comentarios y líneas vacías:
lee_objetivos() {
local nombre tipo destino umbral crit
while IFS='|' read -r nombre tipo destino umbral crit; do
[[ $nombre == \#* || -z $nombre ]] && continue
printf '%s|%s|%s|%s|%s\n' "$nombre" "$tipo" "$destino" "$umbral" "${crit:-media}"
done < "$OBJETIVOS"
}Cada tipo tiene su función comprueba_<tipo> y devuelve el convenio de los plugins clásicos (07-04): 0 = OK, 1 = AVISO, 2 = CRÍTICO, 3 = DESCONOCIDO. Ese convenio es lo que permite que el resto del script no sepa nada de ping ni de curl.
- Las comprobaciones, una a una
comprueba_ping() { # $1 destino, $2 umbral ms -> imprime "ms mensaje"
local salida rtt
salida=$(ping -c1 -W2 -n "$1" 2>/dev/null) || { printf '0 sin respuesta\n'; return 2; }
rtt=${salida#*time=}; rtt=${rtt%% *}
printf '%s rtt=%sms\n' "$rtt" "$rtt"
awk -v r="$rtt" -v u="$2" 'BEGIN {exit !(r+0 > u)}' && return 1 || return 0
}
comprueba_dns() {
local ini fin ip
ini=$(date +%s%3N); ip=$(timeout 3 dig +short +time=2 +tries=1 "$1" A | head -1)
fin=$(date +%s%3N)
[[ -n $ip ]] || { printf '0 no resuelve\n'; return 2; }
printf '%s resuelve a %s\n' "$((fin - ini))" "$ip"
(( fin - ini > $2 )) && return 1 || return 0
}
comprueba_puerto() {
local host=${1%:*} puerto=${1##*:} ini fin
ini=$(date +%s%3N)
timeout 2 bash -c "exec 3<>/dev/tcp/$host/$puerto" 2>/dev/null ||
{ printf '0 cerrado o filtrado\n'; return 2; }
fin=$(date +%s%3N); printf '%s abierto\n' "$((fin - ini))"
}
comprueba_http() {
local resp cod ms
resp=$(curl -sS -o /dev/null --max-time 5 -w '%{http_code} %{time_total}' "$1" 2>/dev/null) \
|| { printf '0 sin conexion\n'; return 2; }
read -r cod ms <<< "$resp"
ms=$(awk -v t="$ms" 'BEGIN {printf "%d", t * 1000}')
[[ $cod == 200 ]] || { printf '%s codigo %s\n' "$ms" "$cod"; return 2; }
printf '%s 200 en %sms\n' "$ms" "$ms"
(( ms > $2 )) && return 1 || return 0
}Detalles que vienen de 06-04 y 06-05. ping -n evita la resolución inversa, que puede añadir segundos de espera por un DNS lento y falsear la medida. /dev/tcp con timeout no necesita nc instalado, pero es un bashismo puro (08-07): si el script tuviera que correr en dash, aquí iría nc -z -w2. curl -w da el código y el tiempo en una sola invocación, en lugar de medir por fuera con date; --max-time es obligatorio, porque una comprobación sin límite puede colgar todo el ciclo. Y el HTTP 200 se exige explícitamente: un 302 hacia una página de mantenimiento no es servicio. Cada función imprime <milisegundos> <mensaje> y devuelve el estado por el código de salida: separar el dato (stdout) del veredicto (código) es la misma división de 09-01 entre recolectar y decidir.
- El despachador y el resultado normalizado
ejecuta_objetivo() { # nombre|tipo|destino|umbral|criticidad -> linea TSV
local nombre tipo destino umbral crit
IFS='|' read -r nombre tipo destino umbral crit <<< "$1"
local fn=comprueba_$tipo salida estado
declare -F "$fn" >/dev/null || { veloz_log_error "tipo desconocido: $tipo"; return 3; }
salida=$(con_reintentos "$fn" "$destino" "$umbral"); estado=$?
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(date -Is)" "$nombre" "$tipo" "$estado" "${salida%% *}" "${salida#* }"
}declare -F como lista blanca (08-03): el tipo del fichero se convierte en nombre de función solo si esa función existe. Un case explícito sería igual de válido (04-05) y más legible para quien no conozca el truco; la ventaja de declare -F es que añadir un tipo nuevo es escribir una función y nada más. El resultado normalizado —fecha ISO, nombre, tipo, estado, milisegundos, mensaje— es el formato interno que consumen el informe, las alertas y el histórico: otra vez la misma arquitectura de los tres proyectos anteriores.
- Paralelismo: por qué aquí sí compensa
Con doce objetivos y una media de 700 ms cada uno, en serie son ocho segundos y medio. Y no son ocho segundos de CPU: son ocho segundos de espera. Ese es el criterio de 08-02 para paralelizar: cuando el proceso está bloqueado esperando a la red, lanzar veinte a la vez no cuesta prácticamente nada.
ejecuta_todos() {
local tmp; tmp=$(mktemp -d); trap 'rm -rf "$tmp"' RETURN
local i=0 obj
while IFS= read -r obj; do
ejecuta_objetivo "$obj" > "$tmp/$(printf '%03d' "$i")" &
((i++))
(( $(jobs -rp | wc -l) >= PARALELISMO )) && wait -n
done < <(lee_objetivos)
wait
cat "$tmp"/*
}Tres cosas hacen que esto sea correcto y no un caos. Cada trabajo escribe en su propio fichero temporal numerado (05-02): si todos escribieran en el mismo, las líneas se entrelazarían. El nombre lleva un índice con ceros a la izquierda (%03d) para que el cat final devuelva el orden del catálogo, no el orden de finalización —un informe cuyas filas bailan cada ejecución es ilegible y hace imposible comparar dos salidas—. Y wait -n limita los trabajos simultáneos: espera a que termine uno cualquiera antes de lanzar el siguiente, que es el equivalente de xargs -P con procesos de fondo. Con xargs -0 -P "$PARALELISMO" el código sería más corto, a cambio de tener que exportar las funciones; con veinte objetivos, cualquiera de las dos vale.
- Reintentos con retroceso
Un paquete perdido no es una caída. Alertar por la primera comprobación fallida genera el ruido que hace que la gente ignore las alertas, que es la peor avería posible de un sistema de monitorización.
con_reintentos() { # $1 = funcion, resto = argumentos
local fn=$1 intento espera=1 salida estado; shift
for intento in 1 2 3; do
salida=$("$fn" "$@"); estado=$?
(( estado == 0 )) && { printf '%s' "$salida"; return 0; }
(( intento < 3 )) && { sleep "$espera"; espera=$((espera * 2)); }
done
printf '%s' "$salida"; return "$estado"
}Retroceso exponencial (05-03): 1 s, 2 s. Se reintenta solo el fallo y se sale al primer éxito, así que el caso normal no cuesta nada. Tres intentos y un máximo de tres segundos de espera son suficientes para filtrar el ruido sin retrasar el ciclo. Cuidado con multiplicar: tres reintentos por doce objetivos secuenciales serían un ciclo de minuto y medio en un apagón general; con el paralelismo del apartado anterior, siguen siendo cuatro segundos.
- Estado y alertas solo en las transiciones
El fichero de estado guarda la última situación conocida de cada objetivo, y la alerta se dispara únicamente cuando cambia (07-04):
procesa_estados() {
declare -A previo
[[ -r $ESTADO ]] && while IFS=$'\t' read -r n e; do previo[$n]=$e; done < "$ESTADO"
local nuevo=$ESTADO.parcial ant; : > "$nuevo"
local fecha nombre tipo estado ms msg
while IFS=$'\t' read -r fecha nombre tipo estado ms msg; do
printf '%s\t%s\n' "$nombre" "$estado" >> "$nuevo"
ant=${previo[$nombre]:-0}
(( ant == 0 && estado != 0 )) && alerta FALLO "$nombre" "$msg" "$ms"
(( ant != 0 && estado == 0 )) && alerta RECUPERADO "$nombre" "$msg" "$ms"
done
mv "$nuevo" "$ESTADO"
}Solo dos transiciones generan aviso: OK→FALLO y FALLO→OK. Un objetivo que lleva seis horas caído no vuelve a avisar cada cinco minutos, y el aviso de recuperación es tan importante como el de fallo, porque cierra el incidente sin que nadie tenga que ir a mirar. La escritura del estado usa otra vez .parcial + mv (08-03): si el script muere a mitad, el fichero de estado anterior queda intacto, y un estado corrupto provocaría una tormenta de falsas transiciones en la ejecución siguiente.
El envío usa jq -n para construir el cuerpo (06-05), nunca printf con el mensaje interpolado:
alerta() {
local clase=$1 objetivo=$2 detalle=$3 ms=$4
veloz_log_info "alerta $clase $objetivo: $detalle"
en_mantenimiento "$objetivo" && { veloz_log_info "silenciada (mantenimiento)"; return 0; }
[[ -n ${WEBHOOK:-} ]] || return 0
jq -n --arg c "$clase" --arg o "$objetivo" --arg d "$detalle" --arg h "$(hostname -s)" \
'{texto: "[\($c)] \($o) en \($h): \($d)"}' |
curl -sS --max-time 10 -X POST -H 'Content-Type: application/json' -d @- "$WEBHOOK" >/dev/null ||
veloz_log_error "no se pudo enviar la alerta"
}El fallo del webhook se registra pero no aborta el monitor: el sistema de avisos caído no debe llevarse por delante la vigilancia. Y el mensaje siempre se escribe primero en el log local, para que quede constancia aunque no salga.
- Ventanas de mantenimiento
# etc/monitor-red.d/mantenimiento.conf -> objetivo|inicio ISO|fin ISO
en_mantenimiento() {
local obj=$1 ahora o ini fin; ahora=$(date +%s)
[[ -r $MANTENIMIENTO ]] || return 1
while IFS='|' read -r o ini fin; do
[[ $o == "$obj" || $o == '*' ]] || continue
(( ahora >= $(date -d "$ini" +%s) && ahora <= $(date -d "$fin" +%s) )) && return 0
done < "$MANTENIMIENTO"
return 1
}El silenciado suprime la alerta, no la comprobación: el histórico sigue registrando el estado real, así que la disponibilidad del mes no se falsea. Es una distinción que muchos sistemas comerciales hacen mal, y la diferencia entre «no me molestes ahora» y «finjamos que no pasó».
- Histórico y subcomando
resumen
resumenCada ejecución añade sus líneas a logs/red-AAAA-MM.tsv. Un fichero por mes mantiene el tamaño acotado sin necesidad de logrotate y hace trivial el informe mensual.
resumen() { # $1 = fichero mensual
awk -F'\t' '
{ n[$2]++; if ($4 == 0) ok[$2]++; if ($5 + 0 > 0) { suma[$2] += $5; m[$2]++ }
if ($4 != 0) fallos[$2]++ }
END {
printf "%-14s %8s %8s %10s %8s\n", "OBJETIVO", "MUESTRAS", "DISPON.", "LAT.MEDIA", "FALLOS"
for (k in n)
printf "%-14s %8d %7.2f%% %8.0fms %8d\n", k, n[k], ok[k]*100/n[k],
(m[k] ? suma[k]/m[k] : 0), fallos[k]+0
}' "$1" | (read -r cab; printf '%s\n' "$cab"; sort -k3 -n)
}La disponibilidad es un cociente entre muestras, no entre minutos: con un ciclo de cinco minutos, cada muestra fallida representa cinco minutos de indisponibilidad, y conviene decirlo en el informe para que nadie confunda la precisión con la exactitud. La latencia media excluye las muestras con 0 ms, que son fallos y no mediciones —incluirlas bajaría artificialmente la media justo cuando el servicio va peor, el error de interpretación más común en monitorización—. El (read -r cab; ...) conserva la cabecera arriba mientras ordena el resto por disponibilidad ascendente, es decir, los peores primero.
- Modo directo, timer y puesta en producción
Para una sesión de diagnóstico va bien un bucle propio, que es preferible a watch porque conserva el histórico y las alertas:
modo_directo() { # \033[H\033[J limpia la pantalla en cada vuelta
while true; do printf '\033[H\033[J'; ejecuta_todos | formatea_texto; sleep "${INTERVALO:-30}"; done
}Pero en producción manda un timer de systemd (07-05) cada cinco minutos, por tres razones concretas: sobrevive al cierre de la sesión SSH, journalctl -u conserva la traza de cada ejecución, y systemctl list-timers muestra si de verdad se está ejecutando. Un bucle en una terminal olvidada parece monitorización hasta el día en que alguien cierra el portátil.
Errores Comunes y Consejos
- Monitorizar el
pingy llamarlo servicio. El host responde y la API devuelve 500: la comprobación que importa es la que hace lo que hace el usuario. - Alertar en cada ejecución mientras dura el fallo. Es la vía rápida a que se ignoren las alertas. Solo transiciones, y aviso de recuperación siempre.
- Comprobaciones sin
timeout. Un objetivo que no responde nunca bloquea el ciclo entero; con paralelismo, agota los procesos.--max-time,-W,timeout: siempre. - Paralelizar escribiendo todos en el mismo fichero. Las líneas se entrelazan: un temporal por trabajo y consolidación ordenada. Y durante el mantenimiento, silencia la alerta, nunca la comprobación, o falsearás la disponibilidad del informe mensual.
- Consejo: monitoriza también el propio monitor. Si
veloz-monitor.timerno se ejecuta, nadie lo nota; una comprobación de «antigüedad de la última línea del histórico» envigilante.shcierra ese agujero.
Ejercicios
- Caducidad del certificado TLS. Añade el tipo
tls, que avise cuando falten menos de 15 días para la caducidad y sea crítico por debajo de 5. - Traza de ruta ante fallo. Cuando un objetivo
pingpase a FALLO, adjunta automáticamente al log una traza de ruta hacia el destino, sin retrasar el ciclo normal. - Informe de tendencia. Extiende
resumencon--comparar MESque muestre la variación de disponibilidad y latencia frente al mes anterior, marcando los empeoramientos.
Soluciones
1. openssl s_client necesita -servername para SNI y una entrada por stdin que cierre la conexión:
comprueba_tls() {
local host=${1%:*} puerto=${1##*:} fin dias
fin=$(echo | timeout 5 openssl s_client -connect "$host:$puerto" -servername "$host" 2>/dev/null |
openssl x509 -noout -enddate 2>/dev/null) || { printf '0 sin certificado\n'; return 2; }
fin=${fin#notAfter=}; dias=$(( ( $(date -d "$fin" +%s) - $(date +%s) ) / 86400 ))
printf '%s caduca en %s dias (%s)\n' "$dias" "$dias" "$fin"
(( dias < 5 )) && return 2; (( dias < 15 )) && return 1; return 0
}El echo | al principio es imprescindible: sin él, s_client deja la conexión abierta esperando entrada y el timeout acaba matándolo. La aritmética de fechas es la de 04-06, restando segundos de época.
2. La traza no debe bloquear el ciclo, así que se lanza en segundo plano y con límite:
diagnostico_extra() { { timeout 20 traceroute -n -w1 -q1 "$1" 2>&1 | veloz_log_info_stdin; } & disown; }Se invoca desde alerta solo en la transición a FALLO —nunca mientras dura— para no lanzar una traza cada cinco minutos durante una caída de dos horas.
3. Se ejecuta resumen sobre los dos ficheros mensuales, se cargan en dos arrays asociativos (04-03) indexados por objetivo y se recorre comparando. La regla de presentación: mostrar la diferencia con signo y marcar solo lo que empeora más de un umbral (por ejemplo, 0,5 puntos de disponibilidad o un 20 % de latencia), porque un informe que lo marca todo no marca nada.
Conclusión
monitor-red.sh convierte «la red va bien» en cinco afirmaciones medibles con su umbral, y las comprueba desde un catálogo en fichero que crece sin tocar código. Las ideas transferibles son cuatro. El convenio 0/1/2/3 de los plugins clásicos, que permite que el despachador no sepa nada de ping ni de curl y que añadir un tipo sea escribir una función. Paralelizar lo que espera: doce objetivos en cuatro segundos en lugar de ocho y medio, con un temporal por trabajo y consolidación en orden fijo para que el informe sea comparable. Alertar en las transiciones, con aviso de recuperación y ventanas que silencian el aviso pero nunca la medición. Y separar la comprobación puntual de la tendencia: la primera alerta, la segunda informa, y ambas salen de la misma línea TSV. Por el camino: reintentos con retroceso (05-03), curl -w y /dev/tcp con timeout (06-04), jq -n para el cuerpo del webhook (06-05), wait -n para limitar la concurrencia (05-02), escritura atómica del estado (08-03) y awk para el resumen mensual (06-01).
Quedan cinco scripts que funcionan, cada uno con sus opciones, su configuración y su forma de invocarse. En 09-05, el cierre del curso, dejan de ser una colección y se convierten en un producto: veloz-ops, un único comando con subcomandos, configuración unificada, instalador idempotente, versionado con Git, batería de pruebas, despliegue en la flota, vuelta atrás y documentación.
Curso de Programación en Bash
Módulo 1: Introducción a Bash
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
