← Automatización con Scripts en Bash Bloque 9 · Automatización en la práctica · Tema 40 de 42

Tema 40

Monitoreo y alertas

Los temas anteriores automatizaron tareas que hay que hacer: compilar, respaldar, desplegar. Este automatiza algo que hay que notar: que el disco se está llenando, que un servicio se detuvo, que el sitio responde con errores, que el certificado vence la semana que viene. Un servidor rara vez falla de golpe; casi siempre avisa antes, para quien esté mirando. El objetivo de este tema es que siempre haya algo mirando, y que avise a una persona solo cuando hace falta, ni antes ni cien veces.

1. Enterarse antes que los usuarios

Monitorear es comprobar de forma periódica y automática el estado de un sistema, y alertar, avisar a una persona cuando algo se sale de lo normal. Un buen monitoreo avisa de los problemas antes de que los usuarios los sufran, y no avisa de nada más.

Las dos mitades de esa definición importan por igual. Un monitoreo que no detecta un problema es inútil; pero uno que avisa demasiado es peor, porque enseña a ignorar sus avisos. Si cada día llegan veinte mensajes de los que diecinueve no requieren nada, el día que llega el importante nadie lo lee. Por eso este tema dedica tanto espacio a cuándo no avisar como a qué comprobar.

Para servidores grandes o muchos servidores existen sistemas dedicados, como Prometheus, Zabbix o Nagios, que guardan historia, dibujan gráficos y coordinan equipos. Pero un script de Bash ejecutado cada cinco minutos cubre muy bien un servidor o unos pocos, y además ayuda a entender qué hacen esos sistemas: en el fondo, ejecutan controles como los de este tema.

2. Qué vigilar

QuéPor quéCómo
El espacio en disco, y los inodosUn disco lleno detiene bases de datos, registros y casi todo lo demás.df
Los serviciosUn servicio detenido es una función que no está.systemctl is-active
Los sitios webLo que ven los usuarios: que responda, sin errores y a tiempo.curl
Los certificados TLSUno vencido deja el sitio inaccesible, y vencen en una fecha conocida.openssl
Los respaldosQue se sigan haciendo, como en el tema 38.La fecha de la última copia.
La memoria y la cargaSeñales de que algo se descontroló./proc/meminfo, /proc/loadavg
Las tareas programadasQue no hayan fallado en silencio.systemctl --failed

Hay una distinción útil entre síntomas y causas. «El sitio no responde» es un síntoma: lo que el usuario sufre. «El disco está al 95 %» es una causa posible de muchos síntomas futuros. Conviene vigilar los dos: los síntomas, porque son lo que importa; las causas, porque permiten actuar antes de que haya síntomas.

3. Un control, cuatro respuestas

Cada comprobación será un control: una función que mira una cosa y responde con un estado y una línea de explicación. Hay una convención muy difundida, nacida con Nagios y adoptada por muchas otras herramientas, que usa cuatro estados, cada uno con un código de salida:

CódigoEstadoSignifica
0OKTodo en orden.
1AVISONo es grave todavía, pero hay que mirarlo pronto.
2CRÍTICOHay un problema que requiere atención ahora.
3DESCONOCIDONo se pudo comprobar: el control mismo falló.
la forma de un control
OK=0 AVISO=1 CRITICO=2 DESCONOCIDO=3

control_ejemplo() {
  …                                    # mirar algo
  echo "lo que se encontró, en una línea"
  return $AVISO                        # o el estado que corresponda
}

estado=$OK
mensaje=$(control_ejemplo) || estado=$?

Separar el estado del mensaje permite que el resto del script decida qué hacer con cualquier control sin saber qué comprueba. El estado se captura con || estado=$?, la técnica del tema 10, porque con set -e un control que devuelve 2 detendría el script. Y el estado DESCONOCIDO merece su lugar: que no se pueda consultar el disco no es lo mismo que el disco esté lleno, y quien recibe el aviso tiene que saber cuál de las dos cosas pasó.

4. El disco

df informa el uso de cada sistema de archivos. Con --output se eligen las columnas, lo que lo hace fácil de usar en un script:

terminal
$ df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs
Mounted on  Size  Used Avail Use%
/            40G   31G  7.1G  82%
/boot       974M  241M  666M  27%
/var        100G   94G  6.2G  94%
$ df --output=pcent /var | tail -n 1 | tr -dc '0-9'
94
$ df --output=ipcent /var | tail -n 1 | tr -dc '0-9'
12

La última línea mira los inodos, las fichas que el sistema de archivos usa para cada archivo. Hay una cantidad fija, y un directorio con millones de archivos diminutos, como una caché o una cola de correo descontroladas, puede agotarlos con el disco medio vacío. El síntoma es desconcertante: No space left on device con espacio libre de sobra. El control mira los dos y se queda con el peor.

Los dos umbrales, por ejemplo 80 % para el aviso y 90 % para el crítico, dan tiempo de reacción: el aviso llega días antes del problema, cuando todavía se puede limpiar con calma. Cuando llega, la pregunta es qué ocupa tanto, y la responde du:

terminal · qué está llenando /var
$ sudo du -xh --max-depth=2 /var | sort -h | tail -n 4
3.1G	/var/lib/docker
8.7G	/var/lib/postgresql
79G	/var/log/tienda
94G	/var

5. Los servicios

Para los servicios de systemd, systemctl is-active responde exactamente lo que hace falta, con su código de salida y una palabra:

terminal
$ systemctl is-active nginx postgresql tienda
active
active
failed
$ systemctl is-active --quiet tienda || echo "tienda no está activo"
tienda no está activo
$ systemctl --failed --no-legend
● tienda.service         loaded failed failed Tienda en línea
● respaldo.service       loaded failed failed Respaldo nocturno de /home

Con --quiet no escribe nada y solo devuelve el código, la forma natural en una condición. systemctl --failed lista todas las unidades que fallaron, incluidas las tareas de los temporizadores del tema 35, y es un control muy útil por sí mismo. Para programas que no son servicios de systemd, pgrep -x nombre comprueba que haya un proceso con ese nombre.

Que un servicio esté activo, sin embargo, no garantiza que funcione: un servidor web puede estar en marcha y responder con errores. Por eso, además de mirar el servicio, se mira lo que ofrece.

6. Los sitios web

curl puede informar, además del contenido, datos sobre la respuesta, con la opción -w: el código HTTP, el tiempo total, el tiempo hasta el primer byte y muchos más. Para un control, interesan el código y el tiempo:

terminal
$ curl -s -o /dev/null --max-time 10 -w '%{http_code} %{time_total}\n' https://tienda.example.com/salud
200 0.184213
$ curl -s -o /dev/null --max-time 10 -w '%{http_code}\n' https://tienda.example.com/no-existe
404
$ curl -s -o /dev/null --max-time 10 https://caido.example.com/; echo "código de curl: $?"
código de curl: 7

Hay dos tipos de fallo, y conviene distinguirlos. Si el servidor responde con un error, como 500 o 503, curl termina bien y el código HTTP lo dice. Si no hay respuesta, curl termina con un código propio, que explica el motivo:

Código de curlSignifica
6No se pudo resolver el nombre: un problema de DNS.
7No se pudo conectar: el servidor rechaza la conexión o no está.
28Se agotó el tiempo de --max-time.
35, 60Error en la negociación TLS, o certificado no válido.

Para el tiempo hay un detalle: 0.184213 tiene decimales, y la aritmética de Bash, como vimos en el tema 7, es solo entera. La comparación con el límite la hace awk, cuyo código de salida se usa como condición: awk -v t="$tiempo" -v max=2 'BEGIN { exit !(t > max) }' termina con 0 si el tiempo supera el máximo. Un control más estricto puede comprobar también que la página contenga lo esperado, por ejemplo con curl -s URL | grep -q 'Bienvenido', lo que detecta una página que responde 200 pero vacía o con un mensaje de error.

7. Los certificados

