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

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 estruturadosMESSAGE, _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) ou wheel/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 servidoresStorage=persistent + SystemMaxUse no 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; MaxRetentionSec alinhado 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