En entornos de producción con múltiples nodos de Nginx, la revisión manual de los archivos de errores (error.log) puede resultar inviable. En este artículo, detallamos cómo diseñar e implementar una solución de monitorización automatizada utilizando un Playbook de Ansible y un Script de Python integrado. El sistema recordará la última posición leída en el log (evitando procesar datos repetidos), agrupará los errores, registrará una auditoría local del proceso y enviará un único correo por nodo en caso de anomalías.
- Arquitectura y componentes clave para automatización de lectura de log Nginx con Ansible.
- Paso 1: Configurar el inventario de Ansible (Hosts y variables).
- Paso 2: El Playbook de Ansible para automatizar lectura de log Nginx y envío de email.
- Explicación detallada del playbook.
- 1. Hosts objetivos del playbook.
- 2. El Control de desplazamiento (Offset).
- 3. Gestión de la Rotación del Log (Logrotate).
- 4. De-duplicación por expresiones regulares.
- 5. Escalado de privilegios (
become: yes) y buenas prácticas. - Paso 3: Crear tarea en Ansible/Semaphore para ejecutar playbook.
- Ejecutar playbook y creación de programación para ejecución periódica.
Arquitectura y componentes clave para automatización de lectura de log Nginx con Ansible
Para automatizar tareas con Ansible, necesitaremos disponer de un servidor con Ansible/Sempahore, en el siguiente tutorial explicamos cómo desplegarlo:
El despliegue de esta automatización se divide en tres capas técnicas:
- Gestión de inventario dinámico (Ansible): permite parametrizar las rutas específicas de los archivos de log de manera independiente para cada nodo o balanceador Nginx.
- Lógica de análisis de logs (Python): desde el playbook de Ansible se generará un script de Python que realiza una lectura secuencial a partir de la última posición conocida (offset). Utiliza expresiones regulares (
re) para extraer el error, descartando las variables mutables del formato de Nginx (como PID o marcas de tiempo). De esta manera, si un error ocurre muchas veces, se almacena en memoria como una sola clave única. - Auditoría e historial local: tanto el script de Python como el orquestador Ansible interactúan directamente con el sistema de archivos local (
/var/log/nginx_monitor.log) utilizando elevación de privilegios (become: yes) para registrar cuándo comenzó el análisis, cuántas líneas se procesaron y el estado exacto del envío de alertas por correo SMTP.
Paso 1: Configurar el inventario de Ansible (Hosts y variables)
El primer paso configuraremos Ansible/Semaphore añadiendo el inventario de hosts (nodos de Nginx a monitorizar) y las variables de cada nodo (ruta del fichero de error.log) y variables de servidor SMTP para envío de email.
En este estudio de caso, cada servidor Linux con Nginx tiene el fichero de error.log con un nombre diferente, por ello, estableceremos en su inventario, en la declaración de cada host, en variable error_log_path, la ruta del fichero de error.log. Desde Ansible/Sempahore, en «Inventario», añadiremos un nuevo inventario:

En el nuevo inventario de servidores, añadiremos, en formato INI, los servidores que queramos automatizar. En este ejemplo, tenemos un grupo de servidores de Nginx (servidores_nginx) y otro grupo servidores_bd, aplicaremos la automatización solo al grupo servidores_nginx:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[servidores_nginx] webproxy ansible_host=192.168.20.135 error_log_path="/var/log/nginx/var/balanceador_error.log" webnodo1 ansible_host=192.168.20.136 error_log_path="/var/log/nginx/var/log/nginx/nodo1_error.log" webnodo2 ansible_host=192.168.20.137 error_log_path="/var/log/nginx/var/log/nginx/nodo2_error.log" webnodo3 ansible_host=192.168.20.138 error_log_path="/var/log/nginx/var/log/nginx/nodo3_error.log" [servidores_db] mariadb1 ansible_host=192.168.20.140 # Grupo padre que une a los dos anteriores [servidores_linux:children] servidores_nginx servidores_db # Variables globales para TODO el inventario de Linux [servidores_linux:vars] ansible_user=anb_sru ansible_python_interpreter=/usr/bin/python3 |
Nota: si en algún nodo Nginx no se define la variable error_log_path, se tomará la ruta de error.log por defecto.

Por otro lado, definiremos las variables con los datos del servidor de SMTP para envío de email (usuario, contraseña, puerto, servidor, emisor, destinatario, etc.). Para ello, desde «Grupos de Variables», pulsaremos en «Nuevo grupo»:

Introduciremos un nombre para el grupo de variables, por ejemplo «servidor_email», y añadiremos las siguientes variables con sus valores correspondientes: smtp_user, smtp_pass, email_from, email_to, smtp_host y smtp_port.

Paso 2: El Playbook de Ansible para automatizar lectura de log Nginx y envío de email
En el servidor de Ansible, si tenemos playbook locales, crearemos un fichero de playbook, con formato YAML, llamado comprobar_errores_nginx.yml. Este archivo coordina el despliegue del script, su ejecución como súper usuario, el parseo de datos estructurados en formato JSON y el control de flujos para el envío del correo. Pegaremos el siguiente contenido:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
--- - name: Monitorear logs de error de Nginx y si hay errores enviar por email hosts: servidores_nginx gather_facts: yes vars: # Si no se define la ruta exacta en el inventario, se toma la de default log_path: "{{ error_log_path | default('/var/log/nginx/error.log') }}" state_file: "/tmp/nginx_log_state.json" script_log_file: "/var/log/nginx_monitor.log" tasks: - name: Crear script de Python con filtrado de errores duplicados y log local copy: dest: /tmp/check_errors.py mode: '0755' content: | #!/usr/bin/env python3 import os import json import sys import re import logging log_path = "{{ log_path }}" state_file = "{{ state_file }}" script_log = "{{ script_log_file }}" # Configuración del log Nativo logging.basicConfig( filename=script_log, level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info(f"--- Iniciando análisis en {log_path} ---") if not os.path.exists(log_path): msg = "Fichero de error de log no encontrado" logging.error(f"{msg}: {log_path}") print(json.dumps({"errors": [], "msg": msg})) sys.exit(0) # Recuperar el Offset (Puntero del archivo) last_position = 0 if os.path.exists(state_file): try: with open(state_file, 'r') as f: last_position = json.load(f).get('offset', 0) logging.info(f"Offset anterior recuperado: {last_position} bytes") except Exception as e: logging.warning(f"No se pudo leer el archivo de estado ({e}). Iniciando desde 0.") last_position = 0 current_size = os.path.getsize(log_path) logging.info(f"Tamaño actual del fichero de log: {current_size} bytes") # Control de Rotación de Logs (Logrotate) if current_size < last_position: logging.warning(f"El log ha rotado o vaciado. Reiniciando offset a 0.") last_position = 0 unique_errors = {} total_lines_processed = 0 critical_errors_found = 0 # Expresión regular para limpiar metadatos variables de Nginx nginx_pattern = re.compile(r'^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \[[a-z]+\] \d+#\d+: \*\d+ (.+)$') with open(log_path, 'r', errors='ignore') as f: f.seek(last_position) for line in f: total_lines_processed += 1 line_str = line.strip() # Filtrado por niveles críticos de Nginx if any(level in line_str for level in ["[error]", "[crit]", "[alert]", "[emerg]"]): critical_errors_found += 1 match = nginx_pattern.match(line_str) if match: error_core = match.group(1) else: error_core = line_str # Evitar duplicidad de errores usando el núcleo del error como clave de diccionario unique_errors[error_core] = line_str new_position = f.tell() # Persistencia del nuevo Offset try: with open(state_file, 'w') as f: json.dump({'offset': new_position}, f) logging.info(f"Nuevo offset guardado con éxito: {new_position} bytes") except Exception as e: logging.error(f"Error al guardar el archivo de estado: {e}") # Formatear la salida estándar en formato JSON limpio para Ansible result_errors = [] for core, full_line in unique_errors.items(): result_errors.append({ "clean_message": core, "full_line": full_line }) logging.info(f"Análisis finalizado. Líneas leídas: {total_lines_processed}. Errores únicos: {len(result_errors)}.") print(json.dumps({"errors": result_errors})) - name: Ejecutar el script de análisis command: python3 /tmp/check_errors.py become: yes register: python_script_output changed_when: false - name: Parsear la salida del script Python set_fact: log_analysis: "{{ python_script_output.stdout | from_json }}" - name: Registrar en el log local el inicio del envío del correo lineinfile: path: "{{ script_log_file }}" line: "{{ ansible_facts['date_time']['date'] }} {{ ansible_facts['date_time']['time'] }} [INFO] Enviando email..." create: yes become: yes when: log_analysis.errors | length > 0 - name: Enviar correo con errores únicos community.general.mail: host: "{{ smtp_host }}" port: "{{ smtp_port | int }}" username: "{{ smtp_user }}" password: "{{ smtp_pass }}" to: "{{ email_to }}" from: "{{ email_from }}" secure: starttls subject: "Atención: Error en Nginx {{ ansible_facts['hostname'] }} ({{ ansible_facts['default_ipv4']['address'] }})" body: | Se han detectado errores en Nginx, en {{ ansible_facts['hostname'] }} ({{ ansible_facts['default_ipv4']['address'] }}). Host: {{ ansible_facts['hostname'] }} IP: {{ ansible_facts['default_ipv4']['address'] }} Fecha: {{ ansible_facts['date_time']['iso8601'] }} -------------------------------------------------- ERRORES DETECTADOS (Sin duplicados consecutivos) -------------------------------------------------- {% for err in log_analysis.errors %} [*] Error: {{ err.clean_message }} [+] Última línea de log completa: {{ err.full_line }} -------------------------------------------------- {% endfor %} delegate_to: localhost when: log_analysis.errors | length > 0 - name: Registrar en el log local la confirmación del envío del correo lineinfile: path: "{{ script_log_file }}" line: "{{ ansible_facts['date_time']['date'] }} {{ ansible_facts['date_time']['time'] }} [INFO] Email enviado...." create: yes become: yes when: log_analysis.errors | length > 0 - name: Limpiar script temporal file: path: /tmp/check_errors.py state: absent |
Guardaremos el fichero de playbook en la carpeta que tengamos establecida para tal propósito:

Explicación detallada del playbook
1. Hosts objetivos del playbook
En la sección Hosts del playbook hemos definido servidores_nginx. Esta línea es muy importante porque en ella estamos definiendo que el playbook se ejecute sólo en el grupo de servidores servidores_nginx, que definimos en el inventario anteriormente. En caso establecer all en Hosts, el playbook se ejecutará en todos los servidores indicados en el inventario asignado a la tarea que ejecuta este playbook.
2. El Control de desplazamiento (Offset)
Leer archivos de gigabytes de tamaño de arriba a abajo en cada ejecución consume CPU e I/O de manera innecesaria. El script mitiga esto utilizando la función nativa de Python f.seek(last_position). Al final del escaneo, f.tell() captura la posición exacta en bytes donde terminó la lectura y la almacena en un archivo JSON local (/tmp/nginx_log_state.json). La siguiente vez que corre el playbook, la lectura comienza directamente desde ese byte exacto.
3. Gestión de la Rotación del Log (Logrotate)
¿Qué pasa si el sistema operativo compacta y vacía el log a medianoche? El script lo controla mediante una validación matemática simple:
|
1 |
if current_size < last_position: |
Si el tamaño actual del archivo en disco es menor que la posición guardada en el historial, significa que el archivo ha sido rotado o truncado. El script reinicia automáticamente el puntero a cero (0) evitando la pérdida de lecturas o errores de desbordamiento.
4. De-duplicación por expresiones regulares
Una línea típica de error en Nginx tiene metadatos dinámicos, por ejemplo:
|
1 |
2026/06/25 10:43:19 [error] 1234#1234: *98765 connect() failed (111: Connection refused) while connecting to upstream... |
Si la IP genera varias peticiones fallidas idénticas, las marcas de tiempo y los ID de conexión (*98765) cambiarán en cada línea de manera constante. Nuestra expresión regular elimina esos metadatos cambiantes:
|
1 |
nginx_pattern = re.compile(r'^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \[[a-z]+\] \d+#\d+: \*\d+ (.+)$') |
Al aislar la cadena (connect() failed...), el script la utiliza como una clave de diccionario (unique_errors[error_core] = line_str). Python descarta de forma automática las entradas anteriores repetidas y mantiene únicamente el último evento registrado en el bloque leído.
5. Escalado de privilegios (become: yes) y buenas prácticas
Uso de Ansible Facts nativos: en cumplimiento con las directivas modernas de desarrollo de Ansible (evitando los Deprecation Warnings de inyección de variables globales), se invoca al diccionario estructurado ansible_facts (ej. {{ ansible_facts['hostname'] }}) en lugar de la sintaxis obsoleta {{ ansible_hostname }}. Esto blinda nuestra automatización para que sea compatible con las futuras versiones de ansible-core.
Elevación de privilegios: aunque el usuario del sistema encargado de ejecutar la automatización (en este caso, anb_sru) tiene reglas sudoers habilitadas, Ansible necesita explícitamente el parámetro become: yes en las tareas de ejecución y en la inyección de líneas con lineinfile. Esto asegura que se posean los privilegios de lectura sobre el directorio restringido /var/log/nginx/ y permisos de escritura para auditar localmente en /var/log/nginx_monitor.log.
Paso 3: Crear tarea en Ansible/Semaphore para ejecutar playbook
En Ansible/Semaphore, crearemos una nueva tarea, desde «Plantillas de Tareas» – «Nueva plantilla», elegiremos «Ansible Playbook»:

Introduciremos los siguientes datos para la plantilla de tarea:
- Nombre: comprobar_errores_nginx.
- Ruta al archivo de playbook: comprobar_errores_nginx.yml.
- Inventario: Servidores_linux. En este caso, aunque en este grupo de inventario tengamos otros servidores, solo cogerá los del grupo indicado en el playbook (hosts): servidores_nginx.
- Repositorio: Local.
- Grupos de Variables: elegiremos el grupo creado anteriormente «servidor_email».

Ejecutar playbook y creación de programación para ejecución periódica
Realizaremos una primera prueba de ejecución, desde «Plantilla de Tareas», pulsaremos en «Play» en la tarea creada:

Se lanzará la ejecución, Ansible conectará con cada nodo/servidor del grupo servidores_nginx, definido en el inventario «servidores_linux», creará el script Python, lo ejecutará y realizará la tarea indicada de lectura de fichero error.log y, si detecta nuevos errores, envío de email:

En cada servidor, habrá creado un fichero de log con el resultado de la ejecución del playbook:
|
1 |
cat /var/log/nginx_monitor.log |

Si alguno de los ficheros access.log de algún nodo tiene errores nuevos, recibiremos un email con los datos del nodo y e lerror:

Una vez revisado el playbook, cuando funcione correctamente, añadiremos la programación oportuna en Semaphore para que el playbook se ejecute cada cierto tiempo. Para ello, desde «Hoorario», pulsaremos en «Nuevo horario» y en «Cron»:

Elegiremos el Template «comprobar_errores_nginx» y estableceremos el Timing, por ejemplo, ejecutaremos la automatización cada 10 minutos:

A partir de ahora, la automatización se ejecutará cada 10 minutos en todos los servidores.