Los certificados TLS de los sitios tienen fecha de vencimiento, y cuando vencen, los navegadores bloquean el acceso. Los que se renuevan automáticamente, como los de Let's Encrypt, también conviene vigilarlos, porque la renovación puede fallar en silencio. openssl se conecta al servidor y lee el certificado:

terminal · cuándo vence un certificado
$ openssl s_client -connect tienda.example.com:443 -servername tienda.example.com < /dev/null 2> /dev/null \
    | openssl x509 -noout -enddate
notAfter=Dec 25 23:59:59 2026 GMT
$ echo $(( ( $(date -d 'Dec 25 23:59:59 2026 GMT' +%s) - $(date +%s) ) / 86400 ))
71

s_client abre la conexión; el < /dev/null hace que la cierre enseguida, en lugar de esperar algo que enviar; -servername indica el nombre del sitio, necesario cuando un mismo servidor atiende varios. El segundo openssl extrae la fecha de vencimiento, y date -d la convierte en segundos para calcular los días que faltan. Con umbrales de 21 días para el aviso y 7 para el crítico, queda tiempo de sobra para renovarlo a mano si la renovación automática falló.

8. La memoria y la carga

El núcleo publica el estado de la memoria y de la carga en archivos de /proc, que se leen como cualquier otro:

memoria disponible y carga
# La memoria disponible para programas nuevos, en porcentaje
awk '/^MemTotal:/ { t = $2 } /^MemAvailable:/ { a = $2 } END { printf "%d\n", a * 100 / t }' /proc/meminfo

# La carga media de los últimos 5 minutos, comparada con la cantidad de procesadores
read -r _ carga5 _ < /proc/loadavg
awk -v c="$carga5" -v n="$(nproc)" 'BEGIN { exit !(c > 2 * n) }' && echo "carga alta: $carga5"

Hay que mirar MemAvailable y no MemFree: Linux usa la memoria libre como caché de disco, de modo que MemFree siempre parece bajo en un sistema sano. La carga es la cantidad media de procesos que están ejecutándose o esperando para hacerlo; solo dice algo comparada con la cantidad de procesadores, y una carga alta durante unos minutos es normal. Por eso estos controles dan muchas falsas alarmas si los umbrales son ajustados, y se usan con cuidado. El script de la sección 12 no los incluye, pero agregar un control es agregar una función, como propone uno de los ejercicios.

9. Avisar una vez, no cada cinco minutos

Si el monitoreo corre cada cinco minutos y el disco está al 92 %, un script ingenuo envía un aviso cada cinco minutos: 288 al día, por el mismo problema. La solución es que el script recuerde el estado de cada control entre una ejecución y otra, y avise solo cuando cambia:

Una línea de tiempo con las ejecuciones del monitoreo cada cinco minutos. El control pasa de OK a CRÍTICO: se envía un aviso. Sigue en CRÍTICO durante varias ejecuciones: no se avisa. A las seis horas, si sigue igual, se envía un recordatorio. Cuando vuelve a OK, se envía un aviso de que se resolvió 10:00 10:05 10:10 10:15 … 16:05 16:10 OK CRÍTICO CRÍTICO CRÍTICO CRÍTICO OK aviso cambió silencio recordatorio a las 6 horas resuelto
Solo tres mensajes en seis horas: cuando aparece el problema, un recordatorio si sigue, y cuando se resuelve.

El estado se guarda en un archivo pequeño por control, en ~/.local/state/vigilar/, con tres números: el estado, desde cuándo y cuándo se avisó por última vez. Hay un problema relacionado, el de los controles que oscilan: un sitio que falla un minuto y se recupera al siguiente produce un aviso y un «resuelto» por cada oscilación. La defensa habitual es exigir que el fallo se repita en dos ejecuciones seguidas antes de avisar, que queda como ejercicio.

10. Los canales de aviso

