Programação funcional: Filtrando logs com simplicidade

Quando não se tem uma pilha ELK (Elasticsearch, Logstach e Kibana) atuando pode ficar complicado analisar logs a fim de obter informações valiosas sobre o comportamento de soluções e muitas vezes o prazo super curtíssimo (coisa de horas) nos obriga a ver algo mais de bate-pronto.

Exemplo honesto. Melhor entendimento da teoria aqui ou DZone com Java.

Bom, para termos um exemplo de uso de programação funcional com Stream no Java 8, sem explicar toda a teoria pois já existem várias postagens do tipo, vamos supor que o nosso setor de métricas pediu o seguinte:
  • Saber quantos erros ocorrem por dia desde 19 de abril de 2017 no componente de integração Horcrux.
  • Agrupar por tipo de erro e informar a quantidade de cada um.
  • A média dos erros por dia.
Sabemos que existem logs que gravam todos os níveis (INFO, ERROR, WARN, TRACE, etc.) no diretório /tmp/my-logs em nosso servidor. OK! Precisamos agora realizar alguns passos sendo o primeiro:
  • Passo 1: Obter todas as entradas que iniciam a partir de 25 de abril de 2017, desconsiderando o stacktrace.
Primeira coisa é sabermos o pattern do timestamp em cada entrada no log. Eis uma entrada de erro justamente na classe Horcrux:
  • 21 Apr 22:02:49,096 ERROR [3035] [Horcrux] org.hibernate.exception.GenericJDBCException: Could not open connection
Veja que o timestamp de exemplo tem como valor 21 Apr 22:02:49,096. então nosso pattern é dd MMM (não precisamos considerar o horário). Todos os nossos logs estão na pasta /tmp/my-logs mas pela simplicidade vou considerar apenas um para depois embrulharmos a leitura de todos por meio de uma única função. Podemos iniciar da seguinte maneira:

Veja que para passar no filtro incluímos também o nome da classe Horcrux e o nível ERROR. Se rodarmos agora o projeto não acontecerá nada pois não existe uma operação terminal como por exemplo forEach ou count, só intermediária que no nosso caso é o filter. Eis o segundo passo:
  • Passo 2: Agrupar por dia.
O operação intermediária filter retorna um Stream, então podemos usar um collect para agrupar por dia, dessa maneira receberemos um Map<LocalDate, List<String>> no final do processo:

Para garantir a ordem natural em função da chave LocalDate podemos informar o uso do TreeMap usando method reference. e então usar uma outra função do Collectors para informar qual estrutura será usada para armazenar as linhas agrupadas, no caso uma lista.

Com isso podemos pensar já sobre o terceiro passo:
  • Passo 3: Para cada dia, agrupar por tipo de erro.
Para agruparmos por tipo de erro em função do exemplo da entrada de erro no log que vimos acima, podemos fazer algo que exclua até o espaço depois do último colchete que circunda Horcrux, então sobraria apenas org.hibernate.exception.GenericJDBCException: Could not open connection. Para concluir isso, vamos usar o Map retornado do exemplo anterior e realizar o corte com Regex (se não executar o find o matcher lançará exception no end):

Quase terminamos, veja que o método retorna Map<String, List<String>>, ou seja, podemos saber a quantidade por tipo de erro lançado no log. Porém precisamos do item final:
  • Passo 4 e final: Com o agrupamento obter a média dos erros no dia.
Como já temos o Map do passo 3, podemos usá-lo para trabalhar com flatMapToInt, assim podemos usar uma série de métodos como average, max, min, sum, count e outros. Podemos tirar a média simplesmente assim:

Um detalhe é que o método ifPresent é da classe Optional, mas no caso o método average retorna um OptionalDouble. Agora temos que ler o que produzimos e jogar no console para checarmos. Juntando tudo fica assim:


Exemplo de saída:


O processo é realizado tudo em um único arquivo e precisamos fazer em todos da pasta /tmp/my-logs. Como o método lines do Files recebe um Path para ler todas as suas linhas, podemos usar outro método dele que é o list que recebe um diretório como argumento para listagem dos arquivos contidos lá. Então ficaria assim:


Veja o quanto foi simples realizarmos a filtragem e trabalharmos com stream! Lógico, é bom ter o entendimento por trás do processo e é por isso que destaquei os pontos importantes para os curiosos em laranja, é vital pesquisar, se aprofundar e depurar. Aliás, coloquei um exemplo do arquivo de log no próprio gist e se não rodar tente configurar o Locale do formatador para EN, com certeza funcionará!

Ao som de Djavan - Aliás.

Comentários