O que o trilha-log resolve
Nasceu de uma arquitetura de logging validada rodando em produção (dev e prod, Log4j2, JSON estruturado) e foi generalizada para qualquer projeto Spring Boot 4.x novo: adiciona a dependência, ganha a base pronta, customiza só o necessário.
| Peça | O que resolve |
|---|---|
RequestCorrelationFilter | Um traceId por requisição, presente em todo log emitido enquanto ela é processada — inclusive de bibliotecas de terceiros. Propaga traceId de cabeçalhos externos (W3C, B3, X-Request-ID). Integra com Micrometer Tracing quando disponível. |
@LogExecution | Log de entrada, saída e exceção de método via AOP, com um breadcrumb de callChain acumulando em chamadas aninhadas. Suporta excludeReturnTypes e limite de frames no callChain. |
@Sensitive / LogMaskingUtil | Mascaramento central: nunca reflete entidade JPA nem coleção/mapa; nunca deixa senha, token etc. em claro. @Sensitive pode ser usada em campos e em parâmetros de método. |
AppLog | Log de evento de negócio no meio de um método, onde o AOP não alcança. Sobrecargas com Map<String, ?> adicionam campos ao MDC temporariamente — aparecem como propriedades independentes no JSON. |
CapturaLogAppender | Captura eventos de log em memória nos seus testes, sem depender de stdout. |
Cada peça é registrada por autoconfiguração padrão do Spring Boot 4.x — nada de @Import manual ou component-scan. Cada bean é @ConditionalOnMissingBean, então qualquer peça pode ser sobrescrita registrando seu próprio bean do mesmo tipo.
Instalação
O trilha-log é distribuído via GitHub Packages da organização, não Maven Central. O pacote é público, mas o protocolo Maven do GitHub Packages exige um token de qualquer forma.
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/trilha-log/trilha-log</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.github.trilhalog</groupId>
<artifactId>trilha-log</artifactId>
<version>0.2.0</version>
</dependency>
</dependencies>
trilha-log já traz o motor de logging (Log4j2). Se o seu projeto usa spring-boot-starter-web ou qualquer outro starter que puxe spring-boot-starter-logging (Logback) transitivamente, exclua-o — as duas bindings SLF4J ativas juntas deixam o provider ambíguo no classpath.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
Credenciais para resolver o pacote
Requer um GitHub PAT (ou gh auth token) com escopo read:packages. Três cenários:
Build local
Adicione um <server> ao ~/.m2/settings.xml:
<settings>
<servers>
<server>
<id>github</id>
<username>SEU_USUARIO_GITHUB</username>
<password>SEU_TOKEN_COM_READ_PACKAGES</password>
</server>
</servers>
</settings>
Docker
Resolva o token via BuildKit secret, nunca via ARG/ENV gravado na imagem:
export GITHUB_ACTOR=seu-usuario-github
export GITHUB_TOKEN=$(gh auth token)
docker build \
--secret id=github_token,env=GITHUB_TOKEN \
--build-arg GITHUB_ACTOR=$GITHUB_ACTOR \
-t sua-app .
CI (GitHub Actions)
actions/setup-java com server-id: github gera o <server> do settings.xml do runner automaticamente, usando o secrets.GITHUB_TOKEN padrão do workflow — nenhum PAT extra precisa ser cadastrado.
Casos de uso
Cada feature isolada já está documentada nas seções seguintes. Aqui, quatro cenários reais combinando as peças — pra situar o "por quê" antes do "como".
Microsserviços atrás de um gateway, com tracing distribuído
Seu serviço é chamado por um API gateway ou por outro microsserviço que já participa de um trace distribuído (Zipkin, OpenTelemetry, ou só um gateway que injeta X-Request-ID). Você quer que o traceId dos seus logs seja o mesmo que já existe na cadeia — não um novo, desconectado.
Não precisa configurar nada: RequestCorrelationFilter já inspeciona traceparent/X-Request-ID/X-Correlation-ID/X-B3-TraceId por padrão (veja Propagação de traceId) e reaproveita o valor recebido em vez de gerar um novo. Se além disso o classpath tem micrometer-tracing com um span ativo, o spanId também aparece no MDC (veja Micrometer Tracing) — o mesmo ID que você vê no Zipkin/Jaeger aparece nos seus logs JSON, sem fazer join manual entre dois sistemas.
Aplicação sob LGPD/GDPR
Seus logs não podem carregar dado pessoal identificável além do necessário — nem IP do cliente, nem CPF/RG em texto plano, nem senha em nenhuma hipótese.
trilha-log:
correlation:
log-ip: false # remove o IP do log de acesso
masking:
extra-keywords:
- cpf
- rg
Some isso com @Sensitive nos parâmetros de método que recebem valor sensível cru (não embrulhado em DTO) — veja Mascaramento — e você cobre os três vetores mais comuns de vazamento de PII em log: IP, documento, e parâmetro solto.
Debugando uma integração de terceiro instável em produção
Um parceiro externo (gateway de pagamento, provedor de frete) está devolvendo erro intermitente e você precisa ver o payload exato que disparou o problema — sem deixar log de corpo de requisição ligado o tempo todo.
trilha-log:
correlation:
log-request-body: true
max-body-log-bytes: 4096
Ative num profile próprio (SPRING_PROFILES_ACTIVE=investigacao), reproduza o problema, correlacione pelo traceId daquela requisição específica no log de acesso, e desligue de novo — o corpo já sai mascarado automaticamente (campos com nome sensível viram "***" mesmo dentro do JSON logado), então mesmo habilitado por um tempo curto não expõe segredo.
Processamento em lote com muitas chamadas aninhadas
Um job noturno itera milhares de registros, cada um passando por uma cadeia de 5+ services com @LogExecution. Sem limite, o callChain de um item processado no fim do lote fica enorme e ilegível; e você quer métricas por item (quantos falharam, qual o valor processado) pesquisáveis num dashboard, não só texto solto no log.
trilha-log:
aspect:
max-call-chain-frames: 4 # mantém só os 4 frames mais recentes
Combine com AppLog e campos MDC estruturados (veja Eventos de negócio) pra emitir um evento indexável por item processado — appLog.info("item processado", Map.of("pedidoId", id, "status", status)) — em vez de tentar extrair isso de uma string de log livre depois.
Correlação de requisição traceId
RequestCorrelationFilter gera um traceId (por padrão, um UUID completo de 32 caracteres — configurável, veja abaixo) e coloca no MDC/ThreadContext antes de chamar o resto da cadeia de filtros. Roda com @Order(HIGHEST_PRECEDENCE) — antes de qualquer filtro de segurança da aplicação consumidora.
traceId é gravado no MDC antes de qualquer filtro de segurança e acompanha todo o resto do processamento — inclusive logs emitidos por Hibernate, Spring Security e outras bibliotecas de terceiro, já que ele vive no MDC/ThreadContext, não em um wrapper de logger. É removido só no finally, depois do log de acesso.Comprimento do traceId
Por padrão o traceId gerado é um UUID sem hifens (32 chars hex). Para usar um traceId mais curto — compatível com o formato legado de 8 chars — configure:
trilha-log:
correlation:
trace-id-length: 8 # 0 = UUID completo (padrão)
Opt-out de IP do cliente
O log de acesso inclui o IP por padrão. Em ambientes com requisitos LGPD/GDPR, desabilite:
trilha-log:
correlation:
log-ip: false
Propagação de traceId externo
Quando a requisição chega com um cabeçalho de rastreamento, o filtro reutiliza aquele valor em vez de gerar um novo. Os cabeçalhos são inspecionados na ordem configurada — o primeiro presente e não vazio vence:
Seu serviço nunca é a primeira parada de uma requisição — está atrás de um API gateway, um BFF, ou é chamado por outro microsserviço. Sem propagação, cada serviço geraria seu próprio traceId desconectado, e correlacionar uma falha através da cadeia viraria trabalho manual de cruzar timestamps. Com propagação, o mesmo ID atravessa todos os serviços — um grep pelo traceId no agregador de log já traz a história completa.
| Cabeçalho | Formato |
|---|---|
traceparent | W3C — 00-<traceId32>-<spanId>-<flags>. Extrai os 32 chars de traceId. |
X-Request-ID | Valor direto (truncado a 32 chars). |
X-Correlation-ID | Valor direto (truncado a 32 chars). |
X-B3-TraceId | Zipkin B3 — valor direto. |
Para alterar a lista ou desativar completamente a propagação:
trilha-log:
correlation:
incoming-trace-headers:
- traceparent # W3C — inspecionado primeiro
- X-Request-ID # lista vazia desativa propagação
Logging de corpo HTTP (opt-in)
Desabilitado por padrão para não vazar dados sensíveis sem consentimento explícito. Quando habilitado, o corpo JSON da requisição é mascarado automaticamente pelo mesmo LogMaskingUtil — campos com nome sensível viram "***" antes do log.
Um parceiro externo ou cliente manda um payload malformado e o erro só reproduz em produção — logar request.getParameter(...) um por um não escala, e reproduzir localmente sem o payload exato é chute. Ligue temporariamente (idealmente num profile separado, não no application.yml comum), capture o traceId da requisição problemática no log de acesso, e desligue de novo — não é pensado pra ficar ligado o tempo todo em produção.
trilha-log:
correlation:
log-request-body: true # opt-in — false por padrão
max-body-log-bytes: 2048 # trunca payloads maiores
O mascaramento do corpo usa ObjectMapper para parsear o JSON. Se jackson-databind não estiver no classpath, o corpo não é logado mesmo com log-request-body: true — nenhuma exceção, nenhuma quebra.
Integração com Micrometer Tracing
Quando micrometer-tracing está no classpath e há um span ativo (ex.: Zipkin, OpenTelemetry), o RequestCorrelationFilter usa o traceId do span corrente em vez de gerar um novo, e também coloca o spanId no MDC:
Você já tem uma stack de tracing distribuído (Zipkin, Jaeger, Grafana Tempo) e quer sair de "encontrei o erro no log, agora preciso adivinhar qual trace no Zipkin corresponde a ele" — sem essa integração, o traceId do log e o trace ID do backend de tracing são dois IDs diferentes que não se correlacionam. Com ela, é o mesmo ID nos dois lugares: acha o log, cola o traceId na busca do Zipkin, vê o span completo.
{"timestamp":"...","level":"INFO","traceId":"4bf92f3577b34da6a3ce929d0e0e4736","spanId":"00f067aa0ba902b7","callChain":"...","message":"..."}
A integração usa capability detection: se o Micrometer não estiver no classpath, o código nem é carregado — sem NoClassDefFoundError. Se estiver mas não houver span ativo, gera o traceId normalmente.
O trilha-log não declara micrometer-tracing como dependência obrigatória. Quem usa Spring Boot Actuator + Zipkin ou OpenTelemetry já recebe a integração automaticamente.
Execução via AOP callChain
@LogExecution pode ser aplicada em classe (vale pra todos os métodos) ou em método (sobrescreve a classe). Quatro atributos:
| Atributo | Tipo | Default | Efeito |
|---|---|---|---|
level | org.slf4j.event.Level | DEBUG | Nível dos logs de entrada/saída. |
logArgs | boolean | true | Loga os argumentos (mascarados). |
logReturn | boolean | true | Loga o retorno (mascarado). |
logException | boolean | true | Loga em ERROR antes de relançar. |
excludeReturnTypes | Class<?>[] | {} | Omite o log de retorno quando o tipo do valor retornado for um dos listados — útil para ResponseEntity e outros tipos verbosos. |
callChain acumula uma entrada por chamada aninhada (push/pop via MDC). Se AutenticacaoService.autenticar() lançar, o aspecto loga em ERROR e relança o Throwable original sem embrulhar — a mesma exceção sobe pela pilha, preservando a stack trace completa.Omitindo retorno para tipos específicos
Em controllers que retornam ResponseEntity, o log de retorno pode ser verboso e redundante com o log de acesso do filtro. Use excludeReturnTypes para suprimir só esses casos:
@LogExecution(excludeReturnTypes = {ResponseEntity.class})
@RestController
public class PedidoController {
public ResponseEntity<PedidoDto> buscar(long id) {
// entrada e exceção logados normalmente; retorno omitido
...
}
}
Limite de frames no callChain
Em stacks profundas, o callChain pode crescer indefinidamente. Configure um limite para manter o contexto mais recente — os frames antigos são substituídos por "... >":
trilha-log:
aspect:
max-call-chain-frames: 5 # 0 = sem limite (padrão)
Mascaramento de dados sensíveis
LogMaskingUtil.mascarar(nome, valor) é o ponto central, usado pelo aspecto de @LogExecution, pelo AppLog e por qualquer código que precise logar um valor que pode conter dado sensível. A ordem de verificação importa — é avaliada nesta sequência, parando na primeira que casar:
- Valor nulo → retorna
null. - Nome bate uma palavra-chave sensível (case-insensitive,
contains) →"***", sem olhar o valor. - Tipo simples (primitivo,
CharSequence,Number,Boolean, enum,UUID) → passa direto. - Entidade JPA (anotada
@Entity) → vira só"NomeDaClasse(entidade)", nunca reflete os campos. Collection,Mapou array → vira só o nome do tipo.- Qualquer outro objeto (DTO, record) → reflection rasa: campo
@Sensitiveou com nome sensível vira"***"; o resto é formatado comoNomeDaClasse{campo=valor}.
@Sensitive em parâmetros de método
Além de campos de DTO, @Sensitive pode ser aplicada diretamente em parâmetros de método anotados com @LogExecution — o aspecto mascara o argumento inteiro, independente do nome ou tipo:
@LogExecution
public boolean autenticar(String usuario, @Sensitive String senha) {
// log de entrada: autenticar(usuario=kevin, senha=***)
...
}
Palavras-chave padrão
senha, password, token, secret, apikey, api-key, authorization, chave — configuráveis via application.yml:
trilha-log:
masking:
extra-keywords:
- cpf
- rg
A checagem de @Entity usa capability detection: se jakarta.persistence não estiver no classpath, essa regra simplesmente não entra em ação — nenhum NoClassDefFoundError, nenhuma quebra silenciosa das outras regras de mascaramento.
Eventos de negócio com AppLog
Para logar um evento de negócio no meio de um método — onde o AOP de @LogExecution não alcança, já que ele só intercepta borda de entrada/saída, não uma condição no meio da lógica:
@LogExecution
@Service
public class AutenticacaoService {
private final AppLog appLog;
public AutenticacaoService(AppLog appLog) {
this.appLog = appLog;
}
public boolean autenticar(LoginRequest request) {
boolean autenticado = !request.senha().isBlank();
if (autenticado) {
appLog.info("login bem-sucedido", request);
} else {
appLog.warn("tentativa de login com senha vazia", request);
}
return autenticado;
}
}
AppLog usa StackWalker para atribuir o log à classe que de fato chamou — não a própria AppLog — chamado diretamente dentro de cada método público (nunca via helper privado extra, senão o frame conta errado).
Campos MDC estruturados
Para emitir campos indexáveis independentes no JSON (pesquisáveis no Elasticsearch/OpenSearch/Loki), use as sobrecargas com Map<String, ?>. Os campos são adicionados ao MDC antes do log e removidos no finally:
// campos simples no MDC — aparecem como propriedades no JSON
appLog.info("pagamento confirmado", Map.of(
"pedidoId", pedido.getId(),
"valor", pedido.getValor()
));
// contexto de objeto + campos MDC combinados
appLog.warn("estoque baixo", produto, Map.of("estoqueAtual", qtd));
{"timestamp":"...","level":"INFO","traceId":"...","callChain":"PedidoService.confirmar",
"pedidoId":"42","valor":"199.90","message":"pagamento confirmado"}
Nunca passe Map.of(...) como argumento context (o parâmetro Object). O mascaramento trata Map como "não reflete" — o resultado vira só o nome da classe, escondendo o dado. Para campos estruturados, use a sobrecarga (String, Map<String, ?>) descrita acima; para um único objeto rico, use um record local.
Configuração
| Propriedade | Default | Efeito |
|---|---|---|
trilha-log.correlation.enabled | true | Liga/desliga o RequestCorrelationFilter. |
trilha-log.correlation.log-ip | true | Inclui o IP do cliente no log de acesso. Desabilitar em ambientes com requisitos LGPD/GDPR. |
trilha-log.correlation.incoming-trace-headers | [traceparent, X-Request-ID, X-Correlation-ID, X-B3-TraceId] | Cabeçalhos inspecionados em ordem para reutilizar traceId externo. Lista vazia desativa a propagação. |
trilha-log.correlation.trace-id-length | 0 | Comprimento do traceId gerado. 0 = UUID completo (32 chars hex). Valores positivos truncam. Não afeta traceIds propagados de cabeçalhos. |
trilha-log.correlation.log-request-body | false | Opt-in: loga o corpo JSON da requisição com mascaramento automático. Requer Jackson no classpath. |
trilha-log.correlation.max-body-log-bytes | 2048 | Limite em bytes do corpo logado. Payloads maiores são truncados. |
trilha-log.aspect.enabled | true | Liga/desliga o LogExecutionAspect. |
trilha-log.aspect.max-call-chain-frames | 0 | Máximo de frames no callChain. 0 = sem limite. Ao ultrapassar, frames antigos viram "... >". |
trilha-log.masking.extra-keywords | [] | Palavras-chave somadas às padrão para mascaramento por nome. |
Qualquer peça pode ser trocada por implementação própria: registre um bean do mesmo tipo (AppLog, LogExecutionAspect, RequestCorrelationFilter) — a autoconfiguração recua automaticamente (@ConditionalOnMissingBean).
Log4j2 & saída JSON
Configuração de Log4j2 é por aplicação — copie este arquivo para src/main/resources/log4j2-spring.xml do seu projeto. O template JSON com os campos traceId/callChain já vem dentro do jar e é referenciado via classpath:.
<Configuration status="WARN">
<Appenders>
<Console name="ConsoleDev" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} traceId=%X{traceId} callChain=%X{callChain} -- %msg%n%throwable"/>
</Console>
<Console name="ConsoleJson" target="SYSTEM_OUT">
<JsonTemplateLayout eventTemplateUri="classpath:trilha-log/log4j2-json-template.json"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<SpringProfile name="!prod">
<AppenderRef ref="ConsoleDev"/>
</SpringProfile>
<SpringProfile name="prod">
<AppenderRef ref="ConsoleJson"/>
</SpringProfile>
</Root>
</Loggers>
</Configuration>
Em produção, uma linha de log vira:
{"timestamp":"2026-08-08T02:32:02.648Z","level":"INFO","traceId":"66678116","callChain":"LoginService.login > AutenticacaoService.autenticar","logger":"...AutenticacaoService","thread":"http-nio-8080-exec-2","message":"login bem-sucedido — LoginRequest{usuario=kevin, senha=***}"}
Os campos traceId e callChain somem sozinhos do JSON quando o MDC não tem valor para eles — nunca aparecem como campo vazio.
Nível de log é controlado só por logging.level.* no application-{profile}.yml — essa propriedade do Spring Boot funciona independente do engine de logging por trás. Não editar nível direto no log4j2-spring.xml.
Testando com CapturaLogAppender
Anexa um appender em memória ao root logger para asserir eventos de log sem depender de captura de stdout:
class LoginServiceTest {
private final CapturaLogAppender appender = new CapturaLogAppender();
@BeforeEach
void setUp() {
appender.anexar();
}
@AfterEach
void tearDown() {
appender.desanexar();
}
@Test
void logaTentativaComSenhaVazia() {
// ...
assertThat(appender.contemMensagem("tentativa de login com senha vazia")).isTrue();
}
}
anexar() força Level.ALL no root logger (não dá pra confiar no nível ambiente quando os testes rodam no mesmo processo Surefire) e desanexar() restaura o nível original salvo.
Perguntas & armadilhas
traceId aparece em logs de bibliotecas de terceiro?
Sim. Ele vive no MDC/ThreadContext, não em um wrapper de logger — qualquer coisa que logue durante aquela requisição (Hibernate, Spring Security, etc.) carrega o mesmo traceId.
@LogExecution pode embrulhar a exceção?
Nunca. Sempre relança o Throwable original capturado — nunca um new RuntimeException(e.getMessage()) — para que a stack trace completa chegue a quem trata.
Qual convenção de nível de log usar no meu projeto?
Sugestão validada em produção: WARN para violação de regra de negócio / uso indevido esperado, DEBUG para ruído comum, ERROR só para falha real de sistema — sempre com stack trace completa no log do servidor, nunca na resposta ao cliente.
E se meu projeto não tiver Servlet API no classpath?
RequestCorrelationFilter é @ConditionalOnClass(Filter.class) — em uma aplicação sem Servlet API (um worker, por exemplo), o bean simplesmente não é criado, sem erro.
O log de corpo HTTP funciona com qualquer Content-Type?
Não. O log-request-body só processa corpos application/json. Para outros tipos o corpo é ignorado silenciosamente — sem erro, sem log parcial.
Micrometer Tracing e propagação de cabeçalhos se sobrepõem?
Sim, com precedência bem definida: se Micrometer Tracing estiver ativo e houver span corrente, ele vence. Caso contrário, os cabeçalhos de entrada (traceparent, etc.) são inspecionados. Se nenhum estiver presente, um traceId é gerado localmente.
excludeReturnTypes afeta o log de exceção?
Não. A lista só suprime o log de retorno normal (returning). Exceções continuam sendo logadas em ERROR independentemente.
Contribuindo
trilha-log é open source sob licença MIT. Issues e PRs são bem-vindos.
mvn testroda a suíte completa antes de qualquer PR.sample-app/é a aplicação de referência para validar mudanças de ponta a ponta.- Discussões de arquitetura maiores (ex.: split em módulos
core/web/test-support) ficam registradas como issues antes de virar código.