Monitoraggio di app con lo stack ELK

I file di log sono spesso fonte di problemi nelle applicazioni, perché magari hanno formati strani, perché sono accessibili rimbalzando su diverse macchine etc. etc. Le informazioni contenute nei log sono necessarie per capire cosa sta succedendo al nostro sistema e negli ultimi anni, andando verso un approccio legato ai microservizi, il numero di log delle applicazioni è ovviamente aumentato. Proprio per questo motivo sono state sviluppate diverse soluzioni che permettono una gestione dei log migliore rispetto al passato. Lo stack ELK (Elastic SearchLogstashKibana) è una di queste soluzioni e in questo articolo vedremo come installarlo e farlo dialogare con una nostra applicazione Java.

Overview su ELK

Quello che va sotto il nome di stack ELK sono in realtà tre diversi prodotti che svolgono ruoli diversi per la gestione delle informazioni. Il primo componente è Logstash, software scritto in JRuby che si occupa di gestire i dati provenienti da diverse fonti, di manipolarli e di inviarli verso altri sistemi/file/output. Logstash può quindi leggere il nostro file di log ed inviarlo a Elastic Search, server di ricerca basato su Lucene che espone servizi REST per gestire i dati.

L’ultimo componente di questo stack tecnologico, Kibana, è un applicativo di frontend che si occupa di visualizzare le informazioni presenti su Elastic Search e di aggregarle in diversi modi. Ovviamente questa descrizione è molto sommaria, come sarà l’articolo, visto che ognuno di questi prodotti richiederebbe molto più spazio.

Setup ELK

Tutto lo stack può essere scaricato dai rispettivi download binari sul sito ufficiale. Una volta scaricati possiamo far partire per primo Elastic Search che si occuperà di memorizzare i dati. Per avviare con la configurazione di default basta lanciare il seguente comando in una shell

cd elasticsearch-XXX/bin
./elasticsearch

Per verificare che il server sia in piedi potete anche andare con il vostro browser all’indirizzo http://localhost:9200/ e dovreste avere come ritorno il seguente JSON

{
 "name" : "DqW039D",
 "cluster_name" : "elasticsearch",
 "cluster_uuid" : "3OXYP9BESpCqZJaWUkP7IA",
 "version" : {
   "number" : "5.2.0",
   "build_hash" : "24e05b9",
   "build_date" : "2017-01-24T19:52:35.800Z",
   "build_snapshot" : false,
   "lucene_version" : "6.4.0"
 },
 "tagline" : "You Know, for Search"
}

Ora passate a alla directory dove avete unzippato Kibana e modificate il file config/kibana.yml, settando la variabile elasticsearch.url, facendola puntare alla vostra installazione come di seguito

elasticsearch.url: "http://localhost:9200"

Avviate Kibana e per visualizzare se è partito correttamente aprite l’interfaccia web all’indirizzo http://localhost:5601 . Dovreste trovare un messaggio d’errore relativo all’assenza di un indice, ma giustamente non ci sono ancora dati su Elastic Search quindi cosa dovrebbe farvi vedere Kibana??

Vediamo quindi un hello world che metta in moto tutti e tre i componenti, partendo dalla seguente configurazione Logstash

input {
 generator {
 }
}

output {
   elasticsearch {
   }
}

Con questa configurazione stiamo dicendo a Logstash che in input simulerà un flusso dati attraverso un generatore d’eventi (generator) e riverserà questo flusso di dati generati in output verso elasticsearch (non dobbiamo specificare niente perchè siamo sulla stessa macchina e stiamo utilizzando la porta di default). Salviamo quindi la configurazione in un file helloworld.config e lanciamo Logstash con il seguente comando

bin/logstash -f helloworld.config

In questo modo Logstash sta generando eventi d’esempio e li sta inviando in output ad ElasticSsearch che li memorizza. Accedendo ora all’interfaccia potrete creare l’indice di cui parlavamo prima e visualizzare i dati che stanno arrivando con una visualizzazione simile a quella della seguente immagine

Configurazione con Log4j

