Buzeli
buzeliSoluções Digitais
SRE

logrotate falhando há semanas em silêncio: como um `|| true` escondeu 3 bugs e parou a rotação de todos os logs

Publicado em 23 de junho de 2026

Ilustração de arquivos de log transbordando porque a engrenagem de rotação travou, com um alerta silenciado

O sintoma: disco enchendo, logs sem girar

Um servidor com o disco subindo devagar e sem motivo aparente. Investigando, os logs em /var/log não estavam mais rotacionando: arquivos de semanas, alguns na casa dos gigabytes, sem o sufixo .1, .2.gz de sempre. O logrotate estava instalado, agendado, e — segundo o systemd — "rodando". Só que rodando e falhando.

O detalhe traiçoeiro: o logrotate processa os arquivos de config em sequência e, ao bater num erro fatal, retorna exit code 1. Para o agendador (cron/systemd timer), aquilo é só "a unidade falhou" — sem alarme, sem página. E como um erro num único stanza derruba o status do run inteiro, a impressão era de que NADA rotacionava, quando na verdade um punhado de configs quebradas contaminava o resultado.

Tornando o silêncio visível: logrotate em modo debug

logrotate não grita por padrão. Mas o modo debug (-d, que é dry-run) mais o verbose (-v) mostram exatamente onde ele tropeça, sem alterar nada:

Copiar
# -d = debug/dry-run (não escreve nada), -v = verbose
sudo logrotate -dv /etc/logrotate.conf 2>&1 | less

# Para ver só os erros:
sudo logrotate -dv /etc/logrotate.conf 2>&1 | grep -iE "error|warning|skipping|duplicate"

Saíram três erros distintos, de três causas independentes. Cada um, sozinho, era suficiente para retornar exit 1.

Bug 1: config duplicada entre dois diretórios de include

A main config dava include em dois diretórios: o padrão /etc/logrotate.d/ e um diretório de automação interna, /opt/ops/logrotate.d/. Ambos tinham stanzas para os mesmos arquivos (o syslog e os logs do AIDE). logrotate trata o mesmo logfile declarado duas vezes como erro fatal:

Copiar
error: /opt/ops/logrotate.d/aide:1 duplicate log entry for /var/log/aide/aide.log

Conserto: um logfile, um dono. Removemos a duplicata do diretório de automação e deixamos cada arquivo numa única stanza. Regra simples: se você dá include em mais de um diretório, garanta que eles não se sobreponham.

Bug 2: diretório world-writable que o logrotate se recusa a tocar

O diretório dos logs do PHP-FPM estava com permissão drwxrwxrwx (777). Por segurança, o logrotate se recusa a rotacionar arquivos em diretórios graváveis por qualquer um — a menos que você diga explicitamente sob qual usuário/grupo operar, via a diretiva su:

