Monitorización de Odoo con ELK Stack y alertas Telegram

Cómo centralizar logs y métricas de Odoo con Elasticsearch, Logstash/Filebeat y Kibana, parsear los logs propios de Odoo y recibir alertas proactivas en un bot de Telegram cuando algo falla.

Por qué la observabilidad en Odoo marca la diferencia

La mayoría de implantaciones Odoo se administran de forma reactiva: alguien llama diciendo que Odoo va lento, se abre una sesión SSH, se mira el log con tail -f, y se intenta deducir qué ha pasado. Este enfoque tiene un problema fundamental: cuando te enteras del problema, el daño ya está hecho. Una query que tarda 45 segundos no hace saltar ninguna alarma hasta que el servidor se satura. Un worker que se muere silenciosamente deja a usuarios sin servicio sin que nadie lo sepa.

La observabilidad proactiva —centralizar logs, medir métricas, definir umbrales y recibir alertas antes de que el usuario se queje— es la diferencia entre un sistema gestionado y uno administrado por crisis. Esta guía describe la arquitectura que hemos implementado en producción en varios clientes, incluyendo Rehabmedic, donde la combinación de ELK Stack y alertas en Telegram nos permitió detectar y resolver incidencias antes de que impactaran en el negocio.

Arquitectura de observabilidad: visión general

  ┌─────────────────────────────────────────────────────┐
  │                  SERVIDOR ODOO                      │
  │  /var/log/odoo/odoo.log                             │
  │  /var/log/postgresql/postgresql.log                 │
  │  métricas de sistema (CPU, memoria, disco)          │
  │        │                                            │
  │  ┌─────▼──────┐                                     │
  │  │  Filebeat   │ (agente ligero, sin lógica)         │
  │  └─────┬──────┘                                     │
  └────────│────────────────────────────────────────────┘
           │  TCP/TLS :5044
  ┌────────▼────────────────────────────────────────────┐
  │                SERVIDOR ELK                          │
  │                                                     │
  │  ┌──────────┐    ┌──────────────┐    ┌───────────┐  │
  │  │ Logstash  │───▶│Elasticsearch │───▶│  Kibana   │  │
  │  │ (parseo  │    │  (almacén +  │    │(dashboard │  │
  │  │ + enriq.)│    │   búsqueda)  │    │ + alertas)│  │
  │  └──────────┘    └──────┬───────┘    └───────────┘  │
  └─────────────────────────│───────────────────────────┘
                            │  Watcher / ElastAlert
                    ┌───────▼──────────┐
                    │   Bot Telegram   │
                    │  (alertas HTTP)  │
                    └──────────────────┘

Los componentes son:

  • Filebeat: agente instalado en el servidor Odoo. Lee los ficheros de log y los envía a Logstash con mínimo consumo de recursos.
  • Logstash: pipeline de procesamiento. Parsea los logs de Odoo (formato propio), extrae campos estructurados, enriquece con metadatos y normaliza.
  • Elasticsearch: base de datos de búsqueda y analítica donde se almacenan todos los eventos indexados.
  • Kibana: interfaz web para exploración, dashboards y configuración de alertas (Watcher o Kibana Alerting).
  • ElastAlert / script Python: motor de alertas que evalúa condiciones sobre Elasticsearch y dispara notificaciones a Telegram.

Qué datos recoger de Odoo

Odoo genera varios flujos de eventos que conviene monitorizar de forma diferenciada:

1. Log de aplicación Odoo (/var/log/odoo/odoo.log)

Es la fuente principal. El formato por defecto de Odoo es:

2026-05-31 08:42:17,123 12345 INFO odoo.http: HTTP GET /web/dataset/call_kw 200 0.045s
2026-05-31 08:42:18,456 12346 WARNING odoo.addons.sale.models.order: Order SO-1234 warning: ...
2026-05-31 08:42:19,789 12347 ERROR odoo.sql_db: bad query: ...

Campos a extraer: timestamp, PID, nivel (INFO/WARNING/ERROR/CRITICAL), logger (módulo), mensaje, URL (si es petición HTTP), tiempo de respuesta, código HTTP.

2. Queries lentas de PostgreSQL

Activar log_min_duration_statement = 1000 en PostgreSQL para registrar todas las queries que tardan más de 1 segundo. Estas entradas en /var/log/postgresql/postgresql.log son críticas para detectar cuellos de botella de BD.

3. Workers y procesos Odoo

En modo multi-worker, Odoo lanza procesos hijos. Monitorizar cuántos workers están activos, cuántos están en estado idle vs ocupado, y si alguno se reinicia de forma anómala.

4. Cron jobs

Los trabajos programados de Odoo pueden fallar silenciosamente. Detectar errores en el log con el patrón cron o ir.cron en el logger.

5. Métricas de sistema

CPU, memoria, uso de disco, conexiones de red activas. Metricbeat (parte del stack Elastic) o node_exporter + Prometheus son buenas opciones complementarias.

Configuración de Filebeat en el servidor Odoo

# /etc/filebeat/filebeat.yml
filebeat.inputs:
  - type: log
    id: odoo-application
    enabled: true
    paths:
      - /var/log/odoo/odoo.log
    fields:
      service: odoo
      environment: production
    fields_under_root: true
    multiline.type: pattern
    multiline.pattern: '^\d{4}-\d{2}-\d{2}'
    multiline.negate: true
    multiline.match: after
    # Las trazas de error de Python son multi-línea; las agrupamos

  - type: log
    id: postgresql
    enabled: true
    paths:
      - /var/log/postgresql/postgresql-16-main.log
    fields:
      service: postgresql
      environment: production
    fields_under_root: true
    multiline.type: pattern
    multiline.pattern: '^\d{4}-\d{2}-\d{2}'
    multiline.negate: true
    multiline.match: after

output.logstash:
  hosts: ["10.0.2.10:5044"]
  ssl.certificate_authorities: ["/etc/filebeat/certs/ca.crt"]
  ssl.certificate: "/etc/filebeat/certs/filebeat.crt"
  ssl.key: "/etc/filebeat/certs/filebeat.key"

logging.level: warning
logging.to_files: true
logging.files:
  path: /var/log/filebeat

El bloque multiline es fundamental: los tracebacks de Python ocupan varias líneas, y sin agrupación cada línea del traceback se indexa como un evento separado, haciendo imposible la búsqueda.

Pipeline Logstash: parseo del formato de log de Odoo

# /etc/logstash/conf.d/odoo.conf
input {
  beats {
    port => 5044
    ssl => true
    ssl_certificate => "/etc/logstash/certs/logstash.crt"
    ssl_key => "/etc/logstash/certs/logstash.key"
    ssl_certificate_authorities => ["/etc/logstash/certs/ca.crt"]
  }
}

filter {
  if [service] == "odoo" {
    grok {
      match => {
        "message" => "%{TIMESTAMP_ISO8601:odoo_timestamp} %{NUMBER:pid:int} %{LOGLEVEL:log_level} %{NOTSPACE:logger}: %{GREEDYDATA:log_message}"
      }
      tag_on_failure => ["_grokparsefailure_odoo"]
    }

    # Parsear líneas HTTP con tiempo de respuesta
    if [logger] == "odoo.http" {
      grok {
        match => {
          "log_message" => "HTTP %{WORD:http_method} %{URIPATH:request_path} %{NUMBER:http_status:int} %{NUMBER:response_time_s:float}s"
        }
        tag_on_failure => ["_grok_http_failure"]
      }
      # Convertir tiempo a ms para facilitar alertas
      if [response_time_s] {
        ruby {
          code => "event.set('response_time_ms', (event.get('response_time_s').to_f * 1000).round)"
        }
      }
    }

    date {
      match => ["odoo_timestamp", "yyyy-MM-dd HH:mm:ss,SSS"]
      target => "@timestamp"
      timezone => "Europe/Madrid"
    }

    # Detectar queries lentas referenciadas en el log de Odoo
    if [log_message] =~ /slow query/ or [log_message] =~ /bad query/ {
      mutate { add_tag => ["slow_query"] }
    }

    # Clasificar severidad de negocio
    if [log_level] in ["ERROR", "CRITICAL"] {
      mutate { add_field => { "alert_severity" => "high" } }
    } else if [log_level] == "WARNING" {
      mutate { add_field => { "alert_severity" => "medium" } }
    }
  }

  if [service] == "postgresql" {
    grok {
      match => {
        "message" => "%{TIMESTAMP_ISO8601:pg_timestamp} %{WORD:pg_tz} \[%{NUMBER:pg_pid:int}\] %{WORD:pg_user}@%{WORD:pg_db} %{LOGLEVEL:log_level}: %{GREEDYDATA:log_message}"
      }
      tag_on_failure => ["_grokparsefailure_pg"]
    }
    if [log_message] =~ /duration:/ {
      grok {
        match => { "log_message" => "duration: %{NUMBER:pg_query_duration_ms:float} ms" }
      }
      if [pg_query_duration_ms] and [pg_query_duration_ms] > 5000 {
        mutate { add_tag => ["slow_query", "pg_slow_query"] }
      }
    }
  }

  mutate {
    remove_field => ["agent", "ecs", "input", "log"]
  }
}

output {
  elasticsearch {
    hosts => ["https://10.0.2.10:9200"]
    index => "odoo-logs-%{+YYYY.MM.dd}"
    user => "logstash_writer"
    password => "<LOGSTASH_PASSWORD>"
    ssl_certificate_verification => true
    cacert => "/etc/logstash/certs/ca.crt"
  }
}

Dashboards Kibana: qué visualizar

Una vez los logs están en Elasticsearch, Kibana permite crear dashboards operativos. Estos son los paneles más útiles para operaciones Odoo:

Dashboard 1: Estado general (vista de guardia)

  • Contador de errores por nivel en las últimas 24h (ERROR, CRITICAL, WARNING).
  • Evolución temporal de errores y warnings (gráfico de barras por hora).
  • Top 10 loggers con más errores (identificar el módulo problemático).
  • Tiempo de respuesta HTTP promedio y percentil 95 (p95 > 3s es señal de alerta).

Dashboard 2: Rendimiento de base de datos

  • Queries lentas por hora (PG queries > 1s, > 5s, > 30s).
  • Top 20 queries más lentas con su texto SQL truncado.
  • Usuarios/sesiones que generan más carga.

Dashboard 3: Workers y salud de procesos

  • Reinicios de workers (patrón: proceso con PID que desaparece y aparece uno nuevo).
  • Errores de cron (filtro por logger: ir.cron y log_level: ERROR).
  • Longpolling — conexiones activas (métrica de gevent).

Los dashboards se exportan como objetos NDJSON e importan en cualquier instancia Kibana con un clic, facilitando la replicación en entornos de staging.

Alertas proactivas vía bot de Telegram

Las alertas en Telegram son la capa que convierte la observabilidad pasiva en activa. El equipo recibe un mensaje instantáneo cuando se supera un umbral, sin necesidad de estar mirando Kibana.

Crear el bot de Telegram

  1. Buscar @BotFather en Telegram y ejecutar /newbot.
  2. Guardar el token (BOT_TOKEN).
  3. Unirse al canal o grupo de alertas y obtener el CHAT_ID con: curl https://api.telegram.org/bot<BOT_TOKEN>/getUpdates.

Script Python de alertas (ElastAlert alternativo ligero)

Para entornos pequeños o medianos, un script Python ejecutado vía cron cada minuto es más simple y transparente que ElastAlert completo:

#!/usr/bin/env python3
# /opt/odoo-monitor/alert_odoo.py
"""Monitor de alertas Odoo -> Telegram.
Ejecuta cada minuto via cron: * * * * * /opt/odoo-monitor/venv/bin/python /opt/odoo-monitor/alert_odoo.py
"""
import os
import json
import requests
from datetime import datetime, timedelta, timezone
from elasticsearch import Elasticsearch

ES_HOST = os.environ["ES_HOST"]          # https://10.0.2.10:9200
ES_USER = os.environ["ES_USER"]
ES_PASS = os.environ["ES_PASS"]
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
INDEX = "odoo-logs-*"

es = Elasticsearch(ES_HOST, basic_auth=(ES_USER, ES_PASS), verify_certs=True)

def send_telegram(message: str) -> None:
    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
    requests.post(url, json={
        "chat_id": CHAT_ID,
        "text": message,
        "parse_mode": "Markdown"
    }, timeout=10)

def count_errors_last_minute() -> int:
    now = datetime.now(timezone.utc)
    one_min_ago = now - timedelta(minutes=1)
    resp = es.count(index=INDEX, body={
        "query": {
            "bool": {
                "must": [
                    {"terms": {"log_level.keyword": ["ERROR", "CRITICAL"]}},
                    {"range": {"@timestamp": {"gte": one_min_ago.isoformat(), "lte": now.isoformat()}}}
                ]
            }
        }
    })
    return resp["count"]

def get_slow_queries_last_minute() -> list:
    now = datetime.now(timezone.utc)
    one_min_ago = now - timedelta(minutes=1)
    resp = es.search(index=INDEX, body={
        "size": 5,
        "query": {
            "bool": {
                "must": [
                    {"term": {"tags": "slow_query"}},
                    {"range": {"@timestamp": {"gte": one_min_ago.isoformat()}}}
                ]
            }
        },
        "sort": [{"pg_query_duration_ms": "desc"}],
        "_source": ["pg_query_duration_ms", "log_message", "@timestamp"]
    })
    return [h["_source"] for h in resp["hits"]["hits"]]

def main():
    # Alerta 1: demasiados errores en el último minuto
    error_count = count_errors_last_minute()
    if error_count >= 5:
        msg = (
            f"*\u26a0\ufe0f ALERTA ODOO — ERRORES EN PRODUCCI\u00d3N*\n"
            f"Se han detectado *{error_count} errores* en el \u00faltimo minuto.\n"
            f"Revisa Kibana: https://kibana.skanndar.internal/app/dashboards\n"
            f"`{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Europe/Madrid`"
        )
        send_telegram(msg)

    # Alerta 2: queries lentas (> 5s)
    slow_queries = get_slow_queries_last_minute()
    if slow_queries:
        top = slow_queries[0]
        duration_s = top.get("pg_query_duration_ms", 0) / 1000
        snippet = top.get("log_message", "")[:120].replace("`", "'")
        msg = (
            f"*\ud83d\udc22 QUERY LENTA EN POSTGRESQL*\n"
            f"Duraci\u00f3n: *{duration_s:.1f}s*\n"
            f"`{snippet}...`"
        )
        send_telegram(msg)

if __name__ == "__main__":
    main()

Guardar el script en /opt/odoo-monitor/alert_odoo.py, crear el entorno virtual con pip install elasticsearch requests y añadir al crontab del sistema:

# /etc/cron.d/odoo-monitor
* * * * * odoomonitor /opt/odoo-monitor/venv/bin/python /opt/odoo-monitor/alert_odoo.py

Las variables de entorno se gestionan vía un fichero .env cargado por el wrapper del cron o por systemd si se prefiere un servicio:

ES_HOST=https://10.0.2.10:9200
ES_USER=alert_reader
ES_PASS=<PASSWORD>
TELEGRAM_BOT_TOKEN=<TOKEN>
TELEGRAM_CHAT_ID=<CHAT_ID>

Tipos de alertas recomendadas para Odoo

CondiciónUmbralSeveridadAcción
Errores CRITICAL en 1 min≥ 1CríticaTelegram inmediato + PagerDuty
Errores ERROR en 1 min≥ 5AltaTelegram inmediato
Query PG > 30 sCualquieraAltaTelegram con snippet SQL
Tiempo respuesta HTTP p95 > 5 s3 min sostenidoAltaTelegram
Worker reiniciadoCualquieraMediaTelegram
Fallo de cron job≥ 2 en 10 minMediaTelegram
Query PG > 5 s≥ 10 en 5 minMediaTelegram (digest cada 15 min)
Disco > 85 %CualquieraMediaTelegram
Sin logs de Odoo en 5 minAusencia de eventosCríticaTelegram (Odoo caído)

La última regla —alertar si no llegan logs— es especialmente valiosa: detecta cuando Odoo o Filebeat se han caído sin generar ningún error explícito.

Buenas prácticas de observabilidad en Odoo

Retención y costes de almacenamiento

Los logs de Odoo en producción pueden generar entre 500 MB y 5 GB diarios dependiendo del nivel de logging. Definir una política de retención (ILM en Elasticsearch) con tres fases: caliente (7 días, SSD), tibia (30 días, HDD), fría (90 días, comprimido o S3). Para instalaciones medianas, 3 meses de retención caben en menos de 100 GB.

No loguear a nivel DEBUG en producción

El nivel log_level = debug en odoo.conf genera un volumen de datos 10-50x mayor e incluye información sensible (valores de campos, tokens). Usar warn o info en producción. Activar debug sólo de forma temporal y sobre bases de datos de test.

Rotar los logs de Odoo con logrotate

# /etc/logrotate.d/odoo
/var/log/odoo/odoo.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        /bin/kill -HUP $(cat /var/run/odoo/odoo.pid 2>/dev/null) 2>/dev/null || true
    endscript
}

Separar métricas de negocio de métricas de sistema

ELK es idóneo para logs y búsqueda de texto. Para métricas de series temporales (CPU, conexiones PG, tiempo de respuesta promedio), Prometheus + Grafana escala mejor y consume menos recursos. Una arquitectura madura combina ambas: ELK para logs y análisis forense, Prometheus/Grafana para métricas y alertas de rendimiento.

Alertas agrupadas, no individuales

Si Odoo tiene un bug que genera 1.000 errores en un minuto, no queremos recibir 1.000 mensajes en Telegram. El script de ejemplo agrupa: envía un único mensaje con el recuento. Para alertas más sofisticadas, ElastAlert soporta frequency, spike, flatline y cardinality como tipos de regla, lo que permite patrones complejos sin escribir código.

Securizar el stack ELK

Desde Elasticsearch 8.x, la seguridad básica está activada por defecto (TLS entre nodos, autenticación obligatoria). En versiones anteriores era opt-in y muchas instalaciones quedaron expuestas. Verificar siempre que Elasticsearch no es accesible desde internet en el puerto 9200 y que Kibana requiere autenticación.

Resultado: lo que verás en producción

Con esta arquitectura en marcha, el equipo de operaciones tiene:

  • Un dashboard en Kibana que muestra el estado de Odoo en tiempo real, con drill-down hasta el mensaje de error exacto en segundos.
  • Alertas en Telegram que llegan antes de que el usuario llame, con contexto suficiente para empezar a diagnosticar sin abrir SSH.
  • Historial de 90 días que permite análisis de tendencias: “¿las queries lentas aumentan los martes por el cron de facturación?”, “¿desde qué versión de módulo empezaron los errores?”.
  • Evidencia objetiva para decisiones de optimización: saber que el 80% de los errores provienen de un único módulo custom cambia las prioridades del sprint.

¿Quieres implementar observabilidad real en tu Odoo?

Solicitar auditoría técnica gratuita

¿Cuánto cuesta implantar Odoo en España? Guía honesta de precios
Rangos reales, factores de coste y los errores que disparan el presupuesto en proyectos ERP