Português (BR)
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
Nesta página
- Comece pela estrutura, não pelo exemplo
- Ancore sempre que puder
- Domine a gulosidade com classes negadas e quantificadores preguiçosos
- Faça escape de metacaracteres literais
- Torne os campos opcionais realmente opcionais
- Prefira grupos nomeados e conheça suas flags
- Teste contra as linhas que quebram as coisas
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
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.
Ancore sempre que puder
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
.* 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
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
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
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 comoERROR,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
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.
Posts relacionados
-
8 min de leitura
Aprenda DevOps em 90 dias: o caminho gratuito e guiado por incidentes de desenvolvedor a engenheiro DevOps
Por que criamos o Mission: 90 Days DevOps — um caminho gratuito, dia a dia, do Linux ao Kubernetes com missões de incidente jogáveis. O plano completo, as decisões de design e os tradeoffs honestos.
-
13 min de leitura
Por Que Meu Alerta Não Chega ao Receiver Certo? Depurando o Roteamento do Alertmanager
Alertas indo para o receiver errado, ou para nenhum receiver? Depure o roteamento do Alertmanager — primeiro match vence, continue ausente, regex de matcher e defaults catch-all.
-
11 min de leitura
Como funciona o roteamento do Alertmanager: matchers, continue e a árvore de rotas
Um modelo mental claro para o roteamento do Alertmanager — a árvore de rotas, os matchers, a flag continue, o agrupamento e a herança de receivers — para você saber exatamente para onde cada alerta vai.