Post Cover

Writing Docker Container Logs to MySQL using Fluentd

Docker Posted Jul 13, 2021

Logging is one of the important aspects of any application that deserves an efficient design and implementation. It is the only source that developers can find helpful in troubleshooting issues, once the code is deployed and running in the servers. And when the application is deployed in a containerized environment, logging is even more important since its tough to get into the container and find out what's happening in it.

To solve this, Docker provides logging drivers which can plug into any of the output interfaces available and help push logs to the integrated components as configured. In this example, let's look at how we can configure and push our Docker container logs into Fluentd.

"Fluentd is an open source data collector which can help in building a centralized and unified logging layer for the Docker container system."

To better explain how this is done, let's take an API written in ASP.NET Core which pushes out trace logs and error logs to the stdout and is deployed into a docker container.

Logging at Source - Some API implementation in ASP.NET Core:

The API has its logging mechanism configured to push into the stdout as below:

services.AddLogging(builder =>
{
    builder.AddConsole();
});

To test this, we'd have a controller in place which would log the response onto the Logger.

namespace SomeApi.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AliveController : ControllerBase
    {
        private ILogger<AliveController> _logger;

        public AliveController(ILogger<AliveController> logger)
        {
            _logger = logger;
        }

        public string Get()
        {
            var response = $"
                I'm Alive! Here's a Guid for you {Guid.NewGuid().ToString()}";
            
            _logger.LogInformation(response);
            return response;
        }
    }
}

The Dockerfile for the API to be deployed into a docker container is created as follows:

FROM mcr.microsoft.com/dotnet/core/sdk:5.0 AS build
WORKDIR /app

COPY *.csproj .

RUN dotnet restore 

COPY . .

RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:5.0 as runtime

WORKDIR /app

COPY --from=build /app/out .

ENTRYPOINT [ "dotnet", "SomeApi.dll" ]

*Learn More: Dockerizing a Simple ASP.NET Core Application for Release Build

Note: If you're not into ASP.NET Core, no issues; we've just taken this as an example for our experiment. You can replace this with any framework or code that suits your purpose.

At this point, we have a working API that is running in a docker container. Next we have to link this docker container to have its logs pushed into the Fluentd system.

Setting up Fluentd Logging Driver:

To have the docker container pass on its logs to Fluentd, we need to configure the logging driver for the container and set it to fluentd. Docker supports several logging drivers which are a kind of known interfaces for the docker that it can use to connect to the particular external system.

In our case, we use fluentd driver that is supported by Docker. When we configure this logging driver, Docker ensures all the transformation needed for the logs that it being generated by the container and pushes it to the fluentd system that is available. We do all this within a Docker-Compose file so as to group all these components together within a system.

The API part of the compose file looks like below:

version: '3'
services:
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile
        ports:
            - 5000:80
        logging: 
            driver: fluentd
            options: 
                tag: "docker.{{.ID}}"
        depends_on: 
            - "logger"

observe that we have a section called "logging" and under which we specify the "driver" as "fluentd" and within the options we provide a "tag" format for every log to be tagged by the container Id of the container from which the log is being generated. This helps us in identifying which log has come from which container.

This completes our API configuration, which is our source of logs for this system. In the next step we deal with setting up fluentd.

Setting up Fluentd Listener:

Fluentd system is available in various forms for different operating systems, and is also available as a Docker image which can be run in parallel as a container. For our experiment, we'd pick the docker container for Fluentd and configure it to suit our requirement.

Fluentd requires configuration about the port and host in which it'd listen on, how it'd process the logs and so on in the form of a conf file, which we add to the fluentd system. fluentd uses this and listens on to the port for any incoming log streams and processes it out accordingly. For our case, we'd initially set the fluentd to print all the incoming logs into the stdout. The fluentd.conf which contains this configuration looks like below:

#fluentd.conf#

<source>
  type forward
  port 24224
  bind 0.0.0.0
</source>

<match *.*>
  @type stdout
</match>

we'll update the docker-compose file that we've created before to contain the fluentd container configuration. The file now looks like below:

version: '3'
services:
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile
        ports:
            - 5000:80
        logging: 
            driver: fluentd
            options: 
                tag: "docker.{{.ID}}"
        depends_on: 
            - "logger"
    logger:
        image: fluent/fluentd:edge-debian
        volumes: 
            - ./logger/tmp:/fluentd/etc
        command: '-c /fluentd/etc/fluentd.conf -v'
        ports: 
            - 24224:24224
            - 24224:24224/udp

In this "logger" container that runs "fluentd", we've configured to setup a volume that shares the fluentd.conf from the local folder. This is to have the flexibility to change the configuration when needed. The container is bound to run on ports 24224, which is the default port for fluentd.

The API container which is configured with fluentd log driver constantly pushes the logs to port 24224, the port on which fluentd listens to. This creates the necessary interaction between the API container and the logger container which runs fluentd.

data/Admin/2021/7/fluentd-stdout.PNG

Writing Logs from Fluentd to MySQL:

To advance this further, let's modify our system such that all the logs written onto fluentd is to be written to a mysql database. fluentd fully supports this, along with several other external integrations by means of "plugins". So basically we need to install whatever integration we're interested in the form a plugin to the fluentd system and configure the fluentd.conf file to handle it. This is similar to our previous case, where we're writing all the logs captured by fluentd to stdout.