Vediamo ora come configurare un’applicazione Java che utilizza Log4j per agganciarla allo stack ELK appena descritto. Le possibilità per realizzare questo collegamento sono svariate e noi ne vedremo due, tramite JSON e tramite Socket. La prima soluzione prevede di loggare le informazioni della nostra applicazione in un formato JSON che possa essere digerito da ElasticSearch. Questo con Log4j è possibile aggiungendo la seguente dipendenza al nostro progetto

<dependency>
    <groupId>net.logstash.log4j</groupId>
    <artifactId>jsonevent-layout</artifactId>
    <version>1.7</version>
</dependency>

Praticamente con questa libreria riusciamo ad avere un output JSON di quello che viene scritto da Log4j. Scriviamo quindi il classico file di configurazione dove andiamo ad aggiungere il layout che è presente in questa libreria

log4j.rootLogger=DEBUG,console

#Console Appender
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=
      [%5p] [%t %d{hh:mm:ss}] (%F:%M:%L) %m%n


#Main Log Appender
log4j.appender.mainLogAppender=org.apache.log4j.RollingFileAppender
log4j.appender.mainLogAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.mainLogAppender.Threshold=INFO
log4j.appender.mainLogAppender.File=prova.log
log4j.appender.mainLogAppender.Append=true
log4j.appender.mainLogAppender.MaxFileSize=5000KB
log4j.appender.mainLogAppender.MaxBackupIndex=10
log4j.appender.mainLogAppender.layout.ConversionPattern=
      %d{DATE} %-5p [%-15c{1}]: %m%n


#JSON appender
log4j.appender.json=org.apache.log4j.DailyRollingFileAppender
log4j.appender.json.File=app.log
log4j.appender.json.DatePattern=.yyyy-MM-dd
log4j.appender.json.layout=net.logstash.log4j.JSONEventLayoutV1
log4j.appender.json.layout.UserFields
      =application:logstashtest,environment:dev

#Custom assignments
log4j.logger.com.javastaff.elk=DEBUG,mainLogAppender,json

#Disable additivity
log4j.additivity.mainLog=false

ed infine un banale main che logga all’infinito

package com.javastaff.elk.logstashtest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class App {

   private static final Logger LOG = LoggerFactory.getLogger(App.class);

   public static void main(String[] args) throws InterruptedException {
      MDC.put("producer", "pippo");
      while(true) {
          LOG.info("Ciao");
          try {
              args[1].equals("ops");
          }
          catch (Exception e) {
              LOG.error(e.toString());
          }
          Thread.currentThread().sleep(1333);
      }
   }
}

Oltre all’utilizzo di un messaggio di INFO e uno di ERROR viene anche utilizzato MDC (Mapped Diagnostic Context) per iniettare una variabile d’ambiente che sarà riportata nei messaggi di log (in questo caso producer=pippo). Ora si può far partire l’applicazione che creerà un log in JSON nel file app.log. Per catturare queste informazioni da Logstash possiamo inserire la seguente configurazione

input {
 file {
    codec => json
    type => "log4j-json"
    path => "/home/federico/progetti/Logstash/app.log"
 }
}

output {
   elasticsearch {
   }
}

Andando sulla console di Kibana vedrete i messaggi della nostra applicazione arrivare (uno normale e uno d’errore). Se volessimo ricevere delle statistiche su quello che sta succedendo nella nostra applicazione potremmo creare una configurazione di Logstash che elabora il log effettuando dei calcoli come nella successiva configurazione

input {
 file {
    codec => json
    type => "log4j-json"
    path => "/home/federico/progetti/Logstash/app.log"
 }
}

filter {
   if [level] == "ERROR" {
      metrics {
          meter => "ERROR"
          add_tag => "metric"
      }
   }
}

output {
   if "metric" in [tags] {
   file {
      codec => line { format => "1m rate: %{[ERROR][rate_1m]} 5m rate: %{[ERROR][rate_5m]} ( %{[ERROR][count]} ) "}
      path => "/var/log/logstash/output.log"
   }
   elasticsearch {
   }
   }
}