El aviso tiene que llegar a donde la persona lo vea. Para no atar el script de monitoreo a un canal, conviene separarlos: el script decide cuándo avisar, y un programa aparte, el avisador, decide cómo. La interfaz es mínima: el asunto como argumento, el detalle por la entrada estándar. Cambiar de canal es cambiar de avisador.

Por correo. Si el equipo tiene un servicio de correo configurado, o un cliente como msmtp que envía a través de un servidor externo, el avisador es una línea, porque mail ya recibe el cuerpo por la entrada:

avisar-correo
#!/usr/bin/env bash
# avisar-correo ASUNTO < DETALLE
exec mail -s "$1" admin@example.com

Por mensaje. Los servicios de chat, como Slack, Mattermost o Discord, ofrecen webhooks: una dirección secreta a la que se envía un mensaje con un POST en formato JSON. Servicios como ntfy o los bots de Telegram funcionan de forma parecida, y permiten recibir los avisos en el teléfono. El único cuidado es armar el JSON correctamente: el texto va entre comillas, y las comillas, las barras invertidas y los saltos de línea que contenga tienen que escaparse, o el mensaje se rompe. Si está instalado, jq -n --arg t "$texto" '{text: $t}' lo hace sin errores; si no, una función con las expansiones del tema 6:

avisar-webhook
#!/usr/bin/env bash
#
# avisar-webhook ASUNTO < DETALLE — envía un aviso a un canal de chat por webhook.
# Slack, Mattermost y Rocket.Chat aceptan {"text": "..."}.
# La URL del webhook, que es secreta, se lee de un archivo.
#
set -euo pipefail

archivo_url="${AVISOS_WEBHOOK:-$HOME/.config/vigilar/webhook}"
[[ -r "$archivo_url" ]] || { echo "${0##*/}: falta el archivo con la URL: $archivo_url" >&2; exit 1; }
url=$(< "$archivo_url")

json() {                               # un texto, como cadena de JSON
  local s="$1"
  s="${s//'\'/'\\'}"
  s="${s//'"'/'\"'}"
  s="${s//$'\n'/'\n'}"
  s="${s//$'\t'/'\t'}"
  s="${s//$'\r'/}"
  printf '"%s"' "$s"
}

texto="*$1*"$'\n'"$(cat)"
curl -fsS --max-time 10 -H 'Content-Type: application/json' \
     --data-binary "{\"text\": $(json "$texto")}" -- "$url" > /dev/null

La barra invertida se escapa primero, porque si no, las barras que agregan los reemplazos siguientes se escaparían otra vez. La URL del webhook es un secreto, porque cualquiera que la tenga puede escribir en el canal: por eso se guarda en un archivo con permisos 600, como la configuración del tema 30, y no en el script, en el crontab ni en la línea de comandos, donde la vería cualquier usuario con ps.

11. Quién vigila al vigilante

Un monitoreo que se detiene es el peor fallo posible, porque parece silencio. Si el servidor entero se apaga, el script de monitoreo que corría en él no avisa nada; si la tarea programada deja de ejecutarse, tampoco. Hay dos defensas, que se complementan:

12. Un script completo

vigilar lee una lista de controles de un archivo de configuración, ejecuta cada uno, y avisa según los cambios de estado de la sección 9.

/etc/vigilar/controles
# Tipo         Qué                                  Umbrales
disco          /                                    80 90
disco          /var                                 80 90
servicio       nginx
servicio       postgresql
servicio       tienda
web            https://tienda.example.com/salud     2
certificado    tienda.example.com                   21 7
respaldo       /mnt/copias                          1
vigilar
#!/usr/bin/env bash
#
# vigilar — ejecuta los controles de un archivo de configuración y avisa
# cuando alguno cambia de estado: cuando aparece un problema y cuando se resuelve.
#
# Uso: vigilar [-v] [-a AVISADOR] [-r HORAS] [-l URL] CONFIGURACIÓN
#   -v  muestra el resultado de cada control
#   -a  programa que envía cada aviso: recibe el asunto como argumento y el
#       detalle por la entrada estándar (sin -a, los avisos van a la salida)
#   -r  repetir el aviso de un problema sin resolver cada HORAS (6; 0, nunca)
#   -l  URL de latido que se consulta al terminar, para un vigilante externo
#
# Cada línea de la configuración es un control:
#   disco        PUNTO_DE_MONTAJE  AVISO%  CRÍTICO%
#   servicio     NOMBRE
#   web          URL  SEGUNDOS_PARA_AVISO
#   certificado  SERVIDOR[:PUERTO]  DÍAS_AVISO  DÍAS_CRÍTICO
#   respaldo     DIRECTORIO  DÍAS_MÁXIMOS
#
set -euo pipefail
export LC_ALL=C.UTF-8