For this, we need to modify our fluentd container to have mysql integration "plugin" installed during the container creation and configure fluentd.conf accordingly.

To get started, first we'd add a mysql database container to our docker-compose file onto which fluentd writes its logs.

version: '3'
services:
    db:
        build: 
            context: ./db
            dockerfile: Dockerfile
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: abc@123
            MYSQL_DATABASE: localdb
        ports: 
            - 3306:3306

we're using a custom Dockerfile instead of the straightforward "mysql" image. Why? Because we need to have some startup scripts to be executed in the databse as we set it up. We'll get to that in a minute.

In the next step we need to have the mysql plugin installed in our fluentd container. For this, we take the custom Dockerfile route and create our custom container from the fluentd:edge-debian image we used previously.

It picks up the fluentd:edge-debian image and installs "fluent-plugin-mysql" that is available to write fluentd logs onto mysql database. This plugin requires mysql-client as prerequisite, so we install "default-libmysqlclient-dev" before it.

The finished file looks like below:

FROM fluent/fluentd:edge-debian AS base

USER root

RUN buildDeps="sudo make gcc g++ libc-dev" \
    && apt-get update \
    && apt-get install -y --no-install-recommends $buildDeps \
    && sudo apt-get install -y default-libmysqlclient-dev \
    && sudo gem install fluent-plugin-mysql \
    && sudo gem sources --clear-all \
    && SUDO_FORCE_REMOVE=yes \
    apt-get purge -y --auto-remove \
    -o APT::AutoRemove::RecommendsImportant=false \
    $buildDeps \
    && rm -rf /var/lib/apt/lists/* \
    && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

USER fluent

And in the docker-compose file we modify our "logger" configuration as below. We replace our build section to have our Dockerfile be picked up instead of the image.

version: '3'
services:
    logger:
        build: 
            context: ./logger
            dockerfile: Dockerfile
        volumes: 
            - ./logger/tmp:/fluentd/etc
        command: '-c /fluentd/etc/fluentd.conf -v'
        ports: 
            - 24224:24224
            - 24224:24224/udp
        depends_on: 
            - "db"

Now we need to modify the fluentd.conf file to write logs to mysql instead of stdout. For this we provide the configuration in a format defined as below:

<source>
  type forward
  port 24224
  bind 0.0.0.0
</source>

<match docker.**>
  @type mysql_bulk
  host db
  database localdb
  username root
  password abc@123
  column_names container_id,container_name,log
  table fluentd_logs
  flush_interval 10s
</match>

The match docker.** ensures that only logs from the Docker containers (remember the tag format we gave in logger driver) be picked up for writing to mysql. Within this we have our database host, database name, table name, columns to be populated and the flush_interval (how frequently logs be written onto the database) details provided.

Observe that for host we've given as "db". This is because this fluentd should write to the database that is running within the container "db" within this compose network. docker-compose ensures these underlying resolutions.

Now for the question, do we have these database, table available in our container when it is up? No. That's why we used a custom Dockerfile for the mysql, instead of an image. The Dockerfile for the mysql database looks like below:

FROM mysql

ADD ./init/schema.sql /docker-entrypoint-initdb.d

EXPOSE 3306

We have put our schema generation and others within a file called schema.sql and during startup we're configuring mysql to run these scripts (they're run because we're adding them to the docker-entrypoint-initdb.d). The schema.sql contains the below script:

USE localdb;
CREATE TABLE fluentd_logs(
    id bigint primary key auto_increment,
    container_id nvarchar(500),
    container_name nvarchar(500),
    log nvarchar(1000)
);

since we provided the MYSQL_DATABASE variable in docker-compose file, the database is created. This is followed by this script execution and thus the db will have all the infra ready for the docker logs be written onto the db when the fluentd receives them from the API.

The complete docker-compose looks like below:

version: '3'
services:
    db:
        build: 
            context: ./db
            dockerfile: Dockerfile
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: abc@123
            MYSQL_DATABASE: localdb
        ports: 
            - 3306:3306 
    logger:
        build: 
            context: ./logger
            dockerfile: Dockerfile
        volumes: 
            - ./logger/tmp:/fluentd/etc
        command: '-c /fluentd/etc/fluentd.conf -v'
        ports: 
            - 24224:24224
            - 24224:24224/udp
        depends_on: 
            - "db"
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile
        ports:
            - 5000:80
        logging: 
            driver: fluentd
            options: 
                tag: "docker.{{.ID}}"
        depends_on: 
            - "logger"

When we run this docker-compose file, the containers api, logger and db are built respectively. When we hit something in the API, it creates logs which are pushed onto port 24224 by the fluentd logger driver tagged with the containerId.

> docker-compose build --no-cache # builds the containers using the configurations

data/Admin/2021/7/fluentd-1.PNG

> docker-compose up # containers boot up

data/Admin/2021/7/fluentd-2.PNG

Test the logging by invoking the API, which generates a response log. data/Admin/2021/7/api-test.PNG

This log stream is received by the logger container that runs fluentd which listens on the same port and then writes it to the fluentd_logs table within the "db" container, which is configured in the fluentd.conf file through the "fluent-plugin-mysql" package.

Verifying by logging into the MySQL database via MySQL client and Querying on the fluentd_logs table. data/Admin/2021/7/fluentd-mysql.PNG

The complete source code is available at: https://github.com/referbruv/docker-fluentd-example

Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

You can now show your support. 😊

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept