Implementar a interface de logs em cada classe.
Utilize um objeto estático para garantir reuso para todas as instâncias
private static final Logger log = LoggerFactory.getLogger(NomeDaClasse.class);
Implementar os logs conforme necessário
// o método permite interpolar certo conteúdo com o log
log.info("mensagem do log - parametro: {}", objeto);
log.info("mensagem do log - p1: {}, p2: {}", arg1, arg2);
// logs de debug
log.debug("agora somando os dois números: {} + {} = {}", v1, v2, v3);
// exemplo de exceção
log.error("mensagem de erro - detalhe da exceção: {}", ex);
Usar uma biblioteca específica
No hands on vamos usar o LogBack, e as suas dependências são as seguintes:
<!-- LogBack core, possui as funcionalidades básicas de escrita e formatação -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.5.6</version>
</dependency>
<!-- funções mais sofisticadas de logging, como formatação em JSON -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
Usar MDC
para adicionar dados de contexto.
Para aplicações Web/API, você pode usar um interceptor nativo do framework - ex: HandlerInterceptor
do Spring (Java); já aplicações "worker", como batch ou que recebem estímulos de fila ou tópico, você pode usar fazer o enriquecimento do MDC no primeiro ponto de contato da aplicação - ex: classe/método main()
@Component
class RequestMdcInterceptor implements HandlerInterceptor {
// capturando propriedades geradas no build
@Autowired
private BuildProperties buildProperties;
@Override
public boolean preHandle(
final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) throws Exception {
MDC.put("appName", buildProperties.getName());
MDC.put("appVersion", buildProperties.getVersion());
MDC.put("appBuildDate", buildProperties.getTime().toString());
MDC.put("traceId", request.getHeader("x-trace-id"));
MDC.put("host", request.getHeader("Host"));
return true;
}
}
// no caso do Spring, é necessário registrar o intereptor para ser reconhecido
@Configuration
class InterceptorsAdapterConfig implements WebMvcConfigurer {
@Autowired
private RequestMdcInterceptor requestMdcInterceptor;
@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(requestMdcInterceptor);
WebMvcConfigurer.super.addInterceptors(registry);
}
}
Para capturar os dados de build através da classe BuildProperties
, é necessário incluir mais alguns dados de build no plugin do maven.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<!-- execution para gerar o arquivo de metadados
que a classe BuildProperties vai ler -->
<execution>
<id>build-info</id>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Aplicar as configurações de formato e destino dos logs.
No caso do LogBack, uma das formas de instrumentação é através da parametrização do arquivo src/main/resources/logback.xml
, ex:
<configuration>
<!-- é possível importar as classes em vez de referenciar toda vez o caminho completo -->
<import class="ch.qos.logback.core.ConsoleAppender" />
<import class="ch.qos.logback.core.FileAppender" />
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder" />
<import class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder" />
<timestamp key="timestamp" datePattern="yyyyMMdd'T'HHmm"/>
<!-- appender específico para arquivo -->
<appender name="FILE" class="FileAppender">
<!-- local onde o arquivo será gravado
OBS: é possível adicionar varávis de ambiente para customizar o nome e o local do arquivo gerado (ex: data/hora)
-->
<file>logs/application-${timestamp}.log</file>
<encoder class="PatternLayoutEncoder">
<!--
pattern de escrita de cada linha de log
* %d{HH:mm:ss.SSSZZ} - timestamp do evento de log
* [%thread] - thread da aplicação em que o log foi gerado
* %-5level - nível de log (ERROR, INFO, DEBUG, etc.)
* %X{traceId} - variável específica "traceID" no MDC
poderia colocar %X para exibir todas
as variáveis
* %X{host} - variável específica "host" no MDC
poderia colocar %X para exibir todas
as variáveis
* %logger{36} - classe em que o log foi gerado
nome limitado a 36 caracteres
* %msg%n - mensagem acompanhada de exceção + stack trace
-->
<pattern>
%d{HH:mm:ss.SSSZZ} [%thread] %-5level %X{traceId} %X{host} %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<!-- é possível ter múltiplos appenders -->
<appender name="STDOUT_PLAIN" class="ConsoleAppender">
<encoder class="PatternLayoutEncoder">
<!-- ... e com formatos diferentes -->
<pattern>
%d{HH:mm:ss.SSSZZ} [%thread] %-5level %X %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="STDOUT_JSON" class="ConsoleAppender">
<!-- classe net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder permite usar formato JSON -->
<encoder class="LoggingEventCompositeJsonEncoder">
<providers>
<!-- a biblioteca conhece o campo timestamp... -->
<timestamp>
<!-- ... mas é possível customizar o nome e até o formato -->
<fieldName>ts</fieldName>
<timeZone>UTC</timeZone>
</timestamp>
<loggerName>
<fieldName>logger</fieldName>
</loggerName>
<logLevel>
<fieldName>level</fieldName>
</logLevel>
<callerData>
<classFieldName>class</classFieldName>
<methodFieldName>method</methodFieldName>
<lineFieldName>line</lineFieldName>
<fileFieldName>file</fileFieldName>
</callerData>
<threadName>
<fieldName>thread</fieldName>
</threadName>
<mdc />
<stackTrace>
<fieldName>stack</fieldName>
</stackTrace>
<message>
<fieldName>msg</fieldName>
</message>
<!--
é possível capturar argumentos dos métodos em que o log foi registrado
OBS: cuidado com as políticas de segurança e privacidade de dados -->
<arguments>
<includeNonStructuredArguments>true</includeNonStructuredArguments>
<nonStructuredArgumentsFieldPrefix>argument:</nonStructuredArgumentsFieldPrefix>
</arguments>
</providers>
</encoder>
</appender>
<!--
mesmo tendo uma configuração global de nível de log,
é possível customizar os níveis segundo o package de cada
classe que estive logando -->
<logger name="ch.qos.logback" level="WARN" />
<logger name="org.mortbay.log" level="WARN" />
<logger name="org.springframework" level="INFO" />
<logger name="org.springframework.beans" level="WARN" />
<!-- por exemplo para a aplicação estou habilitando a opção de DEBUG -->
<logger name="store" level="DEBUG" />
<!--
aqui é onde você habilita/desabilita quais Appenders vai utilizar
o que facilita a manutenção, sem ter que ficar movendo código -->
<root level="INFO">
<!-- OBS: Appender STDOUT_PLAIN está desativado-->
<!--<appender-ref ref="STDOUT_PLAIN" />-->
<!-- é possível utilizar vários Appenders simultaneamente -->
<appender-ref ref="STDOUT_JSON" />
<appender-ref ref="FILE" />
</root>
</configuration>
Suba a infra do Splunk, conforme exemplos na documentação: https://splunk.github.io/docker-splunk/EXAMPLES.html
Realize os procedimentos a seguir e anote:
- token do HTTP Collector
- índice que será usado na busca
- vá para o menu
Settings
- no grupo
Data
, vá paraIndexes
- clique em
New Index
- informe o nome do índice em
Index Name
(ex: logs_vendorservice, raw_salesapp) - parametrize demais dados que fizerem sentido, ou apenas deixe tudo como default
- clique em
Save
- vá para o menu
Settings
- no grupo
Data
, vá paraData inputs
- em
Local inputs
>Type
, clique em+ Add new
na opçãoHTTP Event Collector
- informe o nome em
Name
que seja possível identificar (sugestão: utilize um nome similar ao índice para facilitar a gestão) - clique em
Next
- selecione o índice default (criado anteriormente)
- clique em
Review
e depois emSubmit
Configure o LogDriver para a aplicação. Exemplo utilizando docker-compose.yml
:
services:
app:
#...
logging:
driver: splunk
options:
splunk-url: http://<host>:8088
splunk-index: <index name>
splunk-token: <token xxxx-yyyy-zzzz>
splunk-source: <app name>
splunk-format: "json"
splunk-insecureskipverify: "true"
splunk-verify-connection: "false"
Vá para Apps
> Searching & Reporting
e faça uma busca no splunk.
index=logs_storeapp
| spath
| search properties.appName=store-observability properties.appVersion="0.0.*" severity=ERROR
Instale o pluging do Elastic Search para integrá-lo ao log driver.
docker plugin install elastic/elastic-logging-plugin:8.7.1
Suba a infra do ELK conforme a documentação oficial: https://www.elastic.co/blog/getting-started-with-the-elastic-stack-and-docker-compose
Esse procedimento irá provisionar a infraestrutura com Logstash, Elastic Search e Kibana.
Configure o LogDriver para a aplicação no docker-compose.yml
, utilizando os parâmetros obtidos na instalação do ELK.
services:
app:
logging:
driver: elastic/elastic-logging-plugin:8.7.1
options:
hosts: https://localhost:9200
user: elastic
password: changeme
index: storeapp
ref: https://github.com/splunk/splunk-library-javalogging/blob/main/src/test/resources/logback.xml
Além das bibliotecas do Logstash que adicionamos anteriormente, inclus a biblioteca de logging do Splunk no pom.xml
:
<dependency>
<groupId>com.splunk.logging</groupId>
<artifactId>splunk-library-javalogging</artifactId>
<version>1.9.0</version>
</dependency>
Aparentemente o Splunk não está disponível mais no repositório do Maven. Para baixar suas bibliotecas, é necessário incluir o link do repositório do Artifactory do Splunk no arquivo de .m2/settings.xml
ou no próprio pom.xml
:
<repositories>
<!-- repositório do maven (padrão) -->
<repository>
<id>maven2</id>
<name>Maven 2</name>
<url>https://repo.maven.apache.org/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!-- repositório adicional: Splunk -->
<repository>
<id>splunk</id>
<name>Splunk Artifactory</name>
<url>https://splunk.jfrog.io/splunk/ext-releases-local</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
Configure o arquivo logback.xml
da seguinte maneira:
<configuration>
<import class="net.logstash.logback.layout.LogstashLayout" />
<import class="com.splunk.logging.HttpEventCollectorLogbackAppender" />
<appender name="splunk_http" class="HttpEventCollectorLogbackAppender">
<url>http://<HOST>:8088</url>
<token>TOKEN</token>
<sourcetype>logback</sourcetype>
<index>INDICE</index>
<messageFormat>json</messageFormat>
<middleware>HttpEventCollectorUnitTestMiddleware</middleware>
<connectTimeout>5000</connectTimeout>
<terminationTimeout>2000</terminationTimeout>
<layout class="LogstashLayout" />
</appender>
<root level="INFO">
<appender-ref ref="splunk_http" />
</root>
</configuration>
OBS: os parâmetros <url>
, <token>
e <index>
devem ser obtidos conforme ambiente do Spluk. Especificamente para o token e index, siga o passo a passo a seguir.
- vá para o menu
Settings
- no grupo
Data
, vá paraIndexes
- clique em
New Index
- informe o nome do índice em
Index Name
(ex: logs_vendorservice, raw_salesapp) - parametrize demais dados que fizerem sentido, ou apenas deixe tudo como default
- clique em
Save
- vá para o menu
Settings
- no grupo
Data
, vá paraData inputs
- em
Local inputs
>Type
, clique em+ Add new
na opçãoHTTP Event Collector
- informe o nome em
Name
que seja possível identificar (sugestão: utilize um nome similar ao índice para facilitar a gestão) - clique em
Next
- selecione o índice default (criado anteriormente)
- clique em
Review
e depois emSubmit
Vá para Apps
> Searching & Reporting
e faça uma busca no splunk.
index=logs_storeapp
| spath
| search properties.appName=store-observability properties.appVersion="0.0.*" severity=ERROR
ref: https://github.com/internetitem/logback-elasticsearch-appender
Instale a dependência no pom.xml
<!-- logback appender - ElasticSearch -->
<dependency>
<groupId>com.internetitem</groupId>
<artifactId>logback-elasticsearch-appender</artifactId>
<version>1.6</version>
</dependency>
Configure o appender no logback.xml
<configuration>
<import class="com.internetitem.logback.elasticsearch.ElasticsearchAppender" />
<appender name="ELASTIC" class="ElasticsearchAppender">
<url>https://localhost:9200</url>
<index>logs-%date{yyyy-MM-dd}</index>
<type>tester</type>
<loggerName>es-logger</loggerName> <!-- optional -->
<errorLoggerName>es-error-logger</errorLoggerName> <!-- optional -->
<connectTimeout>30000</connectTimeout> <!-- optional (in ms, default 30000) -->
<errorsToStderr>false</errorsToStderr> <!-- optional (default false) -->
<includeCallerData>false</includeCallerData> <!-- optional (default false) -->
<logsToStderr>false</logsToStderr> <!-- optional (default false) -->
<maxQueueSize>104857600</maxQueueSize> <!-- optional (default 104857600) -->
<maxRetries>3</maxRetries> <!-- optional (default 3) -->
<readTimeout>30000</readTimeout> <!-- optional (in ms, default 30000) -->
<sleepTime>250</sleepTime> <!-- optional (in ms, default 250) -->
<rawJsonMessage>false</rawJsonMessage> <!-- optional (default false) -->
<includeMdc>true</includeMdc> <!-- optional (default false) -->
<maxMessageSize>100</maxMessageSize> <!-- optional (default -1 -->
<authentication class="com.internetitem.logback.elasticsearch.config.BasicAuthentication" /> <!-- optional -->
<properties>
<property>
<name>host</name>
<value>${HOSTNAME}</value>
<allowEmpty>false</allowEmpty>
</property>
<property>
<name>severity</name>
<value>%level</value>
</property>
<property>
<name>thread</name>
<value>%thread</value>
</property>
<property>
<name>stacktrace</name>
<value>%ex</value>
</property>
<property>
<name>logger</name>
<value>%logger</value>
</property>
</properties>
<headers>
<header>
<name>Content-Type</name>
<value>application/json</value>
</header>
</headers>
</appender>
<logger name="es-logger" level="INFO" additivity="false">
<appender name="ES_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- ... -->
<encoder>
<pattern>%msg</pattern> <!-- This pattern is important, otherwise it won't be the raw Elasticsearch format anyomre -->
</encoder>
</appender>
</logger>
<root level="info">
<appender-ref ref="ELASTIC" />
</root>
</configuration>