prog="${0##*/}"
OK=0 AVISO=1 CRITICO=2 DESCONOCIDO=3
nombres=(OK AVISO CRÍTICO DESCONOCIDO)
declare -A cantidad=([disco]=3 [servicio]=1 [web]=2 [certificado]=3 [respaldo]=2)
detallado=0 avisador="" repetir=6 latido=""

uso() { echo "Uso: $prog [-v] [-a AVISADOR] [-r HORAS] [-l URL] CONFIGURACIÓN" >&2; exit 4; }
error() { echo "$prog: $*" >&2; exit 4; }

# --- Los controles: escriben una línea y devuelven OK, AVISO, CRITICO o DESCONOCIDO ---

control_disco() {                      # PUNTO AVISO CRÍTICO
  local espacio inodos mayor
  espacio=$(df --output=pcent -- "$1" 2> /dev/null | tail -n 1 | tr -dc '0-9')
  [[ -n "$espacio" ]] || { echo "no se pudo consultar $1"; return $DESCONOCIDO; }
  inodos=$(df --output=ipcent -- "$1" | tail -n 1 | tr -dc '0-9')
  inodos="${inodos:-0}"                # algunos sistemas de archivos no informan inodos
  mayor=$(( espacio > inodos ? espacio : inodos ))
  echo "$1 al $espacio % de espacio y al $inodos % de inodos"
  (( mayor >= $3 )) && return $CRITICO
  (( mayor >= $2 )) && return $AVISO
  return $OK
}

control_servicio() {                   # NOMBRE
  if systemctl is-active --quiet -- "$1"; then
    echo "$1 activo"
    return $OK
  fi
  echo "$1 está $(systemctl is-active -- "$1" 2> /dev/null || true)"
  return $CRITICO
}

control_web() {                        # URL SEGUNDOS
  local salida codigo tiempo e=0 motivo
  salida=$(curl -s -o /dev/null --max-time 10 -w '%{http_code} %{time_total}' -- "$1") || e=$?
  if (( e )); then
    case "$e" in
      6)     motivo="no se resuelve el nombre" ;;
      7)     motivo="conexión rechazada" ;;
      28)    motivo="sin respuesta en 10 s" ;;
      35|60) motivo="error de TLS o de certificado" ;;
      *)     motivo="error $e de curl" ;;
    esac
    echo "$1: $motivo"
    return $CRITICO
  fi
  read -r codigo tiempo <<< "$salida"
  if (( codigo >= 400 )); then
    echo "$1 responde $codigo"
    return $CRITICO
  fi
  echo "$1 responde $codigo en $tiempo s"
  awk -v t="$tiempo" -v max="$2" 'BEGIN { exit !(t > max) }' && return $AVISO
  return $OK
}

control_certificado() {                # SERVIDOR[:PUERTO] DÍAS_AVISO DÍAS_CRÍTICO
  local servidor="${1%:*}" puerto=443 fin dias
  [[ "$1" == *:* ]] && puerto="${1##*:}"
  if ! fin=$(openssl s_client -connect "$servidor:$puerto" -servername "$servidor" < /dev/null 2> /dev/null |
             openssl x509 -noout -enddate 2> /dev/null); then
    echo "$1: no se pudo leer el certificado"
    return $CRITICO
  fi
  fin="${fin#notAfter=}"
  dias=$(( ( $(date -d "$fin" +%s) - $(date +%s) ) / 86400 ))
  echo "el certificado de $servidor vence en $dias días, el $(date -d "$fin" +%F)"
  (( dias <= $3 )) && return $CRITICO
  (( dias <= $2 )) && return $AVISO
  return $OK
}

control_respaldo() {                   # DIRECTORIO DÍAS
  local ultima dias
  ultima=$(find "$1" -mindepth 1 -maxdepth 1 -type d \
                -name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' -printf '%f\n' 2> /dev/null |
           sort | tail -n 1)
  [[ -n "$ultima" ]] || { echo "no hay respaldos en $1"; return $CRITICO; }
  dias=$(( ( $(date +%s) - $(date -d "$ultima" +%s) ) / 86400 ))
  echo "el último respaldo en $1 es del $ultima, hace $dias días"
  (( dias > $2 )) && return $CRITICO
  return $OK
}

# --- Los avisos ---

duracion() {                           # segundos → «3 h 20 min»
  local m=$(( $1 / 60 ))
  if (( m < 60 )); then echo "$m min"; else echo "$(( m / 60 )) h $(( m % 60 )) min"; fi
}

avisar() {                             # ASUNTO DETALLE
  local asunto="[$(hostname -s)] $1"
  if [[ -n "$avisador" ]]; then
    printf '%s\n' "$2" | "$avisador" "$asunto" || echo "$prog: no se pudo enviar el aviso: $asunto" >&2
  else
    printf '%s\n    %s\n' "$asunto" "$2"
  fi
}

# --- Programa principal ---

while getopts ":va:r:l:" opcion; do
  case "$opcion" in
    v) detallado=1 ;;
    a) avisador="$OPTARG" ;;
    r) repetir="$OPTARG" ;;
    l) latido="$OPTARG" ;;
    *) uso ;;
  esac
