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.
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:
![]() |
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.
- 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void stepOne() throws IOException {
String myAddress = "/tmp/my-logs/my-horcrux-app-2017-05-31.log";
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("dd MMM yyyy");
Files.lines(Paths.get(myAddress)).filter(line -> {
try {
LocalDate myLocalDate = LocalDate.parse(line.substring(0, 6) + " 2017" , myFormat);
return line.contains("[Horcrux]") && line.contains(" ERROR ") && myLocalDate.isAfter(LocalDate.of(2017, Month.APRIL, 19));
} catch (Exception e) {
return false;
}
});
}
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Map<LocalDate, List<String>> stepTwoIncomplete() throws IOException {
String myAddress = "/tmp/my-logs/my-horcrux-app-2017-05-31.log";
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("dd MMM yyyy");
return Files.lines(Paths.get(myAddress)).filter(line -> {
try {
LocalDate myLocalDate = LocalDate.parse(line.substring(0, 6) + " 2017" , myFormat);
return line.contains("[Horcrux]") && line.contains(" ERROR ") && myLocalDate.isAfter(LocalDate.of(2017, Month.APRIL, 19));
} catch (Exception e) {
return false;
}
}).collect(Collectors.groupingBy(l -> {
return LocalDate.parse(l.substring(0, 6) + " 2017" , myFormat);
}));
}
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Map<LocalDate, List<String>> stepTwoComplete() throws IOException {
String myAddress = "/tmp/my-logs/my-horcrux-app-2017-05-31.log";
DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("dd MMM yyyy");
return Files.lines(Paths.get(myAddress)).filter(line -> {
try {
LocalDate myLocalDate = LocalDate.parse(line.substring(0, 6) + " 2017" , myFormat);
return line.contains("[Horcrux]") && line.contains(" ERROR ") && myLocalDate.isAfter(LocalDate.of(2017, Month.APRIL, 19));
} catch (Exception e) {
return false;
}
}).collect(Collectors.groupingBy(l -> {
return LocalDate.parse(l.substring(0, 6) + " 2017" , myFormat);
}, TreeMap::new, Collectors.toList()));
}
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):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void stepThree(Map<LocalDate, List<String>> myMapFromStepTwoComplete) {
Pattern myPattern = Pattern.compile(".*\\[Horcrux\\]");
myMapFromStepTwoComplete.keySet().stream()
.forEach(k -> {
Map<String, List<String>> myGroupedErrors = myMapFromStepTwoComplete.get(k).stream().collect(Collectors.groupingBy(s -> {
Matcher m = myPattern.matcher(s);
m.find();
return s.substring(m.end()+1, s.length());
}));
});
}
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void stepFour(Map<String, List<String>> myMapFromStepThree) {
myMapFromStepThree.keySet().stream().flatMapToInt(exceptionType -> IntStream.of(myMapFromStepThree.get(exceptionType).size()))
.average().ifPresent(average -> System.out.println("The average is " + average));
}
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private static String myAddressDirectory = "/tmp/my-logs/";
private static String myAddressSingleFile = "/tmp/my-logs/my-horcrux-app-2017-05-31.log";
private static Pattern myPattern = Pattern.compile(".*\\[Horcrux\\]");
private static DateTimeFormatter myFormat = DateTimeFormatter.ofPattern("dd MMM yyyy");
public static void main(String args[]) throws Exception {
// Step one and two
Map<LocalDate, List<String>> myByLocalDateMap = Files.lines(Paths.get(myAddressSingleFile)).filter(line -> {
try {
LocalDate myLocalDate = LocalDate.parse(line.substring(0, 6) + " 2017" , myFormat);
return line.contains("[Horcrux]") && line.contains(" ERROR ") && myLocalDate.isAfter(LocalDate.of(2017, Month.APRIL, 19));
} catch (Exception e) {
return false;
}
}).collect(Collectors.groupingBy(l -> {
return LocalDate.parse(l.substring(0, 6) + " 2017" , myFormat);
}, TreeMap::new, Collectors.toList()));
// Step three
myByLocalDateMap.keySet().stream().forEach(localDate -> {
System.out.println("#### Details regarding " + localDate);
Map<String, List<String>> myLinesByErrors = groupByErrors(myByLocalDateMap, localDate);
myLinesByErrors.keySet().stream().forEach(exceptionType -> {
System.out.println(String.format("\t- Count for \"%s\": %s", exceptionType, myLinesByErrors.get(exceptionType).size()));
});
// Step four
myLinesByErrors.keySet().stream().flatMapToInt(exceptionType -> IntStream.of(myLinesByErrors.get(exceptionType).size()))
.average().ifPresent(average -> System.out.println("\t- The average is " + average));
});
// If you want to read each log file contained in the folder
Files.list(Paths.get(myAddressDirectory)).forEach(eachLog -> {
// Your implementation here! :)
});
}
private static Map<String, List<String>> groupByErrors(Map<LocalDate, List<String>> byLocalDateMap, LocalDate localDate) {
return byLocalDateMap.get(localDate).stream().collect(Collectors.groupingBy(s -> {
Matcher m = myPattern.matcher(s);
m.find();
return s.substring(m.end()+1, s.length());
}));
}
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// If you want to read each log file contained in the folder
Files.list(Paths.get(myAddressDirectory)).forEach(eachLog -> {
// Your implementation here! :)
});
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
Postar um comentário