Monitorització d'Odoo amb ELK Stack i alertes de Telegram

Com centralitzar logs i mètriques d'Odoo amb Elasticsearch, Logstash/Filebeat i Kibana, analitzar el format de log propi d'Odoo i rebre alertes proactives en un bot de Telegram quan alguna cosa falla.

Per què l'observabilitat a Odoo marca la diferència

La majoria d'implantacions Odoo es gestionen de forma reactiva: algú truca dient que Odoo va lent, s'obre una sessió SSH, es mira el log amb tail -f i s'intenta deduir què ha passat. Aquest enfocament té un problema fonamental: quan t'assabentes del problema, el dany ja està fet. Una consulta que tarda 45 segons no fa saltar cap alarma fins que el servidor se satura. Un worker que es mor silenciosament deixa els usuaris sense servei sense que ningú ho sàpiga.

L'observabilitat proactiva —centralitzar logs, mesurar mètriques, definir llindars i rebre alertes abans que l'usuari es queixi— és la diferència entre un sistema gestionat i un administrat per crisi. Aquesta guia descriu l'arquitectura que hem implementat en producció a diversos clients, incloent Rehabmedic, on la combinació d'ELK Stack i alertes a Telegram ens va permetre detectar i resoldre incidències abans que impactessin en el negoci.

Arquitectura d'observabilitat: visió 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)  │
                    └──────────────────┘

Els components són:

  • Filebeat: agent instal·lat al servidor Odoo. Llegeix els fitxers de log i els envia a Logstash amb un consum mínim de recursos.
  • Logstash: pipeline de processament. Analitza els logs d'Odoo (format propi), extreu camps estructurats, enriqueix amb metadades i normalitza.
  • Elasticsearch: base de dades de cerca i analítica on s'emmagatzemen tots els esdeveniments indexats.
  • Kibana: interfície web per a l'exploració, dashboards i configuració d'alertes (Watcher o Kibana Alerting).
  • ElastAlert / script Python: motor d'alertes que avalua condicions sobre Elasticsearch i dispara notificacions a Telegram.

Quines dades recollir d'Odoo

Odoo genera diversos fluxos d'esdeveniments que convé monitoritzar de forma diferenciada:

1. Log d'aplicació Odoo (/var/log/odoo/odoo.log)

És la font principal. El format per defecte d'Odoo és:

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: ...

Camps a extreure: timestamp, PID, nivell (INFO/WARNING/ERROR/CRITICAL), logger (mòdul), missatge, URL (si és una petició HTTP), temps de resposta, codi HTTP.

2. Consultes lentes de PostgreSQL

Activa log_min_duration_statement = 1000 a PostgreSQL per registrar totes les consultes que triguen més d'1 segon. Aquestes entrades a /var/log/postgresql/postgresql.log són crítiques per detectar colls d'ampolla de BD.

3. Workers i processos Odoo

En mode multi-worker, Odoo llança processos fills. Monitoritza quants workers estan actius, quants estan en estat inactiu vs ocupat, i si algun es reinicia de forma anòmala.

4. Cron jobs

Els treballs programats d'Odoo poden fallar silenciosament. Detecta errors al log amb el patró cron o ir.cron al logger.

5. Mètriques de sistema

