journalctl: Diagnóstico de Serviços Linux do journal ao Root Cause
O sintoma clássico: um serviço cai no servidor Linux, o systemctl status mostra três linhas de output e um “Failed with result ‘exit-code'”, e ninguém sabe o que aconteceu às 3h da manhã. Toda a resposta está no journal — o log binário central do systemd — e o journalctl é a ferramenta de leitura. Este artigo cobre a consulta desde o básico (journalctl -u nginx.service) até filtros avançados (prioridade, tempo, grepping, boots anteriores), a gestão de disco do journal e, no fim, um livro de receitas por serviço: webserver, base de dados, file share, VPN, DNS e contentores. Cada secção usa comandos com output real, replicável em qualquer distro com systemd (Debian, Ubuntu, RHEL, Fedora, SUSE).
⚠ Atenção: o journal não substitui os logs de aplicação — complementa. O nginx e o PostgreSQL continuam a escrever os seus ficheiros de log próprios por defeito, e o journal apenas captura o stdout/stderr dos serviços. Quando um erro nunca chega ao journal, o primeiro sítio a olhar é a configuração de logging da aplicação (ver secção 4).
Neste artigo
- O que Está a Acontecer — Como o journal funciona
- Passo 1 — Consulta Básica: Units, Tempo e Prioridade
- Passo 2 — Formatos de Output e Campos
- Passo 3 — Diagnóstico por Boot e Falhas de Serviço
- Passo 4 — Gestão de Disco: Disk Usage e Vacuum
- Receitas por Serviço — Webserver, BD, File Share e Outros
- Persistir Logs: journald.conf e Forwarding para Syslog
- Erros Comuns
- Como Evitar Problemas de Log no Futuro
- Artigos Relacionados
O que Está a Acontecer — Como o journal funciona
O journald (systemd-journald.service) é um daemon de logging que captura três fontes: stdout/stderr de todos os serviços lançados pelo systemd, a API do kernel (kmsg) e os logs que aplicações enviam via syslog socket. Tudo o que recebe é gravado num formato binário indexado — os ficheiros em /run/log/journal/ (volátil, vive em memória/tmpfs) e em /var/log/journal/ (persistente, se activado). Por isso o journalctl é mais rápido e mais rico que o grep /var/log/syslog: os entries guardam campos estruturados — MESSAGE, _PID, _SYSTEMD_UNIT, PRIORITY, _BOOT_ID, SOURCE_REALTIME_TIMESTAMP — e a consulta filtra por qualquer um deles (journalctl(1)).
Três particularidades confundem quem vem do mundo /var/log/syslog:
- Persistência não é o defeito default em todas as distros — sem
/var/log/journal/criado, o journal é volátil: reinicia o servidor e perde os logs anteriores. Em Debian/Ubuntu o pacote systemd cria o directório (persistente por defeito); em RHEL/Rocky também. Mas em sistemas minimalistas ou containers pode faltar — a verificação está na secção 7. - Permissões — utilizadores do grupo
systemd-journal(Debian/Ubuntu) ouwheel/adm(RHEL/Fedora) veem todo o journal. Sem isso, cada utilizador só vê os seus próprios logs de user services. É a causa mais comum de “journalctl vazio para o meu utilizador”. - Tamanho auto-limitado — o journald respeita limites de disco definidos em
journald.conf(10% do filesystem, 4 GB máx por defeito nos limites SystemKeepFree/SystemMaxUse) e faz rotação automática. Não enche o disco à toa, mas também apaga logs antigos quando atinge o limite — por isso a secção 4 trata do vacuum.
A última propriedade relevante para diagnóstico: o journal indexa por boot. Cada arranque recebe um _BOOT_ID, e o journalctl pode mostrar os logs do boot actual, do boot anterior (o que é ouro para descobrir por que é que o servidor não arrancou bem) ou de qualquer boot listado com --list-boots.
Passo 1 — Consulta Básica: Units, Tempo e Prioridade
Os quatro filtros cobrem 90% do diagnóstico do dia-a-dia. Todos os exemplos abaixo são sintaxe validada contra o manual oficial de journalctl(1).
Por unidade de serviço — o filtro mais usado:
# Todos os logs do nginx (inclui falhas e restarts)
journalctl -u nginx.service
# Nginx + PHP-FPM juntos, intercalados por tempo (diagnóstico de stack)
journalctl -u nginx.service -u php8.3-fpm.service
O -u filtra pelo campo _SYSTEMD_UNIT — só aparecem entradas geradas por esse unit. Repetir -u várias vezes intercala os logs dos serviços escolhidos em ordem cronológica, o que é essencial quando dois serviços conversam (proxy e backend).
Por intervalo de tempo — --since e --until:
# Desde às 8h de hoje
journalctl -u mariadb.service --since "08:00"
# Janela específica da madrugada
journalctl -u sshd --since "2026-09-02 23:00" --until "2026-09-03 06:00"
# Atalhos relativos
journalctl -u nginx --since "1 hour ago"
journalctl --since today
journalctl --since yesterday --until now
O formato completo é YYYY-MM-DD HH:MM:SS. A hora omitida assume 00:00:00. Os atalhos today, yesterday, now, e expressões como -30min ou 1 hour ago são todos aceites — é mais rápido que calcular timestamps.
Por prioridade — os níveis syslog, 0 (emerg) a 7 (debug):
# Só erro e acima (3=err, 2=alert, 0=emerg)
journalctl -p err
# Do boot actual, só avisos e superiores
journalctl -b -p warning
O -p aceita o número ou o nome (err, warning, info). Filtragens combinadas são o padrão real de trabalho:
# Erros do PostgreSQL nas últimas 24h, sem paginação
journalctl -u postgresql.service --since "24 hours ago" -p err --no-pager
Por texto — grep integrado no journal, antes do output (mais eficiente que pipe):
# Entradas cujo MESSAGE faz match de regex PCRE2
journalctl -u nginx -g "upstream.*timed out"
# Combinação: erro em qualquer serviço, nas últimas 6h
journalctl -p err --since "-6 hours" -g "Connection refused"
O --grep=/-g usa regex PCRE2 sobre o campo MESSAGE. Diferença para journalctl ... | grep: o filtro corre na leitura do journal e respeita os restantes filtros de unit/tempo — com journal grandes faz diferença de segundos.
Complementos imediatos: -n 50 (últimas 50 linhas), -f (follow, tail em tempo real), -r (reverse, mais recente primeiro), --no-pager (para scripts).
Passo 2 — Formatos de Output e Campos
O formato por defeito (short) mostra uma linha por entrada. Para análise mais funda, o -o muda a saída:
# Campos completos com timestamps detalhados (quem escreveu, de onde)
journalctl -u nginx.service -o short-full | head -5
# JSON por linha — para jq, pipelines e ingestão externa
journalctl -u postgresql.service --since "1 hour ago" -o json-pretty | head -40
# Mostra TODOS os campos da entrada (inclui _COMM, _EXE, _HOSTNAME)
journalctl -u sshd -o verbose -n 1
Os formatos principais: short (default, uma linha), short-full (timestamp legível com data completa), json/json-pretty (um objecto por entrada), verbose (todos os campos FIELD=value), cat (só a mensagem, para scripts).
Um exemplo do que o verbose revela — a mesma entrada que em short é uma linha, em campos é uma mini-base de dados:
_BOOT_ID=3f9a2c...
_PID=1234
_SYSTEMD_UNIT=nginx.service
PRIORITY=4
MESSAGE=connect() to 127.0.0.1:9000 failed (111: Connection refused)
Campos que valem ouro em diagnóstico: _PID e _EXE (processo exacto que escreveu), _SYSTEMD_UNIT (unit origem), SYSLOG_IDENTIFIER (para serviços que logam por nome próprio, como sshd), CODE_FILE/CODE_LINE (para apps que usam a API do journal).
Filtrar por campo directamente — qualquer campo indexado pode ser criterio:
# Só o sshd por identifier (evita misturar daemon de outro unit)
journalctl SYSLOG_IDENTIFIER=sshd --since today
# Logs do kernel deste boot
journalctl -k -b 0
# Entradas de um PID específico (investigar processo órfão)
journalctl _PID=2345
Para exportação e retenção — converter o binário em texto plano para guardar ou enviar a terceiros (ex.: fornecedor do ERP):
# Exportar janela de incidente para ficheiro legível
journalctl -u erp-backend.service --since "2026-09-01 12:00" --until "2026-09-01 14:00" > /tmp/incidente-erp.log
O redirect num journal binário funciona porque o journalctl em modo não-interactivo escreve o output processado — o ficheiro resultante é syslog-like e legível por qualquer pessoa.
Passo 3 — Diagnóstico por Boot e Falhas de Serviço
Listar boots e navegar entre eles:
# Lista todos os boots registados (número relativo, ID, timestamps)
journalctl --list-boots
# Logs do boot anterior — por que é que caiu / não arrancou
journalctl -b -1 -p err
# Últimas linhas antes do reboot do boot anterior
journalctl -b -1 -n 100
O --list-boots devolve algo como -2 a1b2c3... Mon 2026-09-01 08:11:01—Mon 2026-09-01 20:44:03, -1 ..., 0 .... O boot -1 é o anterior ao actual — quando um servidor reinicia sozinho às 4h, o que aconteceu a seguir ao último log do boot -1 costuma ser kernel panic, OOM ou falta de energia, e o journalctl -b -1 -e (fim do boot) mostra o estado final.
Ciclo completo de diagnóstico de um serviço que falha:
# 1. Estado actual do unit (código de saída e sinal)
systemctl status nginx.service
# 2. Tudo o que o unit escreveu, desde que arrancou
journalctl -u nginx.service -b
# 3. Só as falhas: restarts, exit codes, OOM
journalctl -u nginx.service -b -p err
# 4. O momento exacto da última falha com contexto (+/-50 linhas de contexto via -n)
journalctl -u nginx.service -n 200 -o short-full | grep -B3 -A3 "failed"
O systemctl status dá o estado (active/failed, PID, ultimas 10 linhas do journal). O passo 2 é onde está a história completa — se o serviço morreu às 3h e só foste ver às 9h, o status já só mostra a linha do último restart, mas o journal tem a sequência completa.
Detectar OOM-killer — quando um serviço morre “sem razão”, o killer de OOM do kernel é suspeito número um:
journalctl -k -b -1 --grep "Out of memory|Killed process"
O -k filtra só mensagens do kernel. Se aparecer Killed process 2345 (php-fpm), o root cause é memória, não o serviço — e a correcção passa por limites de memória ou mais RAM, não por reinstalar o serviço.
Passo 4 — Gestão de Disco: Disk Usage e Vacuum
O journal cresce, e em servidores com discos pequenos (uma VPS de 20 GB) consegue comer gigabytes. Os comandos de gestão:
# Quanto espaço o journal está a usar
journalctl --disk-usage
# Reduzir para um máximo de 500 MB (apaga os ficheiros arquivados mais antigos)
sudo journalctl --vacuum-size=500M
# Manter só as últimas 2 semanas
sudo journalctl --vacuum-time=2weeks
# Rodar agora e depois vacuum numa só passada
sudo journalctl --rotate --vacuum-size=300M
O --disk-usage soma o espaço dos ficheiros activos e arquivados. O --vacuum-size= apaga ficheiros arquivados (não o activo) até ficar abaixo do limite. O --vacuum-time= apaga os anteriores à data. O --rotate fecha os ficheiros activos para arquivos — útil antes de um vacuum para libertar mais.
O permanente é configurar em /etc/systemd/journald.conf — a secção 7 detalha. Para um limite de disco claro em servidor de produção:
# /etc/systemd/journald.conf — gravação persistente com tecto de 1 GB
[Journal]
Storage=persistent
SystemMaxUse=1G
Depois de editar: sudo systemctl restart systemd-journald. O SystemMaxUse=1G garante que o journal nunca passa disso — o journald roda e apaga os arquivos mais antigos sozinho.
Receitas por Serviço — Webserver, BD, File Share e Outros
O padrão de trabalho é sempre o mesmo (-u serviço + filtro). O que muda por serviço é onde está a informação útil e qual o erro típico.
Nginx / Apache (webserver)
O webserver regista no journal o arranque, reloads e erros de config. Os acessos vão para os access logs próprios (não para o journal — Apache logs, Nginx log module).
# Reloads e erros de configuração (o clássico: reload falhou e o site ficou com o config antigo)
journalctl -u nginx.service --since today
# Erros 502 vindos do nginx: ver a falha na ligação ao upstream
journalctl -u nginx.service -g "upstream|connect()"
# Apache: erros de arranque de módulos e vhosts
journalctl -u httpd -p err --since "24 hours ago"
# PHP-FPM (o upstream do 502): processos mortos por OOM
journalctl -u php8.3-fpm.service -b -g "WARNING|ERROR|Killed"
O diagnóstico típico: utilizador reporta 502 → journalctl -u nginx -g "connect()" mostra Connection refused (111) para a porta do FPM → journalctl -u php8.3-fpm -b mostra o FPM morto às 3h → journalctl -k -b -1 -g "Killed process" confirma OOM. Quatro comandos, causa raiz.
PostgreSQL / MariaDB (base de dados)
Os motores de BD registam no journal os eventos de systemd (start/stop/crash) e, conforme a configuração, também os logs da aplicação.
# Ciclo de arranque/queda do PostgreSQL e onde está o log real
journalctl -u postgresql.service --since today
# MariaDB: erros e avisos do unit
journalctl -u mariadb.service -p warning -b
# Detecção de queda por OOM ou lock wait
journalctl -u mysql.service --grep "InnoDB|locked|Killed" --since "7 days ago"
O PostgreSQL com distro package envia por defeito os logs da BD para o journal (log_destination = stderr + unit captura stdout) — o que significa que journalctl -u postgresql contém os FATAL/ERROR da BD completos (PostgreSQL logging config). Em MariaDB, o error log pode ir para stderr (journald) ou para ficheiro conforme log_error — se o ficheiro estiver definido, o journal só terá os eventos do systemd.
Samba (file share)
O Samba tem logging próprio configurável em smb.conf — o backend systemd envia os logs para o journal (smb.conf: logging):
# Eventos do daemon principal: arranques, ligações, falhas de auth
journalctl -u samba.service -u smbd -u nmbd --since today
# Se o smb.conf usa logging = systemd, os logs por nível aparecem no journal:
journalctl SYSLOG_IDENTIFIER=smbd -g "NT_STATUS_ACCESS_DENIED"
# Quem tenta ligar a partilhas sem sucesso (investigação de brute force)
journalctl SYSLOG_IDENTIFIER=smbd --since "1 hour ago" --no-pager | grep "session setup"
No smb.conf, a activação do backend journald: logging = systemd@2 dentro de [global] (backend systemd com nível 2). Sem isso, o Samba escreve nos ficheiros log.smbd e o journal só mostra o ciclo de systemd.
SSH (acesso remoto)
# Quem tentou entrar, quando e com que resultado
journalctl SYSLOG_IDENTIFIER=sshd --since today
# Só falhas de autenticação (investigação de brute force)
journalctl SYSLOG_IDENTIFIER=sshd -g "Failed|Invalid user" --since "24 hours ago"
# IP atacante — contagem para firewall
journalctl SYSLOG_IDENTIFIER=sshd -g "Invalid user" --since today --no-pager | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort | uniq -c | sort -rn | head
O sshd identifica-se com SYSLOG_IDENTIFIER=sshd — filtrar por identifier em vez de -u ssh.service funciona mesmo em distros onde o unit tem nomes diferentes (ssh vs sshd).
DNS e VPN (dnsmasq, Unbound, WireGuard, OpenVPN)
# DNS: consultas, falhas de resolução, cache
journalctl -u dnsmasq -u unbound --since today -p notice
# WireGuard: eventos do kernel (handshakes, erros de interface)
journalctl -k --grep "wireguard" --since today
# OpenVPN: sessões de clientes e renegociações
journalctl -u openvpn-server@server.service -b --since "12 hours ago"
Docker / Podman (contentores)
O journald captura o stdout/stderr dos containers lançados como units (Podman Quadlet, docker.service):
# Logs de TODOS os containers através do motor
journalctl -u docker.service --since "1 hour ago"
# Podman Quadlet: cada container gerado é um unit próprio
journalctl -u nginx-app.service -f
# Contentor que morre em loop de restart: exit codes no journal
journalctl -u docker.service -g "exited with code"
Para contentores que escrevem os próprios ficheiros de log (nginx dentro do container, por exemplo), o journal mostra só o ciclo do container — o resto está em docker logs <container> ou no volume do container. Nos Quadlets, o journalctl -u <nome>.service é o caminho canónico (Podman Quadlet).
Tabela-resumo de filtros por serviço
| Serviço | Comando base | O que procurar |
|---|---|---|
| Nginx | journalctl -u nginx -g "connect()" |
upstream refused, reload failures |
| Apache | journalctl -u httpd -p err |
module errors, SSL handshake |
| PostgreSQL | journalctl -u postgresql -p warning |
FATAL, connection refused, lock timeout |
| MariaDB/MySQL | journalctl -u mariadb --grep "InnoDB" |
crash recovery, OOM kill |
| Samba | journalctl SYSLOG_IDENTIFIER=smbd |
NT_STATUS, session setup failures |
| SSH | journalctl SYSLOG_IDENTIFIER=sshd |
Failed password, Invalid user |
| WireGuard | journalctl -k --grep wireguard |
handshake failures |
| Docker | journalctl -u docker -g exited |
exit codes, restart loops |
Persistir Logs: journald.conf e Forwarding para Syslog
Verificar primeiro o estado actual da persistência:
ls /var/log/journal/ && echo "PERSISTENTE" || echo "VOLÁTIL — logs morrem no reboot"
journalctl --disk-usage
Se o directório não existir, tudo o que o servidor registou desde o boot desaparece no reinício — e a investigação de incidentes de dias anteriores é impossível. Activar persistência:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
O mkdir cria o directório que sinaliza persistência ao journald. O systemd-tmpfiles aplica as permissões correctas do tmpfiles.d. O restart re-carrega a configuração — a partir daqui os boots futuros arquivam em /var/log/journal/.
Os parâmetros que importam em journald.conf (journald.conf(5)):
[Journal]
Storage=persistent # gravar em disco (auto=persistente só se /var/log/journal existir)
SystemMaxUse=1G # tecto total do journal em disco
MaxRetentionSec=1month # apagar entradas com mais de 1 mês
ForwardToSyslog=yes # copiar para rsyslog/syslog-ng (se existir)
O Storage=persistent garante a gravação. O SystemMaxUse põe um tecto claro (sem depender da regra de 10% default). O ForwardToSyslog=yes interessa quando a PME já tem um servidor central de logs via rsyslog — o journald continua a ser a fonte, e o syslog faz o transporte.
Erros Comuns
| Problema | Causa provável | Solução |
|---|---|---|
journalctl -u X devolve “No journal files” ou vazio |
Utilizador fora do grupo systemd-journal/adm | sudo usermod -aG systemd-journal user + re-login |
| Logs desaparecem após reboot | Journal volátil (sem /var/log/journal) | Activar persistência (secção 7) |
--disk-usage mostra GB e disco a encher |
Limites default (10%) altos em disco pequeno | SystemMaxUse= no journald.conf + --vacuum-size= |
| Serviço tem logs no journal mas erros não aparecem | App escreve para ficheiro próprio, não para stderr | Configurar a app para stderr/logfile, ou ajustar journald.conf |
-f não mostra nada de um serviço a correr |
Serviço é de utilizador (--user) ou escreve para ficheiro próprio |
journalctl --user -u X ou verificar o log file da app |
| Timestamps errados / em UTC | Timezone do servidor mal definida | timedatectl set-timezone Europe/Lisbon |
| journal lento em consultas amplas | Sem filtro por boot/unit numa base enorme | Sempre -u, -b ou janela de tempo; --grep em vez de pipe |
Como Evitar Problemas de Log no Futuro
- Persistência activada em todos os servidores —
Storage=persistent+SystemMaxUseno journald.conf desde o primeiro dia. Um journal volátil é um journal inútil para diagnóstico post-mortem. - Serviços críticos a escrever para stderr — unidades criadas pela casa devem logar para stdout/stderr (o journald captura sozinho) em vez de ficheiros próprios, para ter tudo consultável por
journalctl -u. - Limite de disco definido conscientemente — 1-2 GB em servidores normais;
MaxRetentionSecalinhado com a política de retenção da empresa. - Forward para servidor central — se existe rsyslog/syslog-ng ou Grafana Alloy na rede, o
ForwardToSyslog(ou o journal remote do systemd) centraliza antes que um disco morra e leve os logs com ele. - Atalhos documentados para a equipa — um snippet partilhado com os 5-6 comandos por serviço (a tabela da secção 5) evita que cada diagnóstico comece do zero.
Artigos Relacionados
- Podman Quadlet: Contentores Geridos pelo systemd na PME — units systemd gerados para containers, diagnosticáveis com
journalctl -u. - Dia 27: Hardening no Linux — CIS, fail2ban, Auditd e Lynis — fail2ban lê os logs que este artigo ensina a consultar.
- Grafana Alloy: Pipeline de Telemetria Unificado para PME — exportar o journal para um pipeline central de observabilidade.
- Dia 18: NFS e Samba — Partilha de Ficheiros Linux e Windows — o serviço de file share cujo diagnóstico está na secção 5.
- Troubleshooting de Problemas de Arranque no Windows Server — o equivalente Windows: diagnóstico de boot e eventos de serviço.