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

  1. Qué significa «la red va bien», de forma medible
  2. Comprobación puntual frente a tendencia
  3. Diseño: catálogo de objetivos y una función por tipo
  4. Las comprobaciones, una a una
  5. El despachador y el resultado normalizado
  6. Paralelismo: por qué aquí sí compensa
  7. Reintentos con retroceso
  8. Estado y alertas solo en las transiciones
  9. Ventanas de mantenimiento
  10. Histórico y subcomando resumen
  11. Modo directo, timer y puesta en producción

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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ó».

  1. Histórico y subcomando resumen

Cada 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.

  1. 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 ping y 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.timer no se ejecuta, nadie lo nota; una comprobación de «antigüedad de la última línea del histórico» en vigilante.sh cierra ese agujero.

Ejercicios

  1. 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.
  2. Traza de ruta ante fallo. Cuando un objetivo ping pase a FALLO, adjunta automáticamente al log una traza de ruta hacia el destino, sin retrasar el ciclo normal.
  3. Informe de tendencia. Extiende resumen con --comparar MES que 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

Módulo 2: Comandos Básicos de Bash

Módulo 3: Fundamentos de Scripting

Módulo 4: Scripting Intermedio

Módulo 5: Técnicas Avanzadas de Scripting

Módulo 6: Trabajando con Herramientas Externas

Módulo 7: Automatización y Programación

Módulo 8: Mejores Prácticas y Optimización

Módulo 9: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados