Ship SpringBoot App logs directly to Elastic Search
Logback configuration with Elastic Search Appender
- In this section we will see how to ship the logs directly to Elastic search from a spring boot application.
- We will be using the logback xml with elastic search appender configuration.
- For documentation refer git repo
Eliminate the need for side car containers
- When deploying spring boot application in Kubernetes, if we are using Elastic Search for log monitoring, we can use this appender to log messages directly.
- The advantage of using
elastic search appender
would eliminate the need forside car
container orFluentd
solution to ship the logs to centralized monitor.
ELK stack setup available
- Elastic Search and Kibana configured in the local, and available at
http://localhost:9200/
- Create a simple spring boot application using start.spring.io or IDE.
Include logback-elasticsearch-appender dependency in pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>logdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>logdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.internetitem</groupId>
<artifactId>logback-elasticsearch-appender</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Define elastic search configuration in logback-spring.xml file
Refer Spring logback configuration documentation for mode details.
Another advantage of using logback xml is we can control the log configuration using
springprofile
. Which is also demonstrated here, refer the output section for different profiles.The
logback-spring.xml
file is updated since this will be automatically loaded by spring into context. If the file name is different we need pass the log configuration as Java argument or environment variable.CONSOLE
andELASTIC
appender confiration details- Console will print the content to the IDE console.
- Elastic will ship the logs to Elastic Search.
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true" scan="true">
<springProfile name="!local "> <!-- when the profile is not local, below appender will be applied -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<appender name="ELASTIC" class="com.internetitem.logback.elasticsearch.ElasticsearchAppender">
<url>http://localhost:9200/_bulk</url>
<index>demo-%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>false</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>
<!-- We don't want to see a never ending cascade of connection failed ERRORS -->
<!-- <logger name="my-error-logger" level="OFF" /> -->
<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 anymore -->
</encoder>
</appender>
</logger>
<root level="info">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ELASTIC" />
</root>
</springProfile>
</configuration>
Spring Boot RestController code
- This class uses slf4j loggerfactory to instantiage the logger isntance
- The log message included in getInfo() method will be shipped to Elastic Search.
package com.example.logdemo;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class LogdemoApplication {
//we are using slfj loggerfactory to instantiate the logger
private static final Logger logger = LoggerFactory.getLogger(LogdemoApplication.class);
public static void main(String[] args) {
logger.info("Application started ");
SpringApplication.run(LogdemoApplication.class, args);
logger.info("Application running..");
}
@GetMapping("/info")
public String getInfo() {
// we will be verifying the info logs displayed in Elastic search
logger.info("Get endpoint invoked from client...");
Date date = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
String strDate = dateFormat.format(date);
return "Current time from service :"+strDate;
}
}
Output
Console output with active profile as default
- When I tried to run the Spring boot application from Eclipse IDE, the profile didn't get picked up when passed as JVM argument like
-Dspring.profiles.active=default
. In order to pass the active profile, I had to use the environment variable
SPRING_PROFILES_ACTIVE
- Click
Run Configuration
of the corresponding Spring boot main class. Then click the
Environment
tab, add a environment variable with active profile. Refer below snapshot.
- Click
- Console output:
- Upon application startup, we should be able to see the appender registered.
21:46:16.471 [main] INFO com.example.logdemo.LogdemoApplication - Application started 21:46:22,742 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender] 21:46:22,743 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [CONSOLE] 21:46:22,794 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property 21:46:22,927 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [com.internetitem.logback.elasticsearch.ElasticsearchAppender] 21:46:22,942 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [ELASTIC] 21:46:22,968 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [com.internetitem.logback.elasticsearch.config.ElasticsearchProperties] for [properties] property .... 21:46:23,280 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO 21:46:23,280 |-INFO in ch.qos.logback.classic.jul.LevelChangePropagator@70ab2d48 - Propagating INFO level on Logger[ROOT] onto the JUL framework 21:46:23,281 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [CONSOLE] to Logger[ROOT] 21:46:23,283 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [ELASTIC] to Logger[ROOT] ...
- Upon application startup, we should be able to see the appender registered.
Elastic search output
Now, when we hit the url endpoint, http://localhost:8080/info
, the log message gets shipped to Elastic search.
- All the info level logs should be shipped, in below snapshot I only demonstrated the REST GET endpoint log message.
Output when active profile is set as local
- The configuration doesn't gets applied.
22:21:06.587 [main] INFO com.example.logdemo.LogdemoApplication - Application started 22:21:10,387 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration. 22:21:10,390 |-INFO in org.springframework.boot.logging.logback.SpringBootJoranConfigurator@50d13246 - Registering current configuration as safe fallback point .... 22:21:10,788 |-WARN in Logger[com.example.logdemo.LogdemoApplication] - No appenders present in context [default] for logger [com.example.logdemo.LogdemoApplication].