Copiar
error: skipping "/var/log/php-fpm/error.log" because parent directory has insecure permissions
(It's world writable or writable by group which is not "root") Set "su" directive in config file
to tell logrotate which user/group should be used for rotation.

Havia dois caminhos: corrigir a permissão do diretório (o ideal) ou declarar su na stanza. Fizemos os dois — apertamos a permissão para 755 e adicionamos a diretiva como rede de segurança:

Copiar
/var/log/php-fpm/*.log {
    su root adm
    weekly
    rotate 8
    compress
    missingok
    notifempty
}

Bug 3: o glob vazio que é fatal mesmo com missingok

A pegadinha mais sutil. Uma stanza apontava para /var/log/aide/*.log. O diretório existia, mas estava vazio — nenhum arquivo casava com o glob. A intuição diz "tenho missingok, então tudo bem". Errado: missingok cobre um caminho literal ausente, mas um glob que não casa com nada é tratado como erro fatal pelo logrotate em muitas versões:

Copiar
error: /opt/ops/logrotate.d/aide:1 glob failed for /var/log/aide/*.log

Como o AIDE ainda não tinha gerado logs naquele caminho, a stanza inteira estava ali esperando para falhar. Conserto: remover a stanza até existirem arquivos, ou apontar para o caminho literal correto. Glob otimista + diretório vazio = exit 1 silencioso.

A causa por trás da causa: o `|| true` que cegava tudo

Mesmo depois de a rotação voltar a funcionar, faltava entender por que ninguém tinha percebido o problema antes. A resposta estava no postrotate de uma das stanzas — um hook que rodava um script Python para enviar métricas de log a um bucket:

Copiar
postrotate
    /usr/bin/python3 /opt/ops/ship-logs.py >/dev/null 2>&1 || true
endscript

Aquele >/dev/null 2>&1 || true joga toda a saída no lixo e força sucesso aconteça o que acontecer. Rodando o script à mão, o que estava escondido apareceu na hora:

Copiar
$ /usr/bin/python3 /opt/ops/ship-logs.py
Traceback (most recent call last):
  File "/opt/ops/ship-logs.py", line 3, in <module>
    import pandas as pd
ModuleNotFoundError: No module named 'pandas'

O ambiente Python tinha sido recriado em algum upgrade e perdido as dependências. O script falhava em toda execução — mas o || true engolia o erro, e o postrotate "passava". Conserto: reinstalar as dependências e parar de mascarar o erro:

Copiar
pip install pandas pyarrow boto3
# pandas 2.3.3, Python 3.9.18

# e no postrotate: deixar o erro aparecer (logrotate registra a falha do hook)
postrotate
    /usr/bin/python3 /opt/ops/ship-logs.py
endscript
|| true não é tratamento de erro — é supressão de erro. Use só quando a falha for genuinamente irrelevante, e nunca grudado num 2>&1 que também apaga a mensagem. Caso contrário, você está construindo um silêncio que vai te custar caro num dia ruim.

Como testar rotação antes que ela te morda

Depois de qualquer mudança em config de logrotate, valide sem esperar o cron da madrugada:

Copiar
# 1. Dry-run: mostra o que faria, sem escrever
sudo logrotate -dv /etc/logrotate.conf

# 2. Força a execução de verdade e checa o exit code
sudo logrotate -fv /etc/logrotate.conf ; echo "exit: $?"

# 3. Saúde do timer no systemd (a falha não vira alerta sozinha!)
systemctl status logrotate.timer
journalctl -u logrotate.service --since "yesterday" | grep -i error

Um exit diferente de zero ali é o seu único aviso prévio. Se ninguém olha esse exit code, a falha fica invisível até o disco encher.

Lições

1. Um stanza quebrado contamina o run inteiro. logrotate retorna exit 1 ao primeiro erro fatal; para o cron/systemd isso é só "falhou", sem distinguir qual config. Trate o exit code como sinal monitorável.

2. missingok não cobre glob vazio. Caminho literal ausente: ok. Glob que não casa com nada: erro fatal. Não crie stanzas otimistas para logs que ainda não existem.

3. Diretório world-writable exige `su` (ou perm correta). logrotate se recusa a girar logs em diretório 777 por segurança. Aperte a permissão e/ou declare su user group.

4. `|| true` com `2>&1` é uma bomba-relógio. Esconde a falha E a mensagem. Se o hook pode falhar de um jeito que importa, deixe o erro aparecer e seja registrado.

Quando um sinal verde mente, o padrão se repete em outras camadas — como em site retornando 500 com o WAF saudável. O instrumento dizia "ok"; a realidade, não.

Conclusão

Nenhum dos três bugs era difícil — um include duplicado, uma permissão larga, um glob vazio. O que os tornou perigosos foi a soma com um || true que apagava a única evidência. Manutenção de infra é, em boa parte, recusar silêncios convenientes: fazer o erro aparecer hoje, no dry-run, em vez de aparecer sozinho daqui a três semanas, com o disco a 100% e o serviço caindo.