# FIX 2026-08-31 — logrotate: duplicados, conf `asterisk` rota por CRLF y daemons escribiendo en inodos rotados

**Fecha**: 2026-08-31
**Reporte**: al crear `/etc/logrotate.d/asterisk-wav-conversion` (FIX_2026-08-31_cron_wav_trunca_grabaciones)
se vio que `logrotate -d /etc/logrotate.conf` escupía 15 líneas `error: ... duplicate log entry` y
`found error in file X, skipping` para seis ficheros de `/etc/logrotate.d/`. David: "arregla logrotate".
**Estado**: RESUELTO y verificado en producción (0 errores en `logrotate -d`, 468 logs con un único
dueño, rotación forzada de los ficheros grandes, `lsof +L1` sin inodos borrados abiertos).
**Backup**: `/root/backup-logrotate-20260831.tar.gz` (md5 `be0f840cd0a2e42712019d20099ab7ff`, copia en
`C:\Users\A1\deselpa-inv\`). **Rollback completo**: `tar xzf /root/backup-logrotate-20260831.tar.gz -C /`
(restaura `/etc/logrotate.d`, `/etc/logrotate.conf` y `/var/lib/logrotate/status`; borrar después
`/etc/logrotate.d/centralita-misc`, que es nuevo).

---

## Lo que había de verdad (el diagnóstico inicial era impreciso)

Ubuntu 24.10, logrotate 3.22.0, `logrotate.timer` diario (00:53 + jitter; `/etc/cron.daily/logrotate`
sale solo si hay systemd, así que no hay doble ejecución).

1. **`duplicate log entry` NO descarta el fichero entero** en logrotate 3.22. El mensaje
   `found error in file X, skipping` engaña: solo se descarta la *ruta duplicada* dentro del bloque
   posterior; el resto del bloque y del fichero se procesa. Evidencia: `-d` mostraba `Handling 33 logs`
   con los patrones de `asterisk-logs`, `hispatel-origen`, `panel-sync-asterisk`, `php-errors` y
   `rsyslog`, y en el bloque 1 de `asterisk-logs` faltaban justo `state_manager*.log` y
   `monitor_pending_events.log` (los que ya tenía `aggressive-rotation`, que se lee antes).
   Consecuencia práctica: **gana el primer fichero por orden alfabético**, y `aggressive-rotation`
   iba primero.
2. **El único fichero realmente descartado era `/etc/logrotate.d/asterisk`, y no por duplicados sino
   porque tenía finales de línea CRLF** (`asterisk:1 lines must begin with a keyword or a filename`).
   Probado: con `tr -d '\r'` parseaba. Además se duplicaba a sí mismo (glob `*.log` + bloques
   explícitos de `state_manager*.log` y `python_call_system.log`) y su bloque `debug_whisper` apuntaba
   a un fichero inexistente. Nunca ha rotado nada desde que existe (2025-10-27).
3. **`aggressive-rotation` se llevaba `syslog`** con su bloque propio y anulaba la conf del paquete
   `rsyslog` para ese fichero (los demás — auth, kern, mail... — seguían con la conf del paquete).
4. **Dos daemons llevaban 20 días escribiendo en el `.1`**: `panel-sync-asterisk.service` y
   `websocket-monitor.service` tienen `StandardOutput=append:` (fd abierto por systemd). El bloque
   `panel_tarot/*.log` de `aggressive-rotation` (create+rename, sin copytruncate) ganaba al
   `panel-sync-asterisk` con copytruncate del FIX 2026-06-10, que por tanto **no estaba en vigor**.
   `sync_estados.log` llevaba 0 bytes desde el 11-08 y `sync_estados.log.1` iba por **288 MB** y
   creciendo (y como el `.log` estaba vacío, `notifempty` impedía rotarlo nunca más).
5. **Un inodo borrado de 135 MB**: `ami-dnd-listener.service` (`ami_dnd_listener.py`, arrancado el
   11-12-2025) escribe en `python_call_system.log` vía `logging.FileHandler` (fd permanente). El bloque
   de `asterisk-logs` lo rotaba con create+rename → `lsof +L1` mostraba
   `python_call_system.log (deleted)` con 135 MB.
6. **`asterisk-custom` hacía `systemctl reload asterisk` cada noche** para rotar `messages`.
   `asterisk.service` viene del generador SysV: `/etc/init.d/asterisk reload` = `asterisk -rx 'module
   reload'`, es decir, un **reload completo de módulos en producción a las 00:53 todos los días**,
   cuando basta `logger reload` (lo que ya hacía `asterisk-verbose` para full/verbose/debug).
7. **Logs sin ningún dueño**: `/var/log/cleanup_stuck_calls.log` (149 MB, cron cada 3 min),
   `recordings_archive.log` (129 MB), `recordings_archive_cron.log` (119 MB), `auto_convert.log` (66 MB
   en /var/log/asterisk), `monitor_grabaciones.log`, `register_parking.log`, `system_monitor.log`,
   `disk_*`, `droplet-agent.update.log`...
8. `wav_conversion.log.1` (519 MB) y `wav_conversion_cron.log.1` (435 MB), rotados hoy a las 14:17 por
   la conf nueva, seguían sin comprimir (`delaycompress`, semanal).

Quién escribe cada cosa (determina copytruncate vs postrotate):

| Log | Escritor | Modo | Estrategia |
|-----|----------|------|-----------|
| asterisk/messages, full, verbose, debug | Asterisk PID 985 (logger.conf) | fd permanente, reabre con `logger reload` | create + postrotate `asterisk -rx 'logger reload'` |
| asterisk/state_manager*.log | cron cada 15 s `>>` | procesos cortos | create (default) |
| asterisk/monitor_pending_events.log | `monitor-pending-events.timer` → `/root/monitor_pending_events.py` | proceso corto | create |
| asterisk/python_call_system.log | AGIs cortos **+ `ami-dnd-listener.service` (daemon, FileHandler)** | fd permanente | **copytruncate** |
| asterisk/cleanup_*, coordinator_*, parking_cleanup, monitor_grabaciones, register_parking, auto_convert | crons `>>` / AGI por llamada | cortos | copytruncate (mismo bloque) |
| panel_tarot/sync_estados.log | `panel-sync-asterisk.service` `StandardOutput=append:` | fd permanente (systemd) | **copytruncate** |
| panel_tarot/timeout_monitor.log | `timeout_monitor.py` PID 990 (daemon) | fd permanente | **copytruncate** |
| panel_tarot/*.log resto | crons `>>` (PHP) | cortos | copytruncate (mismo glob) |
| websocket-monitor.log | `websocket-monitor.service` `StandardOutput=append:` | fd permanente | **copytruncate** |
| php_errors.log | PHP (abre/cierra por mensaje) | — | create 0644 www-data |
| syslog | rsyslogd | fd permanente, HUP | conf del paquete `rsyslog` (postrotate `rsyslog-rotate`) |
| pbx/hispatel_origen_purga.log, origen_hispatel_rechazos.log | cron.d + PHP web | cortos | `hispatel-origen` (mensual, 24) |
| cleanup_stuck_calls, recordings_archive*, disk_*, system_monitor, droplet-agent.update | crons de root `>>`, scripts largos, systemd append | mixto | **copytruncate** (`centralita-misc`) |

## Mapa antes / después

| Log(s) | ANTES: confs que lo declaraban (negrita = la que ganaba) | DESPUÉS: dueño único |
|--------|-----------------------------------------------------------|----------------------|
| `/var/log/syslog` | **aggressive-rotation** (size 10M, 24), rsyslog | `rsyslog` (paquete: weekly, 4, HUP) |
| `asterisk/state_manager*.log` | **aggressive-rotation**, asterisk (roto), asterisk-logs | `aggressive-rotation` (size 5M, 12) |
| `asterisk/monitor_pending_events.log` | **aggressive-rotation**, asterisk (roto), asterisk-logs | `aggressive-rotation` (size 2M, 6) |
| `php_errors.log` | **aggressive-rotation**, php-errors | `aggressive-rotation` (size 5M, 12, 0644 www-data) |
| `panel_tarot/*.log` (401 ficheros) | **aggressive-rotation** (create), asterisk-logs, panel-sync-asterisk (solo sync_estados, copytruncate) | `aggressive-rotation` (size 5M, 12, **copytruncate**) |
| `asterisk/python_call_system.log` | **asterisk-logs** (create), asterisk (roto, 2 veces) | `asterisk-logs` (daily, 14, maxsize 100M, **copytruncate**) |
| `asterisk/cleanup_*.log`, `coordinator_*.log`, `sync_agentes_tarotistas.log`, `parking_cleanup.log`, `cron_call_check.log` | **asterisk-logs**, asterisk (roto) | `asterisk-logs` |
| `asterisk/auto_convert.log`, `monitor_grabaciones.log`, `register_parking.log` | asterisk (roto) → **nadie** | `asterisk-logs` |
| `asterisk/wav_conversion*.log` | asterisk-wav-conversion (y asterisk roto) | `asterisk-wav-conversion` (sin cambios) |
| `asterisk/messages` | asterisk-custom (postrotate `module reload`) | `asterisk-custom` (postrotate **`logger reload`**) |
| `asterisk/full`, `verbose`, `debug` | asterisk-verbose | `asterisk-verbose` (sin cambios) |
| `websocket-monitor.log` | **asterisk-logs** (create) | `asterisk-logs` (**copytruncate**) |
| `sync_tarotistas_realtime.log`, `pbx/unified.log` | asterisk-logs | nadie (0 bytes desde 10-2025, muertos) |
| `pbx/hispatel_origen_purga.log`, `origen_hispatel_rechazos.log` | **asterisk-logs** (glob pbx/*.log, daily 14), hispatel-origen | `hispatel-origen` (monthly, 24) |
| `cleanup_stuck_calls.log`, `recordings_archive.log`, `recordings_archive_cron.log`, `system_monitor.log`, `disk_emergency_monitor.log`, `disk_cleanup.log`, `disk-space-monitor.log`, `droplet-agent.update.log`, `agent_metrics.log`, `backup_tarot.log`, `coordinator_calculations/daily_cron.log` | **nadie** | `centralita-misc` (nuevo: weekly, 8, maxsize 100M, copytruncate, compress inmediato) |
| 15 `.log` muertos de 2025 en /var/log/asterisk (callcenter*, call_sync*, fix_ringing_calls, migration, test_agi...) | asterisk (roto) → nadie | nadie, a propósito (inactivos, < 1 MB) |

Ficheros de `/etc/logrotate.d/`: **eliminados** `asterisk` (CRLF, todo lo suyo tiene otro dueño),
`panel-sync-asterisk` (absorbido en aggressive-rotation), `php-errors` (duplicado). **Nuevo**
`centralita-misc`. **Editados** `aggressive-rotation`, `asterisk-logs`, `asterisk-custom`,
`asterisk-wav-conversion` (solo el comentario). **Intactos** `rsyslog`, `asterisk-verbose`,
`hispatel-origen`, `apache2`, `php8.3-fpm`, `fail2ban` y los del sistema.

Criterio elegido (por qué lista explícita en vez de glob): un glob `*.log` en `/var/log/asterisk` no
puede excluir nada, y la carpeta mezcla logs con cuatro estrategias distintas (core Asterisk con
`logger reload`, tamaño en aggressive, semanal en wav, daemons con copytruncate). Con lista explícita
cada log tiene un dueño evidente y `-d` no da duplicados. Contrapartida: un log nuevo en
`/var/log/asterisk` no rota hasta añadirlo a `asterisk-logs` (comentario en el fichero).

## Diffs aplicados

```diff
--- /etc/logrotate.d/aggressive-rotation
+# Rotacion "agresiva" por tamano, creada 2025-10-29 ... (cabecera explicando dueño único,
+#  syslog fuera, panel_tarot copytruncate, "hourly" sin efecto con el timer diario)
-# System logs - rotate hourly when >10MB
-/var/log/syslog {
-    hourly
-    rotate 24
-    size 10M
-    compress
-    delaycompress
-    missingok
-    notifempty
-    postrotate
-        /usr/lib/rsyslog/rsyslog-rotate
-    endscript
-}
 /var/log/panel_tarot/*.log {
     hourly
     rotate 12
     size 5M
     compress
     delaycompress
     missingok
     notifempty
+    copytruncate
 }
```

```diff
--- /etc/logrotate.d/asterisk-logs
-/var/log/asterisk/cleanup_*.log /var/log/asterisk/coordinator_*.log /var/log/asterisk/state_manager*.log /var/log/asterisk/python_call_system.log /var/log/asterisk/sync_agentes_tarotistas.log /var/log/asterisk/monitor_pending_events.log /var/log/asterisk/parking_cleanup.log /var/log/asterisk/cron_call_check.log {
+# (cabecera: qué NO incluir aquí porque ya tiene dueño)
+/var/log/asterisk/cleanup_*.log
+/var/log/asterisk/coordinator_*.log
+/var/log/asterisk/python_call_system.log
+/var/log/asterisk/sync_agentes_tarotistas.log
+/var/log/asterisk/parking_cleanup.log
+/var/log/asterisk/cron_call_check.log
+/var/log/asterisk/auto_convert.log
+/var/log/asterisk/monitor_grabaciones.log
+/var/log/asterisk/register_parking.log
+{
     daily
     missingok
     rotate 14
     compress
     delaycompress
     notifempty
-    create 644 root root
     maxsize 100M
-    sharedscripts
-    postrotate
-        /usr/bin/logger -t asterisk-logs "Rotated custom Asterisk logs"
-    endscript
+    copytruncate
 }
-/var/log/panel_tarot/*.log { daily ... rotate 14 ... create 644 root root maxsize 50M }   (dueño: aggressive-rotation)
-/var/log/pbx/*.log { daily ... rotate 14 ... maxsize 50M }                                (dueño: hispatel-origen)
-/var/log/websocket-monitor.log /var/log/sync_tarotistas_realtime.log {
+/var/log/websocket-monitor.log {
     daily
     missingok
     rotate 14
     compress
     delaycompress
     notifempty
-    create 644 root root
     maxsize 50M
+    copytruncate
 }
```

```diff
--- /etc/logrotate.d/asterisk-custom
 /var/log/asterisk/messages {
     ...
     postrotate
-        systemctl reload asterisk 2>/dev/null || true
+        /usr/sbin/asterisk -rx 'logger reload' > /dev/null 2>&1 || true
     endscript
 }
```

```diff
--- /etc/logrotate.d/asterisk-wav-conversion   (solo comentario)
-# NOTA: /etc/logrotate.d/asterisk (glob /var/log/asterisk/*.log) esta descartado entero
-# por logrotate (duplicados con aggressive-rotation); si se arregla, este fichero pasaria
-# a ser duplicado y habria que excluir wav_conversion* alli.
+# NOTA 2026-08-31: /etc/logrotate.d/asterisk (glob *.log, roto por CRLF) se ha eliminado; este
+# fichero es el UNICO dueno de wav_conversion*.log (ver asterisk-logs, que los excluye
+# expresamente). Ver docs/FIX_2026-08-31_logrotate_duplicados.md.
```

Nuevo `/etc/logrotate.d/centralita-misc`:

```
/var/log/cleanup_stuck_calls.log
/var/log/recordings_archive.log
/var/log/recordings_archive_cron.log
/var/log/system_monitor.log
/var/log/disk_emergency_monitor.log
/var/log/disk_cleanup.log
/var/log/disk-space-monitor.log
/var/log/droplet-agent.update.log
/var/log/agent_metrics.log
/var/log/backup_tarot.log
/var/log/coordinator_calculations/daily_cron.log
{
    weekly
    missingok
    rotate 8
    compress
    notifempty
    maxsize 100M
    copytruncate
}
```

Eliminados: `/etc/logrotate.d/asterisk`, `/etc/logrotate.d/panel-sync-asterisk`,
`/etc/logrotate.d/php-errors` (están en el tar de backup).

## Verificación de la configuración

```
logrotate -d /etc/logrotate.conf 2>&1 | grep -Eci '^error|duplicate'   → 0
grep -l $'\r' /etc/logrotate.d/*                                        → ninguno
Handling 29 logs (antes 33: los 4 de menos son los bloques duplicados/muertos)
```

OJO: `grep -ci 'duplicate\|error'` a secas da 21 falsos positivos porque casa con `php_errors.log`
y `apache2/error.log`; el filtro correcto es `^error`.

Cruce log → dueño (script: `considering log` de `-d` vs. `ls` de `/var/log/asterisk/*.log`,
`/var/log/panel_tarot/*.log`, `/var/log/pbx/*.log`, core de Asterisk, syslog, php_errors,
websocket-monitor y los de `centralita-misc`): **468 logs considerados, 0 aparecen dos veces**, y los
únicos sin dueño son los 15 `.log` muertos de 2025 de `/var/log/asterisk` + `pbx/unified.log` (0 B).

## Corrección en caliente de los daemons y primera rotación (16:03–16:04)

1. `sync_estados.log` y `websocket-monitor.log`: en vez de reiniciar los servicios (websocket-monitor
   tenía 64 conexiones establecidas del panel), se borró el `.log` vacío (0 B) y se renombró el `.1`
   al nombre canónico: el daemon sigue escribiendo en el mismo inodo, que ahora se llama
   `sync_estados.log`, y copytruncate ya funciona. `lsof` confirma `php 3650956 → sync_estados.log`,
   `php 3651137 → websocket-monitor.log`.
2. `ami-dnd-listener`: copia del inodo borrado (`cp /proc/1415/fd/3`) guardada comprimida en
   `/var/log/asterisk/python_call_system.log.inodo-borrado-20260831.gz` (12,4 MB) y
   `systemctl restart ami-dnd-listener` (Restart=always; arriba en 3 s, PID 629723, escribe ya en
   `python_call_system.log`: `=== Iniciando AMI DND Listener ===`).
3. Rotación forzada SOLO de las confs afectadas, con confs temporales `su root adm` + `include`
   (no de todo `/etc/logrotate.conf`, que habría rotado 379 `panel_YYYY-MM-DD.log`):
   `asterisk-wav-conversion`, `centralita-misc` y un bloque suelto para `sync_estados.log` (dos pasadas
   para que el `.1` de 288 MB pasara a `.2.gz`).

| Fichero | Antes | Después |
|---------|------:|--------:|
| `asterisk/wav_conversion.log.1` | 519 MB sin comprimir | `.2.gz` 36,9 MB |
| `asterisk/wav_conversion_cron.log.1` | 435 MB sin comprimir | `.2.gz` 31,5 MB |
| `panel_tarot/sync_estados.log.1` (daemon escribiendo dentro) | 288 MB | `.2.gz` 15,3 MB; `.log` vivo, crece |
| `python_call_system.log (deleted)` | 135 MB en inodo sin nombre | 0 (copia `.inodo-borrado-20260831.gz` 12,4 MB) |
| `cleanup_stuck_calls.log` | 149 MB | `.1.gz` 2,5 MB |
| `recordings_archive.log` | 129 MB | `.1.gz` 10,6 MB |
| `recordings_archive_cron.log` | 119 MB | `.1.gz` 9,8 MB |
| `du -sh /var/log` | 2,5 GB | 1,1 GB |
| `df /` | 18 GB usados (12 %) | 16 GB (11 %) |

Nada borrado: todo el histórico está comprimido en sitio.

Comprobaciones posteriores: `lsof +L1 | grep /var/log` → vacío; los tres daemons escriben en el
fichero nuevo y este crece; `asterisk -rx 'core show version'` OK; `asterisk -rx 'logger reload'`
(el postrotate nuevo) ejecutado a mano → Asterisk sigue escribiendo en `full` y `messages`
(`lsof` fd 28w/29w); `logrotate.timer` activo, próximo disparo 2026-09-01 00:53 CEST.

## Qué vigilar mañana (primer ciclo real, 2026-09-01 ~00:53)

```
journalctl -u logrotate.service --since today
grep -E 'python_call_system|websocket-monitor|sync_estados|asterisk/messages|/var/log/syslog"' /var/lib/logrotate/status
lsof -nP +L1 | grep /var/log          # debe seguir vacío
ls -la /var/log/asterisk/python_call_system.log* /var/log/asterisk/messages*
```

`syslog` pasa de "diario si >10 MB, 24 copias" a la política del paquete (semanal, 4 copias, ~100 MB
sin comprimir el `.1`): irrelevante con 139 GB libres. Si se prefiere diario, lo limpio es cambiar
`weekly` por `daily` en `/etc/logrotate.conf` (global) y no tocar la conf del paquete (es conffile y
saltaría en las actualizaciones). No se ha tocado.

## Pendientes / hallazgos colaterales (no tocados)

- **`/etc/cron.d/asterisk-cleanup`** — RESUELTO 2026-08-31 21:35: desde su creacion
  (2025-07-09 21:55) no terminaba en salto de linea y cron lo rechazaba ENTERO (verificado en
  syslog: `ERROR (Missing newline before EOF, this crontab file will be ignored)`), asi que sus
  4 tareas no corrieron NUNCA desde cron. Saneado: newline anadido y las 4 lineas comentadas
  con nota fechada — (1) cleanup_orphaned_calls cada 15 min: redundante, el crontab de root ya
  lo lanza cada 3 min desde 2025-11-05; (2)+(4) rotaciones caseras `find -size +10M -mv .old`:
  pisarian a logrotate (asterisk-logs ya rota cleanup_orphaned_calls.log); (3)
  process_transfers_from_cel.py (CEL -> call_transfers cada 5 min): **pendiente decision
  David** — la tabla tiene solo 8 filas de prueba de 2025-07-09, nada vivo la consume, y el
  script ademas BORRA de call_transfers lo mas viejo de 90 dias en cada pasada; si se quiere
  el informe de transferencias, reactivar y dar dueno de logrotate a process_transfers.log.
  Cron recarga el fichero sin quejas (RELOAD 21:35:01 sin ERROR). Backup del original:
  `/root/asterisk-cleanup.bak_20260831` (md5 57fe90b4974e7f7d2c2b050f2cc6f9ab, copia en
  `C:\Users\A1\deselpa-inv\`).
- Crontab de root: línea horaria que trunca `php_errors.log` a 5000 líneas si supera 50 MB
  (`tail -n 5000 ... > .tmp && mv`) — rotación casera que compite con logrotate; con aggressive
  (size 5M) casi nunca saltará, pero sobra. Y `cleanup_parking_slots.py` aparece dos veces cada 5 min
  en `/etc/cron.d/parking_cleanup`.
- `/var/log/asterisk/messages` recibe ~50–100 k avisos/día
  `taskprocessor.c: The 'stasis/pool-control' task processor queue reached N scheduled tasks`
  (98 349 ayer). Es crónico, no lo ha provocado este cambio, pero es un síntoma de carga en Asterisk
  que merece mirarse (`core show taskprocessors`).
- 379 ficheros `panel_tarot/panel_YYYY-MM-DD.log` (uno por día desde 06-2025, ~9 MB en total): los rota
  logrotate por el glob pero nunca superan 5 MB → se acumulan sin límite. Es retención de la
  aplicación, no de logrotate; hace falta una purga (`find -mtime +90`) si se quiere acotar.
- Los 15 `.log` muertos de 2025 en `/var/log/asterisk` (< 1 MB en total) y `sync_tarotistas_realtime.log`,
  `pbx/unified.log`, `recordings_archive_manual_20260106_112000.log` (10 MB) se pueden archivar o borrar.
- `websocket-monitor.service` lleva 7 reinicios (`NRestarts=7`); no se ha investigado por qué.