done
shift $(( OPTIND - 1 ))
(( $# == 1 )) || uso
config="$1"
[[ -r "$config" ]] || error "no se puede leer: $config"
[[ "$repetir" =~ ^[0-9]+$ ]] || uso
[[ -z "$avisador" ]] || command -v -- "$avisador" > /dev/null || error "no se encuentra el avisador: $avisador"

# Primero se lee y valida toda la configuración, para no ejecutar la mitad
controles=()
n=0
while IFS= read -r linea || [[ -n "$linea" ]]; do
  n=$(( n + 1 ))
  [[ "$linea" =~ ^[[:space:]]*(#|$) ]] && continue
  read -ra campos <<< "$linea"
  tipo="${campos[0]}"
  [[ -n "${cantidad[$tipo]:-}" ]] || error "$config:$n: tipo de control desconocido: $tipo"
  (( ${#campos[@]} - 1 == cantidad[$tipo] )) ||
    error "$config:$n: el control $tipo necesita ${cantidad[$tipo]} datos"
  controles+=("${campos[*]}")
done < "$config"

dir_estado="${XDG_STATE_HOME:-$HOME/.local/state}/vigilar"
mkdir -p "$dir_estado"
peor=$OK

for control in "${controles[@]}"; do
  read -ra campos <<< "$control"
  estado=$OK
  mensaje=$("control_${campos[0]}" "${campos[@]:1}") || estado=$?
  (( estado > peor )) && peor=$estado
  (( detallado )) && printf '%-12ls %s\n' "${nombres[estado]}" "$mensaje"   # %ls: ancho en caracteres

  # El estado anterior: «estado desde último_aviso», o nada si estaba bien
  archivo="$dir_estado/$(printf '%s' "$control" | tr -c 'A-Za-z0-9._-' '_')"
  anterior=$OK desde=0 avisado=0
  [[ -f "$archivo" ]] && read -r anterior desde avisado < "$archivo"
  ahora=$(date +%s)

  if (( estado != anterior )); then
    if (( estado == OK )); then
      avisar "RESUELTO: $control" "$mensaje; estuvo en ${nombres[anterior]} $(duracion $(( ahora - desde )))"
      rm -f -- "$archivo"
    else
      avisar "${nombres[estado]}: $control" "$mensaje"
      echo "$estado $ahora $ahora" > "$archivo"
    fi
  elif (( estado != OK && repetir > 0 && ahora - avisado >= repetir * 3600 )); then
    avisar "SIGUE EN ${nombres[estado]}: $control" "$mensaje; desde hace $(duracion $(( ahora - desde )))"
    echo "$estado $desde $ahora" > "$archivo"
  fi
done

# El latido: si deja de llegar, el vigilante externo avisa
if [[ -n "$latido" ]]; then
  curl -fsS --max-time 10 --retry 3 -o /dev/null -- "$latido" || echo "$prog: no se pudo enviar el latido" >&2
fi
exit "$peor"
terminal
$ vigilar -v /etc/vigilar/controles
OK           / al 61 % de espacio y al 9 % de inodos
CRÍTICO      /var al 94 % de espacio y al 12 % de inodos
[web1] CRÍTICO: disco /var 80 90
    /var al 94 % de espacio y al 12 % de inodos
OK           nginx activo
OK           postgresql activo
OK           tienda activo
OK           https://tienda.example.com/salud responde 200 en 0.184213 s
AVISO        el certificado de tienda.example.com vence en 18 días, el 2026-11-02
[web1] AVISO: certificado tienda.example.com 21 7
    el certificado de tienda.example.com vence en 18 días, el 2026-11-02
OK           el último respaldo en /mnt/copias es del 2026-10-15, hace 0 días
$ vigilar /etc/vigilar/controles               # cinco minutos después: nada cambió, nada que avisar
$ echo $?
2
# … se limpian los registros viejos de /var/log/tienda:
$ vigilar /etc/vigilar/controles
[web1] RESUELTO: disco /var 80 90
    /var al 41 % de espacio y al 12 % de inodos; estuvo en CRÍTICO 1 h 35 min
$ vigilar /etc/vigilar/controles-mal
vigilar: /etc/vigilar/controles-mal:4: el control web necesita 2 datos

Para ejecutarlo cada cinco minutos con un temporizador de systemd, como en el tema 35, hay un detalle: systemd considera fallido un servicio que termina con un código distinto de cero, y el código 2 de un disco lleno marcaría el servicio de monitoreo como fallido en cada ejecución. La directiva SuccessExitStatus=1 2 3 en el servicio le indica que esos códigos son resultados normales, de modo que solo el 4, un error del script, cuenta como fallo. Con cron no hace falta, porque cron ignora los códigos de salida.

/etc/systemd/system/vigilar.service y vigilar.timer
[Service]
Type=oneshot
ExecStart=/usr/local/bin/vigilar -a /usr/local/bin/avisar-webhook -l https://hc-ping.com/… /etc/vigilar/controles
SuccessExitStatus=1 2 3

[Timer]
OnCalendar=*:0/5
[Install]
WantedBy=timers.target

13. Errores frecuentes

EscribisteQué pasaCorrección
Avisar cada vez que un control fallaCientos de mensajes por el mismo problema; se aprende a ignorarlos.Avisar solo los cambios de estado, con recordatorios espaciados.
No avisar cuando el problema se resuelveQuien recibió la alarma no sabe si todavía tiene que actuar.Un aviso de «resuelto», con la duración.
Un solo umbral, en el límiteEl aviso llega cuando ya no hay margen.Dos umbrales: aviso con tiempo, crítico cuando urge.
Vigilar solo el espacio del discoUn disco sin inodos falla con espacio libre.Mirar también df --output=ipcent.
(( tiempo > 2 )) con 0.18Bash no admite decimales: error de sintaxis.Comparar con awk.
curl sin --max-timeUn sitio colgado cuelga también el monitoreo.Siempre con un límite de tiempo.
Mirar MemFreeSiempre parece bajo, por la caché: falsas alarmas.MemAvailable.
La URL del webhook en el script o en el crontabCualquiera que la vea puede escribir en el canal.Un archivo con permisos 600.
Armar el JSON con el texto sin escaparUnas comillas en el mensaje rompen el aviso.Escapar, o jq --arg.
Monitorear solo desde el propio servidorSi el servidor se cae, no avisa nadie.Un latido a un servicio externo, y controles desde otra máquina.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué un monitoreo que avisa demasiado es peligroso?

Porque enseña a ignorar sus avisos. Si la mayoría no requiere nada, el aviso importante se pierde entre los demás o se lee tarde.

2. ¿Qué diferencia hay entre los estados CRÍTICO y DESCONOCIDO?

CRÍTICO indica que se comprobó y hay un problema. DESCONOCIDO, que no se pudo comprobar: el control mismo falló. Quien recibe el aviso tiene que saber cuál de las dos cosas pasó, porque se resuelven de forma distinta.

3. ¿Cómo puede un disco dar «No space left on device» con espacio libre?

Si se agotaron los inodos, por ejemplo por millones de archivos diminutos. No se pueden crear archivos nuevos aunque quede espacio. Se ve con df -i o df --output=ipcent.

4. ¿Por qué el control de un sitio web distingue el código de salida de curl del código HTTP?

Porque son fallos distintos: si el servidor responde con un error, como 503, curl termina bien y el código HTTP lo indica; si no hay respuesta, curl termina con su propio código, que dice si fue el DNS, la conexión, el tiempo o el TLS.

5. ¿Por qué el tiempo de respuesta se compara con awk y no con (( ))?

Porque el tiempo tiene decimales, como 0.184213, y la aritmética de Bash solo trabaja con enteros.

6. ¿Para qué sirve < /dev/null en openssl s_client?

Para que s_client reciba enseguida un fin de archivo y cierre la conexión después de mostrar el certificado, en lugar de quedarse esperando datos para enviar.

7. ¿Por qué se mira MemAvailable y no MemFree?

Porque Linux usa la memoria que no necesitan los programas como caché de disco, y MemFree siempre parece bajo. MemAvailable estima la memoria que realmente podrían usar programas nuevos.

8. ¿En qué momentos avisa vigilar sobre un control?

Cuando cambia a un estado distinto del anterior, incluido el regreso a OK, que se informa como «resuelto»; y, si el problema sigue, cada tantas horas como indique -r. Mientras el estado no cambia, no avisa.

9. ¿Qué es un latido, y qué detecta que los controles no pueden detectar?

Una consulta a un servicio externo al final de cada ejecución. Si deja de llegar, ese servicio avisa. Detecta que el propio monitoreo dejó de funcionar: el servidor se apagó, la tarea dejó de ejecutarse o el script falla antes de terminar.

10. ¿Por qué el servicio de systemd de vigilar lleva SuccessExitStatus=1 2 3?

Porque vigilar termina con el peor estado de sus controles, y un problema vigilado no es un fallo del monitoreo. Sin esa directiva, systemd marcaría el servicio como fallido cada vez que un control no esté en OK; con ella, solo el código 4, un error del propio script.

Para practicar

  1. Escribe una configuración para tu equipo con un control de cada tipo, y ejecuta vigilar -v. Cambia los umbrales del disco para provocar un aviso y luego un «resuelto».
  2. Comprueba con curl -w los tiempos de varios sitios, y averigua qué código de salida da curl con un nombre inexistente, con un puerto cerrado y con --max-time 0.001.
  3. Agrega un control memoria PORCENTAJE_AVISO PORCENTAJE_CRÍTICO, que mire el porcentaje disponible según MemAvailable.
  4. Agrega un control fallidos, sin datos, que se ponga en CRÍTICO si systemctl --failed lista alguna unidad, y la nombre en el mensaje.
  5. Haz que vigilar solo avise de un problema cuando se repite en dos ejecuciones seguidas, guardando en el archivo de estado un contador de fallos.
  6. Crea una cuenta en un servicio de latidos, o simula uno con un temporizador en otra máquina, y comprueba qué pasa cuando detienes el temporizador de vigilar.