Visualize structured logs in ELK stack

By default, the logging frameworks outputs the traces in plain text and tools like ELK can’t fully process these traces. We could workaround the issue by adapting the messages using Logstash, but if we are using another tool like Prometheus, we would need to workaround it there as well. Therefore, if we “structure” or send the traces in JSON format directly, all the tools can benefit of.

In this tutorial, we’ll see how to visualize these traces in Kibana (using ELK stack).

Start ELK stack

We’ll be using docker compose to start-up the whole ELK stack.

First, we need to configure our logstash pipeline:

  • logstash.conf: let’s listen the file system as input; and send the logs to elasticsearch as only output.
input {
    file {
        path => "/tmp/logs/*.log"
    }
}

output {
    elasticsearch {
        hosts => [ "elasticsearch:9200" ]
        index => "logback"
    }
}
  • docker-compose.yml:
version: "3"

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    networks:
      - logging-network

  logstash:
    image: docker.elastic.co/logstash/logstash:7.1.1
    depends_on:
      - elasticsearch
    ports:
      - 12201:12201/udp
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro
      - /tmp/logs:/tmp/logs
    networks:
      - logging-network

  kibana:
    image: docker.elastic.co/kibana/kibana:7.1.1
    depends_on:
      - logstash
    ports:
      - 5601:5601
    networks:
      - logging-network

networks:
  logging-network:
    driver: bridge
  • Run docker compose:
> docker-compose up

Now, we can browse our traces in our local Kibana.

Start-Up our app

The only thing we would need to do here is to output the traces in the folder that logstash is listening (see above).

logback.xml:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/tmp/logs/app.log</file>
    <encoder>
        <pattern>%d{dd MMM yyyy HH:mm:ss,SSS} %-5p [%t] %c %logger{36} - %msg%n</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/tmp/logs/app.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
    </rollingPolicy>
</appender>

<root level="INFO">
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
</root>

Configure Kibana to visualize logs

  • First, we need to create a new index: follow the instructions here
  • And then we can now see our traces in the Discovery page in Kibana! This is how looks like now:

Kibana Logs

As we can see, the traces are NOT structured. They are still plain messages. Let’s do it!

Structure the traces

With logback and Spring, this is extremely easy:

1.- Add the logstash maven dependency in our pom.xml:

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

2.- Configure the appender to use the new logstash encoder:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/tmp/logs/app.log</file>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> <!-- New encoder! -->
    <rollingPolicy
        class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/tmp/logs/app.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
    </rollingPolicy>
</appender>

Now, we’re sending writting traces in JSON format and logstash is reading these traces from the output file.

Mastering Logs

If we’re building a services app, another really well-designed framework is Logbook by Zalando. This framework will output all the information about requests/responses done within our application. So, every request/response made to our app or done to a third party by our app will be traced. This framework is really well integrated with Spring Boot and HTTP utilities like RestRemplate, HttpClient or OkHttp with minor tweaking.

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>${logbook.version}</version>
</dependency>

So, we’re saying that can trace in and out calls just adding the above dependency in our Spring Boot application, so it would be great if we could correlate all these in/out calls somehow. Let’s embrace Spring Cloud Sleuth that will do exactly this for us. To enable it, it’s just about to add the dependency in the pom.xml as well:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

This dependency will add the “traceId” and “correlation” fields to the trace.

Another nice addition is to structure the Logbook traces a deeper level using Logstash:

logstash.conf:

input {
    ...
}

filter {
    if [logger_name] == "org.zalando.logbook.Logbook" {
        json {
            source => "message"
        }
    }
}

output {
    ...
}

Now, we can see lot of new fields in Kibana like the http.origin, or http.method or http.request. Really useful to troubleshoot issues.

StructuredArguments: Adding new fields when logging traces

The logstash encoder dependency adds a very simple way to add new fields just adding:

import static net.logstash.logback.argument.StructuredArguments.keyValue;

int size = 10;
log.info("Listing messages {}", keyValue("size", size));

However, using this dependency is not the only way to add custom fields, indeed the standard approach is using the MDC (Mappend Diagnostic Context):

MDC.put("size", size);
log.info("Listing messages ");
MDC.remove("size");
try (var ignored = MDC.putCloseable("size", size)) {
  log.info("Listing messages ");
}

Finally, we need to tell the logstash encoder to include the structured arguments in the logback.xml:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/tmp/logs/app.log</file>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"> 
        <provider class="net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider"/><!-- New! -->
    </encoder>
    <rollingPolicy
        class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>/tmp/logs/app.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
    </rollingPolicy>
</appender>

And now, we can see the new field available in Kibana:

New Field

Update: At latest version of the logstash encoder, you don’t need to provide the provider in order to see the MDC attributes. By default, all the fields will be output.

Conclusion

In this tutorial, we learnt how to structure and master the traces so they can be easily processed by ELK stack and any other analytic tools. However, JSON has also some limitations like limited length size or typeless, and that’s why GELF cames in to help. I never used GELF before and I think this would be good for a future post rather.

For composing the docker files and configuration, I follow this repository instructions. Also, this [post] was very useful to approach this guide.

[ Design Patterns, ELK ]