Pular para o conteúdo

OpsCanopy 5 min de leitura

Escrevendo expressões regulares robustas para linhas de log

Um guia prático para construir regexes que fazem o parsing de linhas de log de forma confiável — ancoragem, grupos de captura, escape, gulosidade e os modos de falha que te pegam em produção.

  • regex
  • logs
  • parsing

Compartilhar
Nesta página

Expressões regulares robustas para fazer o parsing de linhas de log com grupos de captura nomeados

Uma expressão regular que faz o parsing de uma linha de log no seu editor e uma expressão regular que sobrevive a uma semana de tráfego real raramente são a mesma expressão. Os logs são mais barulhentos do que as três linhas de exemplo contra as quais você testou: timestamps mudam de formato, campos ficam ausentes, um caminho sem escape introduz um metacaractere no seu padrão, e um .* que parecia inofensivo silenciosamente devora metade da linha. Este post percorre as técnicas que tornam uma regex de linha de log robusta — e os modos de falha que pegam as pessoas de surpresa.

Comece pela estrutura, não pelo exemplo

Section 1 of 7 · ~1 min

A maioria das linhas de log é mais estruturada do que parece. Antes de recorrer ao .*, dê nome aos campos que você realmente quer e ao texto literal que os separa. Uma linha típica no estilo de acesso —

2026-06-08T10:14:22Z INFO  api request_id=8f3a method=GET path=/v1/users status=200 dur=42ms

— é um timestamp, um nível, e então um conjunto de pares key=value. Faça a correspondência com o formato diretamente em vez de torcer para que um padrão frouxo acerte a substring certa:

^(?<ts>\S+)\s+(?<level>\w+)\s+.*\bstatus=(?<status>\d{3})\b

Aqui o \S+ para o timestamp é deliberado: ele corresponde ao token inteiro sem que você precise codificar cada variante de timestamp. O \bstatus=(?<status>\d{3})\b fixa o campo a um limite de palavra para que ele não possa acidentalmente corresponder a http_status= ou a um status embutido em outro token.

Uma linha de log com uma expressão regular, mostrando grupos de captura nomeados correspondendo aos segmentos de timestamp, nível e mensagem

Ancore sempre que puder

Section 2 of 7 · ~1 min

Um padrão sem âncora pode corresponder em qualquer lugar da linha, o que é ao mesmo tempo mais lento e mais surpreendente. Se uma linha deve sempre começar com um timestamp, diga isso com ^. Se você está fazendo a correspondência de uma linha inteira, ancore ambas as pontas com ^…$. A ancoragem transforma “encontre isto em algum lugar” em “a linha se parece exatamente com isto”, que normalmente é o que você quer dizer — e faz com que uma linha que não corresponde falhe rapidamente em vez de fazer backtracking pela string inteira.

^(?<ip>\d{1,3}(?:\.\d{1,3}){3})\s+\S+\s+\S+\s+\[(?<when>[^\]]+)\]

Note o [^\]]+ para o timestamp entre colchetes em vez de .+: uma classe de caracteres negada diz “tudo até o colchete de fechamento” sem os jogos de gulosidade descritos abaixo.

Domine a gulosidade com classes negadas e quantificadores preguiçosos

Section 3 of 7 · ~1 min

.* e .+ são gulosos: eles agarram o máximo possível e só devolvem caracteres quando forçados. Ao longo de uma linha longa com delimitadores repetidos, esse backtracking é a origem tanto das correspondências erradas quanto das lentidões catastróficas.

Considere extrair a mensagem de um campo entre aspas:

msg="(?<msg>.*)"

Em uma linha com dois campos entre aspas, o .* corresponde através de ambos, engolindo a aspa de fechamento do primeiro e a aspa de abertura do segundo. Duas correções confiáveis — prefira a primeira:

msg="(?<msg>[^"]*)"     # negated class: stop at the next quote
msg="(?<msg>.*?)"       # lazy quantifier: as few chars as possible

A classe negada [^"]* é normalmente mais rápida e mais clara do que o .*? preguiçoso porque nunca precisa fazer backtracking — ela simplesmente não consegue cruzar uma aspa para começar. Recorra a uma classe de caracteres negada antes de um quantificador preguiçoso sempre que um único delimitador encerrar o campo.

Faça escape de metacaracteres literais

Section 4 of 7 · ~1 min

As linhas de log estão cheias de caracteres que significam algo para um motor de regex: . em IPs e hostnames, ? e + em URLs, [ ] em muitos formatos de timestamp, ( ) em stack traces. Corresponder a eles literalmente significa fazer escape deles.

path=/v1/users\?page=2     # the ? is a literal query separator, not "optional"
\[ERROR\]                  # literal square brackets around the level
\(timeout\)                # literal parentheses, not a group

Uma regra prática rápida: se você está copiando uma substring literal de uma linha de log real para o seu padrão, faça escape de cada . ^ $ * + ? ( ) [ ] { } | \ que ela contém. O custo de um . sem escape é que ele corresponde a qualquer caractere, então 10.0.0.1 também vai corresponder a 10x0y0z1 — raramente o que você quer quando está tentando validar uma entrada.

Torne os campos opcionais realmente opcionais

Section 5 of 7 · ~1 min

Logs reais omitem campos. Uma requisição sem um usuário ainda é uma requisição, e o seu padrão não deveria falhar com ela. Envolva a parte variável em um grupo de não captura com ?:

^(?<ts>\S+)\s+(?<level>\w+)(?:\s+user=(?<user>\S+))?\s+path=(?<path>\S+)

O (?:…)? torna toda a cláusula user= opcional sem poluir seus grupos de captura. Prefira grupos de não captura (?:…) para trabalho que envolve apenas agrupamento, de modo que suas capturas numeradas/nomeadas continuem significativas.

Prefira grupos nomeados e conheça suas flags

Section 6 of 7 · ~1 min

Grupos nomeados ((?<status>…)) são muito mais legíveis do que \1, \2 seis meses depois, e eles sobrevivem a alguém inserindo um novo grupo no meio do padrão. Duas flags importam constantemente para logs:

  • Insensível a maiúsculas/minúsculas (i): níveis aparecem como ERROR, error, Error. Corresponda com (?i) ou com a flag do motor em vez de soletrar [Ee][Rr][Rr][Oo][Rr].
  • Multiline (m): quando você cola um bloco de logs, ^ e $ devem ancorar a cada linha, não ao blob inteiro. Com a flag multiline, ^(?<level>\w+) testa cada linha independentemente.
(?im)^(?<ts>\S+)\s+(?<level>error|warn|info|debug)\b

Teste contra as linhas que quebram as coisas

Section 7 of 7 · ~1 min

O exemplo que prova que sua regex funciona raramente é o exemplo que prova que ela é robusta. Monte um pequeno conjunto de entradas adversárias e mantenha-o por perto: uma linha sem o campo opcional, uma linha com duas strings entre aspas, uma mensagem contendo o delimitador pelo qual você dividiu, um timestamp malformado, uma linha vazia, e uma linha que tem o dobro do comprimento usual. Se o seu padrão sobreviver a essas, ele vai sobreviver à produção.

Esse é exatamente o ciclo para o qual o Regex Log Tester foi construído: cole seu padrão e um bloco de linhas de log reais, e veja ao vivo quais linhas correspondem, quais não correspondem, e o que cada grupo de captura e grupo nomeado de fato capturou — para que você pegue o .* guloso ou o . sem escape antes que ele vá para produção. Tudo roda no seu navegador; seus logs nunca saem da página.

Abrir o Regex Log Tester →