Como te comenté en el capítulo anterior, después de generar reportes automáticos toca asegurarse de que lleguen a quien los necesita. Tus scripts trabajan en segundo plano. Hacen backups de madrugada, verifican healthchecks cada minuto, despliegan código a las 3 AM. Cuando algo falla — o incluso cuando todo sale bien — necesitas saberlo sin tener que mirar manualmente.
Las notificaciones desde Bash transforman scripts mudos en sistemas alertantes:
- Backup nocturno fallido → notificación urgente por Telegram + email
- Healthcheck de servicio caído → alerta crítica con sonido
- Cron diario exitoso → notificación informativa por email
Bash no tiene una librería de notificaciones, pero puede hablar con cualquier servicio que tenga una API HTTP, un cliente CLI o un protocolo de red.
¿Por qué notificar desde Bash?
Tus scripts trabajan en segundo plano. Hacen backups de madrugada, verifican healthchecks cada minuto, despliegan código a las 3 AM, rotan logs al mediodía. Cuando algo falla — o incluso cuando todo sale bien — necesitas saberlo sin tener que mirar manualmente.
Las notificaciones desde Bash transforman scripts mudos en sistemas alertantes:
- Backup nocturno fallido → notificación urgente por Telegram + email + desktop
- Healthcheck de servicio caído → alerta crítica con SMS + sonido + wall
- Cron diario exitoso → notificación informativa por email
- Despliegue completado → mensaje a Slack/Discord del equipo
Bash no tiene una librería de notificaciones, pero puede hablar con cualquier servicio que tenga una API HTTP, un cliente CLI o un protocolo de red. En este capítulo aprenderás a integrar todos los canales de notificación desde un solo script configurable:
| Canal | Herramienta | Ideal para |
|---|---|---|
| Desktop | notify-send | Usuario local en escritorio |
| Sonido | paplay, aplay | Alertas audibles |
| Sistema | wall, write, logger | Todos los usuarios del sistema |
mail, mutt, ssmtp | Notificaciones formales con adjuntos | |
| Telegram | curl + Bot API | Notificaciones móviles en tiempo real |
| Webhook | curl + JSON | Equipos en Slack/Discord/Mattermost |
| Push | curl + ntfy/Gotify | Push móvil auto-gestionado |
| SMS | curl + Twilio | Alertas críticas donde sea |
Niveles de notificación
Cuatro niveles estándar
Antes de escribir una sola función de envío, define los niveles que usarán todas las notificaciones:
NOTIFY_LEVELS=("INFO" "WARN" "ERROR" "CRITICAL")
| Nivel | Prioridad | ¿A quién? | Canales típicos |
|---|---|---|---|
| INFO | Baja | Solo log / email | Email, logger, desktop si verbose |
| WARN | Media | Operador | Email, Telegram, desktop |
| ERROR | Alta | Administrador | Telegram, webhook, push, SMS |
| CRITICAL | Máxima | Todos + guardia | SMS, sonido, wall, todos los canales |
Decisión de canal por nivel
# Ejemplo de lógica de enrutamiento
notify_dispatch() {
local level="$1"
local message="$2"
case "$level" in
INFO) notify_email "$message" ;;
WARN) notify_email "$message"
notify_telegram "$message" ;;
ERROR) notify_telegram "$message"
notify_webhook "$message"
notify_desktop "$message" ;;
CRITICAL) notify_telegram "$message"
notify_webhook "$message"
notify_sms "$message"
notify_sound "$message"
notify_wall "$message" ;;
esac
}
Thresholds configurables
# Solo notificar si el nivel es >= al threshold configurado
NOTIFY_THRESHOLD="WARN" # INFO, WARN, ERROR, CRITICAL
nivel_numerico() {
case "$1" in
INFO) return 0 ;;
WARN) return 1 ;;
ERROR) return 2 ;;
CRITICAL) return 3 ;;
*) return 0 ;;
esac
}
if nivel_numerico "$level" -ge nivel_numerico "$NOTIFY_THRESHOLD"; then
notify_dispatch "$level" "$message"
fi
Notificaciones desktop
notify-send — Urgencia, expiración, iconos
notify-send es el comando estándar de Linux para mostrar notificaciones en el escritorio. Forma parte de libnotify-bin.
# Instalación
sudo apt install libnotify-bin # Debian/Ubuntu
sudo dnf install libnotify # Fedora
sudo pacman -S libnotify # Arch
# Uso básico
notify-send "Título" "Mensaje de prueba"
# Con urgencia: low, normal, critical
notify-send -u critical "¡Alerta!" "El disco está al 95%"
# Con tiempo de expiración en milisegundos
notify-send -t 5000 "Notificación" "Desaparece en 5 segundos"
# Con icono personalizado
notify-send -i dialog-warning "Cuidado" "Backup no verificado"
notify-send -i /ruta/al/icono.png "Backup" "Completado"
# Reemplazar notificación anterior (same ID)
notify-send -p -r 1234 "Progreso" "50%" # -p imprime el ID
notify-send -r 1234 "Progreso" "100%"
Iconos comunes del sistema:
| Icono | Contexto |
|---|---|
dialog-information | Info general |
dialog-warning | Advertencia |
dialog-error | Error |
media-playback-stop | Detenido |
media-record | Grabando / en progreso |
emblem-important | Importante |
Sonidos del sistema
Las notificaciones visuales no sirven si no estás mirando la pantalla. Los sonidos te alertan aunque estés en otra aplicación.
# paplay — PulseAudio (Linux moderno)
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
paplay /usr/share/sounds/freedesktop/stereo/dialog-warning.oga
paplay /usr/share/sounds/freedesktop/stereo/dialog-error.oga
# aplay — ALSA (alternativa sin PulseAudio)
aplay /usr/share/sounds/alsa/Front_Center.wav
# speaker-test — Pitido genérico (no necesita archivo)
speaker-test -t sine -f 1000 -l 1 2>/dev/null # kHz, 1 segundo
speaker-test -t pink -l 1 2>/dev/null # Ruido rosa
# beep — Altavoz interno de la placa (necesita módulo pcspkr)
sudo modprobe pcspkr
beep -f 800 -l 200 -r 3 # Hz, 200ms, 3 veces
Sonidos del theme freedesktop:
SONIDOS=(
"/usr/share/sounds/freedesktop/stereo/complete.oga"
"/usr/share/sounds/freedesktop/stereo/dialog-warning.oga"
"/usr/share/sounds/freedesktop/stereo/dialog-error.oga"
"/usr/share/sounds/freedesktop/stereo/bell.oga"
"/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"
)
notify_sound() {
local level="$1"
case "$level" in
INFO) paplay "${SONIDOS[0]}" 2>/dev/null ;;
WARN) paplay "${SONIDOS[1]}" 2>/dev/null ;;
ERROR) paplay "${SONIDOS[2]}" 2>/dev/null ;;
CRITICAL) paplay "${SONIDOS[2]}" 2>/dev/null
paplay "${SONIDOS[4]}" 2>/dev/null ;;
esac
}
wall y write — Mensajes a todos los usuarios
wall (write all) envía un mensaje a todas las terminales de todos los usuarios del sistema. write envía a un usuario concreto.
# Mensaje a todos los usuarios
wall "⚠ El servidor se reiniciará en 5 minutos. Guarda tu trabajo."
# Con origen identificado
echo "Backup completado a las $(date)" | wall -n # -n suprime el banner
# Mensaje a un usuario específico
write lorenzo <<< "El backup de tu home ha finalizado"
# Solo si el usuario tiene mesg habilitado
if mesg y 2>/dev/null; then
write lorenzo <<< "Notificación urgente"
fi
⚠
wallrequiere permisos de root omesg yen sistemas modernos. En systemd, los mensajes viajan porlogindy pueden no aparecer en todos los terminales.
Email desde Bash
mail/mailx — El clásico
El comando mail (o mailx) es el estándar POSIX para enviar correos. Necesita un MTA (Mail Transfer Agent) instalado y configurado.
# Instalación
sudo apt install mailutils # Debian/Ubuntu (proporciona mail)
sudo apt install bsd-mailx # Alternativa BSD
sudo dnf install mailx # Fedora
# Envío básico
echo "Cuerpo del mensaje" | mail -s "Asunto del correo" usuario@ejemplo.com
# Con archivo como cuerpo
mail -s "Reporte de backup" admin@ejemplo.com < /tmp/reporte.txt
# Con destinatarios múltiples
echo "Alerta" | mail -s "⚠ Error crítico" admin1@ejemplo.com admin2@ejemplo.com
# Con remitente personalizado
echo "Cuerpo" | mail -s "Asunto" -a "From: backup-script@servidor.com" user@ejemplo.com
# CC y BCC (depende de la implementación)
echo "Cuerpo" | mail -s "Asunto" -c cc@ejemplo.com -b bcc@ejemplo.com user@ejemplo.com
mutt — Multipropósito con adjuntos
mutt es un cliente de correo completo que desde Bash brilla para enviar adjuntos y formatear HTML.
# Instalación
sudo apt install mutt
# Envío con adjunto
echo "Cuerpo del mensaje" | mutt -s "Backup diario" \
-a /backups/backup_20260611.tar.gz -- admin@ejemplo.com
# Múltiples adjuntos
mutt -s "Logs del sistema" \
-a /var/log/syslog.log \
-a /var/log/auth.log -- admin@ejemplo.com < /tmp/cuerpo.txt
# Envío silencioso (sin interactividad)
mutt -s "Alerta automática" -e "set copy=no" \
admin@ejemplo.com < /tmp/mensaje.txt
# Configuración SMTP inline
mutt -s "Reporte" \
-e "set smtp_url=smtp://usuario:pass@smtp.ejemplo.com:587/" \
admin@ejemplo.com < /tmp/reporte.txt
ssmtp — MTA mínimo para scripts
ssmtp es un MTA mínimo que solo envía, perfecto para servidores sin postfix completo.
# Instalación
sudo apt install ssmtp
# Configuración: /etc/ssmtp/ssmtp.conf
cat > /etc/ssmtp/ssmtp.conf << 'EOF'
root=admin@ejemplo.com
mailhub=smtp.gmail.com:587
AuthUser=tu-cuenta@gmail.com
AuthPass=contraseña-de-aplicacion
UseTLS=YES
FromLineOverride=YES
EOF
# Envío
echo -e "Subject: Alerta del servidor\n\nEl backup ha fallado" | \
ssmtp admin@ejemplo.com
# Con remitente personalizado
printf "From: backup-script@midominio.com\nSubject: Resumen\n\nTodo OK\n" | \
ssmtp admin@ejemplo.com
Adjuntar archivos con mutt
notify_email_attach() {
local to="$1"
local subject="$2"
local body="$3"
shift 3
local attachments=("$@")
if [[ ${#attachments[@]} -eq 0 ]]; then
echo "$body" | mail -s "$subject" "$to"
return
fi
# Construir argumentos -a para mutt
local attach_args=()
for file in "${attachments[@]}"; do
if [[ -f "$file" ]]; then
attach_args+=("-a" "$file")
else
echo "⚠ Archivo no encontrado: $file" >&2
fi
done
echo "$body" | mutt -s "$subject" "${attach_args[@]}" -- "$to"
}
Telegram Bot API
Crear un bot y obtener credenciales
- Habla con @BotFather en Telegram
- Envía
/newboty sigue las instrucciones - Recibirás un Token como
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 - Obtén tu Chat ID: envía un mensaje al bot y visita
https://api.telegram.org/bot<TOKEN>/getUpdates
TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
CHAT_ID="123456789"
⚠ Nunca hardcodees el token en el script. Usa variables de entorno o un archivo de configuración con permisos 600.
sendMessage — Texto con parse_mode
# Mensaje de texto simple
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Hola desde Bash!"
# Con parse_mode: MarkdownV2 o HTML
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=MarkdownV2" \
-d "text=*Backup completado* ✅\n· Archivos: 1,234\n· Tamaño: 2.3 GB\n· Duración: 45s"
# Con HTML
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=HTML" \
-d "text=<b>⚠ ERROR CRÍTICO</b>\n<code>Disco / lleno al 98%</code>"
# Deshabilitar preview de enlaces
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Revisa el reporte: https://ejemplo.com/reporte" \
-d "disable_web_page_preview=true"
Caracteres especiales en MarkdownV2: _ * [ ] ( ) ~ ` > # + - = | { } . ! deben escaparse con \.
sendDocument — Enviar archivos
# Enviar archivo como documento
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendDocument" \
-F "chat_id=${CHAT_ID}" \
-F "document=@/var/log/backup.log" \
-F "caption=📄 Log del backup nocturno"
# Enviar imagen como foto
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendPhoto" \
-F "chat_id=${CHAT_ID}" \
-F "photo=@/tmp/grafica_uso_disco.png" \
-F "caption=📊 Uso de disco semanal"
Función completa
notify_telegram() {
local level="$1"
local message="$2"
local file="${3:-}"
local token="${TELEGRAM_TOKEN:-}"
local chat_id="${TELEGRAM_CHAT_ID:-}"
[[ -z "$token" || -z "$chat_id" ]] && return 1
local icon=""
case "$level" in
INFO) icon="ℹ️ " ;;
WARN) icon="⚠️ " ;;
ERROR) icon="❌ " ;;
CRITICAL) icon="🚨 " ;;
esac
local text="${icon}*[${level}]* ${message}"
if [[ -n "$file" && -f "$file" ]]; then
curl -s -X POST "https://api.telegram.org/bot${token}/sendDocument" \
-F "chat_id=${chat_id}" \
-F "document=@${file}" \
-F "caption=${text}" \
-F "parse_mode=MarkdownV2" > /dev/null
else
curl -s -X POST "https://api.telegram.org/bot${token}/sendMessage" \
-d "chat_id=${chat_id}" \
-d "text=${text}" \
-d "parse_mode=MarkdownV2" > /dev/null
fi
}
Webhooks (Slack / Discord / Mattermost)
Slack — Incoming Webhook
# Obtén la URL del webhook en: Slack API → Incoming Webhooks
SLACK_WEBHOOK="https://hooks.slack.com/services/T00/B00/xxxxx"
# Mensaje simple
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{"text": "Backup completado exitosamente"}'
# Mensaje con formato (mrkdwn)
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"text": "Resumen de Backup",
"attachments": [
{
"color": "#36a64f",
"title": "Backup Nocturno",
"fields": [
{"title": "Estado", "value": "✅ Exitoso", "short": true},
{"title": "Archivos", "value": "1,234", "short": true},
{"title": "Tamaño", "value": "2.3 GB", "short": true},
{"title": "Duración", "value": "45s", "short": true}
],
"footer": "Servidor producción",
"ts": '"$(date +%s)"'
}
]
}'
Discord — Webhook
DISCORD_WEBHOOK="https://discord.com/api/webhooks/123456/xxxxx"
# Mensaje simple
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{"content": "@here ⚠ El servidor web ha caído"}'
# Con embed (rich message)
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"username": "MonitorBot",
"avatar_url": "https://ejemplo.com/icon.png",
"embeds": [{
"title": "🔴 Alerta: Servidor MySQL",
"description": "El servicio MySQL no responde en producción",
"color": 15158332,
"fields": [
{"name": "Host", "value": "db-prod-01", "inline": true},
{"name": "Puerto", "value": "3306", "inline": true},
{"name": "Último OK", "value": "2026-06-11 02:30:00 UTC"}
],
"footer": {"text": "Monitor automático"},
"timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
}]
}'
Colores comunes en Discord:
| Color | Decimal | Uso |
|---|---|---|
| Verde | 3066993 | ✅ INFO |
| Amarillo | 16776960 | ⚠ WARN |
| Rojo | 15158332 | ❌ ERROR |
| Rojo oscuro | 10038562 | 🚨 CRITICAL |
Mattermost — Webhook
MATTERMOST_WEBHOOK="https://mattermost.ejemplo.com/hooks/xxxxx"
curl -s -X POST "$MATTERMOST_WEBHOOK" \
-H "Content-Type: application/json" \
-d '{
"text": "### 🚨 Error en backup\n**Host:** servidor01\n**Error:** Disco lleno\n**Fecha:** 2026-06-11",
"username": "BackupBot",
"icon_url": "https://ejemplo.com/backup-icon.png"
}'
Función webhook genérica
notify_webhook() {
local level="$1"
local message="$2"
local webhook_url="${WEBHOOK_URL:-}"
[[ -z "$webhook_url" ]] && return 1
local color
case "$level" in
INFO) color=3066993 ;; # verde
WARN) color=16776960 ;; # amarillo
ERROR) color=15158332 ;; # rojo
CRITICAL) color=10038562 ;; # rojo oscuro
esac
local payload
payload=$(cat << EOF
{
"content": "[${level}] ${message}",
"embeds": [{
"title": "Notificación de ${HOSTNAME}",
"description": "$(echo "$message" | head -c 2000)",
"color": ${color},
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"footer": {"text": "Script: $(basename "$0") PID: $$"}
}]
}
EOF
)
curl -s -X POST "$webhook_url" \
-H "Content-Type: application/json" \
-d "$payload" > /dev/null
}
Push notifications
ntfy.sh — Push simple y público
ntfy.sh es un servicio de push notifications gratuito y open-source. Puedes usarlo público o self-hosted.
# Enviar notificación a un topic público
curl -d "Backup completado" https://ntfy.sh/mi-topic-personal
# Con título, prioridad y tags
curl -H "Title: Backup Nocturno" \
-H "Priority: high" \
-H "Tags: white_check_mark" \
-d "Backup completado exitosamente" \
https://ntfy.sh/mi-topic
# Prioridades: 1 (min), 2 (low), 3 (default), 4 (high), 5 (max)
curl -H "Priority: 5" \
-H "Tags: warning" \
-H "Title: ⚠ Disco casi lleno" \
-d "El disco / está al 95% de capacidad" \
https://ntfy.sh/mi-topic
# Con click action (abrir URL)
curl -H "Click: https://ejemplo.com/dashboard" \
-d "Revisa el dashboard" \
https://ntfy.sh/mi-topic
# Autenticación con token (self-hosted o ntfy.sh pro)
curl -H "Authorization: Bearer tk_mi_token" \
-d "Mensaje secreto" \
https://ntfy.sh/mi-topic-privado
Tags útiles de ntfy:
| Tag | Icono |
|---|---|
white_check_mark | ✅ |
warning | ⚠️ |
x | ❌ |
rotating_light | 🚨 |
computer | 💻 |
floppy_disk | 💾 |
email | 📧 |
alarm_clock | ⏰ |
Gotify — Self-hosted
Gotify es un servidor de push notifications auto-gestionado.
GOTIFY_URL="http://gotify.ejemplo.com"
GOTIFY_TOKEN="AppToken123"
curl -X POST "${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" \
-F "title=Backup completado" \
-F "message=Todo correcto (2.3 GB, 1,234 archivos)" \
-F "priority=5"
# Prioridades: 0 (min) a 10 (max)
Apprise — Unifica todos los canales
Apprise es una librería que unifica todos los servicios de notificación bajo una sola API.
# Instalación
sudo apt install apprise
pip install apprise # o desde pip
# Enviar a múltiples servicios a la vez
apprise -t "Backup completado" -b "Todo correcto" \
"slack://tokenA/tokenB/tokenC/" \
"tgram://BOTTOKEN/CHATID/" \
"mailto://user:pass@smtp.ejemplo.com:587?to=admin@ejemplo.com"
# Soporta 100+ servicios: Telegram, Slack, Discord, Twilio, Pushover,
# Gotify, ntfy, Email, Teams, Matrix, Signal, y muchos más
SMS con Twilio API
Twilio permite enviar SMS desde cualquier lenguaje que haga HTTP. Necesitas una cuenta de Twilio (el crédito inicial suele ser ~$15).
# Credenciales
TWILIO_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_FROM="+1234567890" # Número Twilio
TWILIO_TO="+0987654321" # Tu número
# Enviar SMS con curl (Basic Auth)
curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json" \
-u "${TWILIO_SID}:${TWILIO_TOKEN}" \
--data-urlencode "From=${TWILIO_FROM}" \
--data-urlencode "To=${TWILIO_TO}" \
--data-urlencode "Body=🚨 CRITICAL: Servidor web caído en producción"
# Verificar respuesta
respuesta=$(curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json" \
-u "${TWILIO_SID}:${TWILIO_TOKEN}" \
--data-urlencode "From=${TWILIO_FROM}" \
--data-urlencode "To=${TWILIO_TO}" \
--data-urlencode "Body=Test" 2>&1)
if echo "$respuesta" | grep -q '"status": "queued"'; then
echo "✅ SMS enviado correctamente"
else
echo "❌ Error al enviar SMS: $respuesta"
fi
⚠ Control de costos: Los SMS no son gratis. Usa SMS solo para nivel CRITICAL y aplica rate limiting estricto.
Batching y rate limiting
Buffer de notificaciones + Digest
Cuando un script genera muchas notificaciones seguidas (ej: healthcheck que verifica 50 servicios), conviene acumularlas en un buffer y enviar un solo mensaje resumen.
# Buffer global de notificaciones
declare -a NOTIFY_BUFFER=()
# Acumular en buffer
notify_buffer() {
local level="$1"
local message="$2"
NOTIFY_BUFFER+=("[$level] $(date +%H:%M:%S) - $message")
}
# Enviar digest
notify_flush() {
local level="${1:-INFO}"
local force="${2:-false}"
[[ ${#NOTIFY_BUFFER[@]} -eq 0 ]] && return
local digest
digest=$(printf "• %s\n" "${NOTIFY_BUFFER[@]}")
local summary="📊 *Resumen de notificaciones*\n"
summary+="· Total: ${#NOTIFY_BUFFER[@]} eventos\n"
summary+="· Período: $(date '+%Y-%m-%d %H:%M')\n\n"
summary+="$digest"
notify_dispatch "$level" "$summary"
NOTIFY_BUFFER=()
}
# Uso en un bucle
for svc in nginx mysql redis; do
if systemctl is-active --quiet "$svc"; then
notify_buffer "INFO" "$svc está activo"
else
notify_buffer "ERROR" "$svc está CAÍDO"
fi
done
# Al final, enviar el digest
notify_flush "WARN"
Rate limiting por canal
Evita tormentas de notificaciones: si algo falla 20 veces en un minuto, solo notifica la primera y luego cada N minutos.
# Archivo de timestamps por tipo de notificación
RATE_FILE="/tmp/notify_rate_$$.txt"
check_rate_limit() {
local channel="$1" # telegram, email, slack, etc.
local min_interval="${2:-300}" # segundos entre notificaciones
local last_time=0
if [[ -f "$RATE_FILE" ]]; then
last_time=$(grep "^${channel}:" "$RATE_FILE" 2>/dev/null | cut -d: -f2)
last_time=${last_time:-0}
fi
local now
now=$(date +%s)
if (( now - last_time < min_interval )); then
return 1 # Rate limited, no enviar
fi
# Actualizar timestamp
if [[ -f "$RATE_FILE" ]]; then
sed -i "/^${channel}:/d" "$RATE_FILE"
fi
echo "${channel}:${now}" >> "$RATE_FILE"
return 0 # OK, enviar
}
# Uso
if check_rate_limit "telegram" 300; then
notify_telegram "ERROR" "Servicio caído"
else
echo "⚠ Telegram rate limited, esperando 5 min" >&2
fi
Circuit breaker
Si un canal falla repetidamente (ej: el webhook de Slack devuelve 429 Too Many Requests), desactívalo temporalmente:
declare -A CHANNEL_FAILURES=()
MAX_FAILURES=3
COOLDOWN=600 # minutos
notify_with_circuit_breaker() {
local channel="$1"
shift
local failures="${CHANNEL_FAILURES[$channel]:-0}"
if (( failures >= MAX_FAILURES )); then
echo "⚠ Circuit breaker abierto para $channel ($failures fallos)" >&2
return 1
fi
if "$@"; then
CHANNEL_FAILURES[$channel]=0
else
CHANNEL_FAILURES[$channel]=$((failures + 1))
if (( CHANNEL_FAILURES[$channel] >= MAX_FAILURES )); then
echo "🚨 Circuit breaker activado para $channel. Esperando ${COOLDOWN}s" >&2
(sleep "$COOLDOWN" && unset CHANNEL_FAILURES[$channel]) &
fi
fi
}
Configuración centralizada
Centraliza toda la configuración de notificaciones en un solo archivo:
# ~/.notifirc — Configuración de notificaciones
# Cargar con: source ~/.notifirc
# ── Canal: Desktop ──
NOTIFY_DESKTOP_ENABLE=true
NOTIFY_DESKTOP_LEVELS="WARN ERROR CRITICAL"
NOTIFY_DESKTOP_EXPIRE=5000
NOTIFY_DESKTOP_ICON_WARN=dialog-warning
NOTIFY_DESKTOP_ICON_ERROR=dialog-error
# ── Canal: Email ──
NOTIFY_EMAIL_ENABLE=true
NOTIFY_EMAIL_LEVELS="INFO WARN ERROR CRITICAL"
NOTIFY_EMAIL_TO="admin@ejemplo.com"
NOTIFY_EMAIL_FROM="backup-script@servidor.local"
NOTIFY_EMAIL_MTA="mail" # mail | mutt | ssmtp
# ── Canal: Telegram ──
NOTIFY_TELEGRAM_ENABLE=true
NOTIFY_TELEGRAM_LEVELS="WARN ERROR CRITICAL"
NOTIFY_TELEGRAM_BOT_TOKEN="123456:ABC-DEF1234ghIkl"
NOTIFY_TELEGRAM_CHAT_ID="123456789"
# ── Canal: Webhook (Slack/Discord) ──
NOTIFY_WEBHOOK_ENABLE=true
NOTIFY_WEBHOOK_LEVELS="ERROR CRITICAL"
NOTIFY_WEBHOOK_URL="https://hooks.slack.com/services/..."
# ── Canal: SMS ──
NOTIFY_SMS_ENABLE=false
NOTIFY_SMS_LEVELS="CRITICAL"
NOTIFY_SMS_TWILIO_SID="AC..."
NOTIFY_SMS_TWILIO_TOKEN="..."
NOTIFY_SMS_FROM="+1234567890"
NOTIFY_SMS_TO="+0987654321"
# ── Canal: Sonido ──
NOTIFY_SOUND_ENABLE=false
NOTIFY_SOUND_LEVELS="CRITICAL"
# ── Canal: Sistema ──
NOTIFY_WALL_ENABLE=false
NOTIFY_WALL_LEVELS="CRITICAL"
# ── General ──
NOTIFY_RATE_INTERVAL=300 # Segundos entre notificaciones del mismo canal
NOTIFY_CIRCUIT_BREAKER=3 # Fallos antes de desactivar canal
NOTIFY_BUFFER_ENABLE=true # Acumular antes de enviar
NOTIFY_BUFFER_FLUSH_INTERVAL=60 # Forzar flush cada N segundos
Función que evalúa qué canales activar:
canal_habilitado_para_nivel() {
local canal="$1"
local level="$2"
local enable_var="NOTIFY_${canal}_ENABLE"
local levels_var="NOTIFY_${canal}_LEVELS"
[[ "${!enable_var:-false}" != "true" ]] && return 1
local levels="${!levels_var:-}"
[[ -z "$levels" ]] && return 1
# Verificar si el nivel está en la lista
for l in $levels; do
[[ "$l" == "$level" ]] && return 0
done
return 1
}
Casos prácticos
Backup notifications
#!/bin/bash
# backup_notify.sh — Backup con notificaciones multicanal
set -euo pipefail
source ~/.notifirc
ORIGEN="/home/lorenzo/documentos"
DESTINO="/backups/documentos_$(date +%Y%m%d).tar.gz"
LOG="/var/log/backup_notify.log"
notify() {
local level="$1"
local msg="$2"
echo "$(date) [$level] $msg" >> "$LOG"
if canal_habilitado_para_nivel "DESKTOP" "$level"; then
notify-send -u "${level,,}" "[$level] Backup" "$msg"
fi
if canal_habilitado_para_nivel "TELEGRAM" "$level"; then
notify_telegram "$level" "$msg"
fi
if canal_habilitado_para_nivel "EMAIL" "$level"; then
notify_email "$level" "$msg"
fi
}
echo "=== Iniciando backup: $(date) ===" >> "$LOG"
if tar czf "$DESTINO" "$ORIGEN" 2>> "$LOG"; then
notify "INFO" "Backup completado: $DESTINO ($(du -h "$DESTINO" | cut -f1))"
else
notify "ERROR" "Backup FALLIDO: $ORIGEN → $DESTINO"
fi
Healthcheck + alerta
#!/bin/bash
# healthcheck.sh — Verificar servicios y notificar si caen
source ~/.notifirc
SERVICIOS=("nginx" "postgresql" "redis" "docker")
FALLOS=()
for svc in "${SERVICIOS[@]}"; do
if ! systemctl is-active --quiet "$svc" 2>/dev/null; then
FALLOS+=("$svc")
fi
done
if [[ ${#FALLOS[@]} -gt 0 ]]; then
local msg="Servicios CAÍDOS: ${FALLOS[*]}"
notify_dispatch "ERROR" "$msg"
notify_sound "ERROR"
exit 1
else
notify_dispatch "INFO" "Healthcheck OK: todos los servicios activos"
fi
Cron wrapper notificador
#!/bin/bash
# cron_notify.sh — Envuelve cualquier comando de cron y notifica el resultado
# Uso: ./cron_notify.sh [--silent-ok] <comando> [args...]
source ~/.notifirc
SILENT_OK=false
if [[ "$1" == "--silent-ok" ]]; then
SILENT_OK=true
shift
fi
if [[ $# -eq 0 ]]; then
echo "Uso: $0 [--silent-ok] <comando> [args...]" >&2
exit 1
fi
INICIO=$(date +%s)
COMANDO="$*"
# Capturar stdout y stderr
TMPOUT=$(mktemp)
TMPERR=$(mktemp)
if "$@" >"$TMPOUT" 2>"$TMPERR"; then
DURACION=$(( $(date +%s) - INICIO ))
if $SILENT_OK; then
echo "[OK] $COMANDO — ${DURACION}s" | notify_email "INFO" "Cron OK: $COMANDO"
fi
else
DURACION=$(( $(date +%s) - INICIO ))
ERROR=$(cat "$TMPERR" | head -20)
SALIDA=$(cat "$TMPOUT" | head -20)
MSG="❌ *Comando fallido*\n· Host: $(hostname)\n· Comando: \`$COMANDO\`\n· Duración: ${DURACION}s\n· Exit: $?\n\n*stderr:*\n\`\`\`\n${ERROR}\n\`\`\`"
notify_dispatch "ERROR" "$MSG"
if [[ -s "$TMPERR" ]]; then
notify_telegram "ERROR" "📎 Log de error adjunto" "$TMPERR"
fi
fi
rm -f "$TMPOUT" "$TMPERR"
Funciones profesionales
# ──────────────────────────────────────────────────
# Funciones profesionales para notificaciones
# ──────────────────────────────────────────────────
# notify_send_nivel — Notificación desktop con urgencia e icono
# Uso: notify_send_nivel INFO|WARN|ERROR|CRITICAL "mensaje" [icono_opcional]
notify_send_nivel() {
local level="$1"
local message="$2"
local icon="${3:-}"
command -v notify-send &>/dev/null || return 1
local urgency="normal"
local expire=5000
local default_icon="dialog-information"
case "$level" in
INFO)
urgency="low"
expire=3000
default_icon="dialog-information"
;;
WARN)
urgency="normal"
expire=5000
default_icon="dialog-warning"
;;
ERROR)
urgency="critical"
expire=8000
default_icon="dialog-error"
;;
CRITICAL)
urgency="critical"
expire=0 # No expira hasta que el usuario la cierre
default_icon="emblem-important"
;;
esac
notify-send -u "$urgency" -t "$expire" -i "${icon:-$default_icon}" \
"[$level] $(hostname)" "$message"
}
# notify_email — Enviar email con nivel y adjunto opcional
# Uso: notify_email INFO|WARN|ERROR|CRITICAL "mensaje" [archivo_adjunto]
notify_email() {
local level="$1"
local message="$2"
local attach="${3:-}"
local to="${NOTIFY_EMAIL_TO:-}"
local mta="${NOTIFY_EMAIL_MTA:-mail}"
[[ -z "$to" ]] && return 1
command -v "$mta" &>/dev/null || return 1
local subject="[$level] $(hostname) — $(date '+%Y-%m-%d %H:%M')"
local body="Nivel: $level
Host: $(hostname)
Script: $(basename "$0") [PID: $$]
Fecha: $(date '+%Y-%m-%d %H:%M:%S')
$message
---
Notificación automática generada por $(basename "$0")"
case "$mta" in
mail|mailx)
if [[ -n "$attach" && -f "$attach" ]]; then
# mailx no soporta adjuntos nativos; usar mutt
command -v mutt &>/dev/null || {
echo "$body" | "$mta" -s "$subject" "$to"
return
}
echo "$body" | mutt -s "$subject" -a "$attach" -- "$to"
else
echo "$body" | "$mta" -s "$subject" "$to"
fi
;;
mutt)
if [[ -n "$attach" && -f "$attach" ]]; then
echo "$body" | mutt -s "$subject" -a "$attach" -- "$to"
else
echo "$body" | mutt -s "$subject" "$to"
fi
;;
ssmtp)
{
echo "From: ${NOTIFY_EMAIL_FROM:-root@$(hostname -f)}"
echo "To: $to"
echo "Subject: $subject"
echo ""
echo "$body"
} | ssmtp "$to"
;;
esac
}
# notify_telegram — Enviar mensaje o archivo a Telegram
# Uso: notify_telegram INFO|WARN|ERROR|CRITICAL "mensaje" [archivo]
notify_telegram() {
local level="$1"
local message="$2"
local file="${3:-}"
local token="${NOTIFY_TELEGRAM_BOT_TOKEN:-${TELEGRAM_TOKEN:-}}"
local chat_id="${NOTIFY_TELEGRAM_CHAT_ID:-${TELEGRAM_CHAT_ID:-}}"
[[ -z "$token" || -z "$chat_id" ]] && return 1
# Emoji por nivel
local emoji
case "$level" in
INFO) emoji="ℹ️" ;;
WARN) emoji="⚠️" ;;
ERROR) emoji="❌" ;;
CRITICAL) emoji="🚨" ;;
esac
local text="${emoji} *[${level}]* $(hostname)"
text+=$'\n'"$(echo "$message" | sed 's/[_*\[\]()~`>#+\-=|{}.!]/\\&/g')"
if [[ -n "$file" && -f "$file" ]]; then
curl -s -X POST "https://api.telegram.org/bot${token}/sendDocument" \
-F "chat_id=${chat_id}" \
-F "document=@${file}" \
-F "caption=${text}" \
-F "parse_mode=MarkdownV2" > /dev/null 2>&1
else
curl -s -X POST "https://api.telegram.org/bot${token}/sendMessage" \
-d "chat_id=${chat_id}" \
-d "text=${text}" \
-d "parse_mode=MarkdownV2" \
-d "disable_web_page_preview=true" > /dev/null 2>&1
fi
}
# notify_webhook — Enviar notificación formateada a Slack/Discord/Mattermost
# Uso: notify_webhook INFO|WARN|ERROR|CRITICAL "mensaje"
notify_webhook() {
local level="$1"
local message="$2"
local webhook_url="${NOTIFY_WEBHOOK_URL:-${WEBHOOK_URL:-}}"
[[ -z "$webhook_url" ]] && return 1
local color name
case "$level" in
INFO) color="3066993"; name="✅ INFO" ;;
WARN) color="16776960"; name="⚠️ WARN" ;;
ERROR) color="15158332"; name="❌ ERROR" ;;
CRITICAL) color="10038562"; name="🚨 CRITICAL" ;;
esac
local payload
payload=$(cat << JSONEOF
{
"content": "${name} desde $(hostname)",
"embeds": [{
"title": "Notificación automática",
"description": "$(echo "$message" | head -c 1800)",
"color": ${color},
"fields": [
{"name": "Nivel", "value": "${level}", "inline": true},
{"name": "Host", "value": "$(hostname)", "inline": true},
{"name": "Script", "value": "$(basename "$0")", "inline": true}
],
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"footer": {"text": "PID: $$"}
}]
}
JSONEOF
)
curl -s -X POST "$webhook_url" \
-H "Content-Type: application/json" \
-d "$payload" > /dev/null 2>&1
}
Errores comunes
Error 1: Hardcodear tokens y credenciales en el script
# ❌ Token visible en el código (Git, pantallazos, etc.)
TOKEN="123456:ABC-DEF1234ghIkl"
# ✅ Variables de entorno o archivo con permisos 600
export TELEGRAM_TOKEN="123456:ABC-DEF1234ghIkl" # en ~/.bashrc o .env
source ~/.notifirc # archivo con chmod 600
Error 2: No verificar que el comando de notificación existe
# ❌ En un servidor sin X, notify-send falla silenciosamente
notify-send "Alerta" # command not found → script sigue, pero no notifica
# ✅ Verificar disponibilidad antes de usar
if command -v notify-send &>/dev/null; then
notify-send -u critical "Alerta" "Disco lleno"
fi
Error 3: Enviar demasiadas notificaciones (tormenta)
# ❌ Bucle que verifica 50 servicios y envía 50 notificaciones individuales
for svc in $(systemctl list-units --type=service --all --no-pager | awk '{print $1}'); do
if ! systemctl is-active --quiet "$svc"; then
notify_telegram "ERROR" "$svc caído" # ¡50 mensajes!
fi
done
# ✅ Acumular en buffer y enviar digest
for svc in ...; do
if ! systemctl is-active --quiet "$svc"; then
notify_buffer_add "ERROR" "$svc caído"
fi
done
notify_buffer_flush # mensaje con los 50 servicios
Error 4: No escapar caracteres especiales en Telegram MarkdownV2
# ❌ El mensaje contiene . o - que rompen MarkdownV2
text="Error en backup_2026.06.11-version_final.tar.gz"
# ✅ Escapar caracteres especiales
text=$(echo "$text" | sed 's/[_*\[\]()~`>#+\-=|{}.!]/\\&/g')
Error 5: Ignorar el rate limiting de APIs externas
# ❌ Enviar 20 mensajes a Slack en 1 segundo → HTTP 429
for i in {1..20}; do
curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Alerta $i\"}"
done
# ✅ Aplicar espera entre mensajes
for i in {1..20}; do
curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Alerta $i\"}"
sleep 2 # respetar rate limit de Slack (1 msg/seg)
done
Error 6: No usar parse_mode seguro o fallar por HTML inválido
# ❌ HTML mal formado → Telegram ignora el parse_mode y muestra el raw
curl -d "parse_mode=HTML" -d "text=<b>Error</i> en backup" # <b> no cerrado
# ✅ Usar MarkdownV2 con escape, o validar HTML
curl -d "parse_mode=MarkdownV2" -d "text=*Error* en backup"
Error 7: Olvidar que wall requiere permisos en systemd
# ❌ wall en systemd no llega a terminales gráficos
wall "Mensaje importante" # Puede que nadie lo vea
# ✅ Combinar wall con notify-send y logger
notify-send -u critical "Aviso" "Mensaje importante"
logger -p user.alert -t "$0" "Mensaje importante"
wall "Mensaje importante" 2>/dev/null || true
Error 8: No manejar fallo de red en los envíos
# ❌ Si no hay internet, la notificación falla silenciosamente
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CID}" -d "text=Alerta" # Falla, nadie lo sabe
# ✅ Verificar conectividad y reintentar
if ping -c 1 -W 2 api.telegram.org &>/dev/null; then
for intento in {1..3}; do
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CID}" -d "text=Alerta" && break
sleep $((intento * 2))
done
else
# Canal alternativo: log local
logger -p user.err "No se pudo enviar notificación Telegram: sin conexión"
fi
Error 9: No limpiar archivos temporales de rate limiting
# ❌ Los archivos de rate limit se acumulan en /tmp
RATE_FILE="/tmp/notify_rate_$$.txt"
# Si el script se mata con kill -9, el archivo temporal queda para siempre
# ✅ Trap para limpiar siempre
trap 'rm -f "$RATE_FILE"' EXIT INT TERM
Error 10: Confundir canales y enviar información sensible al canal equivocado
# ❌ Enviar logs con contraseñas por Telegram (canal inseguro)
notify_telegram "ERROR" "$(cat /etc/backup/passphrase.txt)"
# ✅ Filtrar información sensible por canal
if [[ "$NIVEL" == "CRITICAL" ]]; then
notify_telegram "ERROR" "Error en backup (detalles en email)"
notify_email "ERROR" "$(cat /var/log/backup.log)" # Adjunto completo por email
fi
Más información,
- Telegram Bot API — sendMessage — Documentación oficial de la API de bots
- Telegram Bot API — sendDocument — Envío de archivos y documentos
- Slack Incoming Webhooks — Envío de mensajes a Slack desde cualquier servicio
- Discord Webhook Guide — Cómo crear y usar webhooks en Discord
- Mattermost Incoming Webhooks — Webhooks entrantes en Mattermost
- Twilio SMS API — Envío de SMS programático
- ntfy.sh — Documentación — Push notifications con curl
- Gotify — Documentación — Servidor de push notifications self-hosted
- Apprise — Supported Notifications — Lista de 100+ servicios soportados
- libnotify — Specification — Especificación de notificaciones de escritorio
- Freedesktop Sound Theme — Sonidos del sistema y su propósito
- Bash Hackers Wiki — curl examples — Ejemplos avanzados de curl en Bash
- Rate Limiting Best Practices — Estrategias de rate limiting para APIs