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 inodos | Un disco lleno detiene bases de datos, registros y casi todo lo demás. | df |
| Los servicios | Un servicio detenido es una función que no está. | systemctl is-active |
| Los sitios web | Lo que ven los usuarios: que responda, sin errores y a tiempo. | curl |
| Los certificados TLS | Uno vencido deja el sitio inaccesible, y vencen en una fecha conocida. | openssl |
| Los respaldos | Que se sigan haciendo, como en el tema 38. | La fecha de la última copia. |
| La memoria y la carga | Señales de que algo se descontroló. | /proc/meminfo, /proc/loadavg |
| Las tareas programadas | Que 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ódigo | Estado | Significa |
|---|---|---|
| 0 | OK | Todo en orden. |
| 1 | AVISO | No es grave todavía, pero hay que mirarlo pronto. |
| 2 | CRÍTICO | Hay un problema que requiere atención ahora. |
| 3 | DESCONOCIDO | No se pudo comprobar: el control mismo falló. |
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:
$ 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:
$ 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:
$ 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:
$ 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 curl | Significa |
|---|---|
| 6 | No se pudo resolver el nombre: un problema de DNS. |
| 7 | No se pudo conectar: el servidor rechaza la conexión o no está. |
| 28 | Se agotó el tiempo de --max-time. |
| 35, 60 | Error 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:
$ 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:
# 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:
- Al cambiar a un estado peor, se avisa: es la noticia.
- Mientras el estado se mantiene, silencio. La persona ya lo sabe.
- Si el problema sigue sin resolverse mucho tiempo, un recordatorio cada tantas horas, por si el primer aviso se perdió o quedó olvidado.
- Al volver a OK, se avisa también, con cuánto duró el problema. Sin este aviso, quien recibió la alarma no sabe si todavía tiene que hacer algo.
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:
#!/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:
#!/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:
- Vigilar desde afuera. Los controles de los síntomas, como que el sitio responda o que el certificado esté vigente, funcionan mejor desde otra máquina, que es además el punto de vista de los usuarios. Basta con ejecutar el mismo script, con otra configuración, en un segundo equipo.
- Un latido. Al terminar cada ejecución, el script consulta una dirección de un servicio externo, como Healthchecks.io o la función equivalente de muchos sistemas de monitoreo. Ese servicio espera un latido cada cinco minutos, y si deja de recibirlo, es él quien avisa. Se lo llama «interruptor de hombre muerto» (dead man's switch): avisa por la ausencia de una señal, no por su presencia. Detecta que el servidor se cayó, que la tarea dejó de correr y que el script falla antes de terminar.
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.
# 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
#!/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"
$ 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
- La configuración se valida entera antes de empezar: un tipo desconocido o una cantidad equivocada de datos se informan con el número de línea, y no se ejecuta ningún control. El array asociativo
cantidadsirve a la vez como lista de tipos válidos y como regla de validación, y un tipo solo se acepta si está en él: así, el nombre de la funcióncontrol_TIPOnunca viene de un dato sin comprobar. - Agregar un control es agregar una función
control_NOMBREy su entrada encantidad. El resto del script no cambia. - Avisa solo los cambios: el estado de cada control se guarda en un archivo cuyo nombre sale de la propia línea de configuración, con los caracteres raros reemplazados por guiones bajos. Cuando un control vuelve a OK, su archivo se borra, y el aviso de «resuelto» informa cuánto duró el problema.
- El avisador es intercambiable: sin
-a, los avisos se escriben en la salida estándar, que es lo que cron envía por correo, de modo que con cron yMAILTOya funciona sin nada más. Con-a avisar-webhook, van a un canal de chat. Si el avisador falla, el script lo informa y sigue con los demás controles. - El código de salida es el peor estado, de 0 a 3, como un control más, y 4 para los errores del propio script o de la configuración. Así, otro script puede usar
vigilarcomo condición. - El latido va al final, después de todos los controles: si el script se cuelga o falla antes, el latido no llega y el vigilante externo avisa.
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.
[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
| Escribiste | Qué pasa | Corrección |
|---|---|---|
| Avisar cada vez que un control falla | Cientos 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 resuelve | Quien 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ímite | El aviso llega cuando ya no hay margen. | Dos umbrales: aviso con tiempo, crítico cuando urge. |
| Vigilar solo el espacio del disco | Un disco sin inodos falla con espacio libre. | Mirar también df --output=ipcent. |
(( tiempo > 2 )) con 0.18 | Bash no admite decimales: error de sintaxis. | Comparar con awk. |
curl sin --max-time | Un sitio colgado cuelga también el monitoreo. | Siempre con un límite de tiempo. |
Mirar MemFree | Siempre parece bajo, por la caché: falsas alarmas. | MemAvailable. |
| La URL del webhook en el script o en el crontab | Cualquiera que la vea puede escribir en el canal. | Un archivo con permisos 600. |
| Armar el JSON con el texto sin escapar | Unas comillas en el mensaje rompen el aviso. | Escapar, o jq --arg. |
| Monitorear solo desde el propio servidor | Si el servidor se cae, no avisa nadie. | Un latido a un servicio externo, y controles desde otra máquina. |
14. Resumen
- Monitorear es comprobar el estado periódicamente; alertar, avisar a una persona solo cuando hace falta. Un monitoreo que avisa de más es tan malo como uno que no avisa.
- Cada control devuelve OK, AVISO, CRÍTICO o DESCONOCIDO, con los códigos 0 a 3, y una línea de explicación.
df --output=pcenteipcentdan el uso del espacio y de los inodos; dos umbrales dan tiempo de reacción;duencuentra qué ocupa.systemctl is-active --quietcomprueba un servicio;systemctl --failedlista lo que falló.curl -w '%{http_code} %{time_total}'informa el código y el tiempo; su código de salida explica por qué no hubo respuesta.openssl s_clientyopenssl x509 -enddatedan el vencimiento de un certificado.- La memoria se mide con
MemAvailable, y la carga, comparada con la cantidad de procesadores. - Se avisa al cambiar de estado, con recordatorios espaciados si el problema sigue y un aviso cuando se resuelve; el estado se guarda entre ejecuciones.
- El avisador, separado del monitoreo, recibe el asunto y el detalle; puede enviar un correo o un mensaje por webhook, con el JSON bien escapado y la URL guardada como secreto.
- Un latido a un servicio externo y controles desde otra máquina detectan que el propio monitoreo dejó de funcionar.
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
- 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». - Comprueba con
curl -wlos tiempos de varios sitios, y averigua qué código de salida dacurlcon un nombre inexistente, con un puerto cerrado y con--max-time 0.001. - Agrega un control
memoria PORCENTAJE_AVISO PORCENTAJE_CRÍTICO, que mire el porcentaje disponible segúnMemAvailable. - Agrega un control
fallidos, sin datos, que se ponga en CRÍTICO sisystemctl --failedlista alguna unidad, y la nombre en el mensaje. - Haz que
vigilarsolo avise de un problema cuando se repite en dos ejecuciones seguidas, guardando en el archivo de estado un contador de fallos. - 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.