CPU, memòria, ús de disc, connexions de xarxa actives. Metricbeat (part de l'stack Elastic) o node_exporter + Prometheus són bones opcions complementàries.

Configuració de Filebeat al 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 bloc multiline és fonamental: els tracebacks de Python ocupen diverses línies i, sense agrupació, cada línia del traceback s'indexa com un esdeveniment separat, fent la cerca impossible.

Pipeline Logstash: anàlisi del format de log d'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è visualitzar

Un cop els logs estan a Elasticsearch, Kibana permet crear dashboards operatius. Aquests són els panells més útils per a les operacions d'Odoo:

Dashboard 1: Estat general (vista de guàrdia)

  • Comptador d'errors per nivell en les darreres 24 h (ERROR, CRITICAL, WARNING).
  • Evolució temporal d'errors i warnings (gràfic de barres per hora).
  • Top 10 loggers amb més errors (identificar el mòdul problemàtic).
  • Temps de resposta HTTP mitjà i percentil 95 (p95 > 3s és senyal d'alerta).

Dashboard 2: Rendiment de base de dades

  • Consultes lentes per hora (PG queries > 1s, > 5s, > 30s).
  • Top 20 consultes més lentes amb el seu text SQL truncat.
  • Usuaris/sessions que generen més càrrega.

Dashboard 3: Workers i salut de processos

  • Reinicis de workers (patró: procés amb PID que desapareix i n'apareix un de nou).
  • Errors de cron (filtre per logger: ir.cron i log_level: ERROR).
  • Longpolling — connexions actives (mètrica de gevent).

Els dashboards s'exporten com a objectes NDJSON i s'importen en qualsevol instància de Kibana amb un clic, facilitant la replicació en entorns de staging.

Alertes proactives via bot de Telegram

Les alertes a Telegram són la capa que converteix l'observabilitat passiva en activa. L'equip rep un missatge instantani quan se supera un llindar, sense necessitat d'estar mirant Kibana.

Crear el bot de Telegram

  1. Cerca @BotFather a Telegram i executa /newbot.
  2. Desa el token (BOT_TOKEN).
  3. Uneix-te al canal o grup d'alertes i obtén el CHAT_ID amb: curl https://api.telegram.org/bot<BOT_TOKEN>/getUpdates.

Script Python d'alertes (alternativa lleugera a ElastAlert)

Per a entorns petits o mitjans, un script Python executat via cron cada minut és més senzill i transparent que ElastAlert complet:

#!/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()

Desa el script a /opt/odoo-monitor/alert_odoo.py, crea l'entorn virtual amb pip install elasticsearch requests i afegeix-lo al crontab del sistema:

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

Les variables d'entorn es gestionen via un fitxer .env carregat pel wrapper del cron o per systemd si es prefereix un servei:

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

Tipus d'alertes recomanades per a Odoo

CondicióLlindarSeveritatAcció
Errors CRITICAL en 1 min≥ 1CríticaTelegram immediat + PagerDuty
Errors ERROR en 1 min≥ 5AltaTelegram immediat
Query PG > 30 sQualsevolAltaTelegram amb snippet SQL
Temps de resposta HTTP p95 > 5 s3 min sostingutAltaTelegram
Worker reiniciatQualsevolMitjanaTelegram
Fallada de cron job≥ 2 en 10 minMitjanaTelegram
Query PG > 5 s≥ 10 en 5 minMitjanaTelegram (resum cada 15 min)
Disc > 85 %QualsevolMitjanaTelegram
Sense logs d'Odoo en 5 minAbsència d'esdevenimentsCríticaTelegram (Odoo caigut)

L'última regla —alertar si no arriben logs— és especialment valuosa: detecta quan Odoo o Filebeat s'han caigut sense generar cap error explícit.

Bones pràctiques d'observabilitat a Odoo

Retenció i costos d'emmagatzematge

Els logs d'Odoo en producció poden generar entre 500 MB i 5 GB diaris depenent del nivell de logging. Defineix una política de retenció (ILM a Elasticsearch) amb tres fases: calenta (7 dies, SSD), tèbia (30 dies, HDD), freda (90 dies, comprimit o S3). Per a instal·lacions mitjanes, 3 mesos de retenció caben en menys de 100 GB.

No logar a nivell DEBUG en producció

El nivell log_level = debug a odoo.conf genera un volum de dades 10–50× superior i inclou informació sensible (valors de camps, tokens). Utilitza warn o info en producció. Activa debug només de forma temporal i sobre bases de dades de prova.

Rotar els logs d'Odoo amb 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ètriques de negoci de mètriques de sistema

ELK és idoni per a logs i cerca de text. Per a mètriques de sèries temporals (CPU, connexions PG, temps de resposta mitjà), Prometheus + Grafana escala millor i consumeix menys recursos. Una arquitectura madura combina ambdues: ELK per a logs i anàlisi forense, Prometheus/Grafana per a mètriques i alertes de rendiment.

Alertes agrupades, no individuals

Si Odoo té un bug que genera 1.000 errors en un minut, no volem rebre 1.000 missatges a Telegram. L'script d'exemple agrupa: envia un únic missatge amb el recompte. Per a alertes més sofisticades, ElastAlert suporta frequency, spike, flatline i cardinality com a tipus de regla, cosa que permet patrons complexos sense escriure codi.

Securitzar l'stack ELK

Des d'Elasticsearch 8.x, la seguretat bàsica està activada per defecte (TLS entre nodes, autenticació obligatòria). En versions anteriors era opt-in i moltes instal·lacions van quedar exposades. Verifica sempre que Elasticsearch no és accessible des d'internet al port 9200 i que Kibana requereix autenticació.

Resultat: el que veuràs en producció

Amb aquesta arquitectura en marxa, l'equip d'operacions disposa de:

  • Un dashboard a Kibana que mostra l'estat d'Odoo en temps real, amb drill-down fins al missatge d'error exacte en segons.
  • Alertes a Telegram que arriben abans que l'usuari truchi, amb prou context per començar a diagnosticar sense obrir SSH.
  • Historial de 90 dies que permet anàlisi de tendències: "les consultes lentes augmenten els dimarts pel cron de facturació?", "des de quina versió de mòdul van començar els errors?".
  • Evidència objectiva per a decisions d'optimització: saber que el 80% dels errors provenen d'un únic mòdul personalitzat canvia les prioritats del sprint.

Vols implementar observabilitat real al teu Odoo?

Sol·licitar auditoria tècnica gratuïta

Quant costa implantar Odoo a Espanya? Guia honesta de preus
Rangs de preus reals, factors de cost i els errors que fan disparar el pressupost en projectes ERP