Praticamente oltre all’input e all’output esiste la fase di filter, sulla quale possiamo appunto filtrare le informazioni in diversi modi. Per maggiori informazioni su questo argomento vi rimando alla documentazione ufficiale di Logstash. Vediamo ora come sia possibile inviare i dati tramite Socket. Su log4j significa aggiungere un SocketAppender alla configurazione

#TCP appender

log4j.appender.tcp=org.apache.log4j.net.SocketAppender
log4j.appender.tcp.Port=3456
log4j.appender.tcp.RemoteHost=localhost
log4j.appender.tcp.ReconnectionDelay=10000
log4j.appender.tcp.Application=logstashtest

e poi ovviamente dobbiamo cambiare la configurazione di Logstash per dirgli di mettersi in ascolto sulla porta 3456

input {
 log4j {
    mode => "server"
    host => "0.0.0.0"
    port => 3456
    type => "log4j"
 }
}

output {
 file {
      path => "/var/log/logstash/tcpoutput.log"
 }
}

 

Configurazione con Logback

La configurazione di Logback è ovviamente simile a quella di Log4j. Per generare un output JSON verrà utilizzata la seguente libreria

<dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>4.8</version>
</dependency>

Mentre la configurazione è la seguente

<configuration>
   <property name="LOG_PATH" value="app.log" />
   <appender name="jsonAppender" 
         class="ch.qos.logback.core.rolling.RollingFileAppender">
      <File>${LOG_PATH}</File>
      <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
      <rollingPolicy 
         class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
         <maxIndex>1</maxIndex>
         <fileNamePattern>${LOG_PATH}.%i</fileNamePattern>
      </rollingPolicy>
      <triggeringPolicy 
         class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
         <MaxFileSize>1MB</MaxFileSize>
      </triggeringPolicy>
   </appender>
   
   <logger name="jsonLogger" additivity="false" level="DEBUG">
      <appender-ref ref="jsonAppender"/>
   </logger>

   <root level="INFO">
      <appender-ref ref="jsonAppender"/>
   </root>
</configuration>

Il codice d’esempio è esattamente lo stesso in quanto nel progetto d’esempio abbiamo utilizzato SLF4J e in base al profilo Maven che viene lanciato possiamo verificare il progetto con le due diverse librerie di logging. Per quanto riguarda invece la configurazione per la Socket, anche in questo caso andremo a definire una configurazione analoga a quella vista su Log4j

<appender name="STASH" 
      class="net.logstash.logback.appender.LogstashTcpSocketAppender">
   <destination>localhost:3456</destination>
   <encoder 
      class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
   <providers>
      <mdc/>
      <context/>
      <logLevel/>
      <loggerName/>
      <threadName/>
      <message/>
      <logstashMarkers/>
      <arguments/>
   </providers>
   </encoder>
</appender>

e la configurazione di Logstash sarà la seguente

input {
   tcp {
      port => 3456
      codec => "json"
   }
}

output {
   file {
      path => "/var/log/logstash/tcpoutput.log"
   }
}

 

Conclusioni

Lo stack ELK può sicuramente essere utile in moltissime situazioni dove dobbiamo effettuare il monitoraggio di applicazioni, volendo anche di diversa natura. Come detto in apertura, visto l’attuale trend dei microservizi, il monitoraggio di N diverse applicazioni diventa ancora più importante. Ad esempio suddividendo la nostra architettura in N diversi microservizi potremmo utilizzare lo stack ELK per riunire in un unico punto il log delle operazioni, magari utilizzando come identificativo un UUID generato dalla prima applicazione che lo propaga poi verso le altre. In questo modo poi su tutti i log avremmo riportato questa informazione che identifica la chiamata che stiamo effettuando e sul cruscotto di Kibana potremmo poi definire un filtro per visualizzare tutti i log filtrati per questa informazione. Di seguito trovate il link al progetto che contiene i sorgenti contenuti nell’articolo

Progetto d'esempio dell'articolo "Monitoraggio di app con lo stack ELK"
https://github.com/fpaparoni/ELKTest
0 forks.
0 stars.
0 open issues.

Recent commits:

Federico Paparoni

Looking for a right "about me"...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.