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.cronylog_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
- Buscar
@BotFatheren Telegram y ejecutar/newbot. - Guardar el token (
BOT_TOKEN). - Unirse al canal o grupo de alertas y obtener el
CHAT_IDcon: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ón | Umbral | Severidad | Acción |
|---|---|---|---|
| Errores CRITICAL en 1 min | ≥ 1 | Crítica | Telegram inmediato + PagerDuty |
| Errores ERROR en 1 min | ≥ 5 | Alta | Telegram inmediato |
| Query PG > 30 s | Cualquiera | Alta | Telegram con snippet SQL |
| Tiempo respuesta HTTP p95 > 5 s | 3 min sostenido | Alta | Telegram |
| Worker reiniciado | Cualquiera | Media | Telegram |
| Fallo de cron job | ≥ 2 en 10 min | Media | Telegram |
| Query PG > 5 s | ≥ 10 en 5 min | Media | Telegram (digest cada 15 min) |
| Disco > 85 % | Cualquiera | Media | Telegram |
| Sin logs de Odoo en 5 min | Ausencia de eventos | Crítica | Telegram (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.