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.cronilog_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
- Cerca
@BotFathera Telegram i executa/newbot. - Desa el token (
BOT_TOKEN). - Uneix-te al canal o grup d'alertes i obtén el
CHAT_IDamb: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ó | Llindar | Severitat | Acció |
|---|---|---|---|
| Errors CRITICAL en 1 min | ≥ 1 | Crítica | Telegram immediat + PagerDuty |
| Errors ERROR en 1 min | ≥ 5 | Alta | Telegram immediat |
| Query PG > 30 s | Qualsevol | Alta | Telegram amb snippet SQL |
| Temps de resposta HTTP p95 > 5 s | 3 min sostingut | Alta | Telegram |
| Worker reiniciat | Qualsevol | Mitjana | Telegram |
| Fallada de cron job | ≥ 2 en 10 min | Mitjana | Telegram |
| Query PG > 5 s | ≥ 10 en 5 min | Mitjana | Telegram (resum cada 15 min) |
| Disc > 85 % | Qualsevol | Mitjana | Telegram |
| Sense logs d'Odoo en 5 min | Absència d'esdeveniments | Crítica | Telegram (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.