2018.05.31

Docker Monitoring Tool On Rack

I do some experiments with a dockerized environments for the last year or so. Sometimes I need to get some simple statistics from the server where I some docker containers are running.

I don't want to establish an ssh connection and execute commands in shell to collect information all the time. So I decided to find a nice web UI tool for this purposes.

There're a lot of powerful tools wich you can use for monitoring docker containers: Prometheus, CAdvisor, Scout, DataDog etc. But it would take some time to find out how to work with them and how to set up an environment to gather information properly and overall they will consume additional system resources.

So I decided to build my own monitoring tool. And it took me about 20 minutes to accomplish this task. It's easily configurable and it doesn't need much RAM and CPU cause it's build upon a Rack web server interface wich calls a docker client to get some stats.

Build A Rack Application

First of all you'll need to create an ruckup configuration file wich is called config.ru by default:


mkdir -p docker-monitor/views
mkdir -p docker-monitor/tmp

cd docker-monitor

touch config.ru

With following content in it:


# config.ru

require "./application"

run Application.new

The application file is located in the same directory next to config.ru file. The only method it needs to provide is Application#call method wich is required by the Rack interface. I will provide the full code of the application.rb file. I think that the code is fairly simple and does not require a lot of explanation.


require "erb"

class Application

  def call(env)
    @current_time = Time.new.strftime("%Y-%m-%d %H:%M:%S")
    @stats = docker_stats
    render :index
  end

  private

  def render(view)
    template = load_view(view)
    erb = ERB.new(template)
    body = erb.result(binding)
    ["200", { "Content-Type" => "text/html" }, [body]]
  end

  def load_view(view)
    @_views ||= {}
    @_views[view.to_sym] ||= begin
      templates = Dir.glob("#{__dir__}/views/#{view}*.erb")
      template_path = File.absolute_path(templates.first)
      File.read(template_path)
    end
  end

  def docker_stats
    io = IO.popen("docker stats --no-stream", "r")
    rows = io.read.split(/\n/).map { |row| row.split(/\s{3,}/) }
    rows.each { |row| row[0], row[1] = row[1], row[0] }
    io.close
    rows
  end
end

The docker_stats method calls docker stats command to get some basic statistics.

The load_view method loads a ERB view, located in the views/ directory by the name.

The render method renders a view against current context via the binding method so all instance variables will be accessible in a view too.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Docker Monitor</title>
  <style>
    .stats-table {
      width: 100%;
      border-collapse: collapse;
      border: 1px solid #000;
      text-align: center;
    }

    .stats-table td, .stats-table th {
      border: 1px solid #000;
      padding: 5px 10px;
    }

    .stats-table tr:nth-child(even) {
      background-color: #eee; 
    }
  </style>
</head>
<body>
  <h1>Currently Running Docker Containers <small style="float: right"><%= @current_time %></small></h1>

  <table class="stats-table">
    <% @stats.each_with_index do |row, row_num| %>
      <tr>
        <% if row_num == 0 %>
          <% row.each do |col| %>
            <th scope="col"><%= col %></th>
          <% end %>
        <% else %>
          <% row.each_with_index do |col, col_num| %>
            <% if col_num == 0 %>
              <th scope="row"><%= col %></th>
            <% else %>
              <td><%= col %></td>
            <% end %>
          <% end %>
        <% end %>
      </tr>
    <% end %>
  </table>
</body>
</html>

Rackup

Almost done for now!

You can run the rackup command from within of the working directory and then check the localhost:9292 URL.

The table with stats should be shown there.

running Docker containers

Autoreload

So to don't reload the page every time when you need a fresh stats the rack-refresher gem can be used.

The installation is simple:


gem install rack-refresher

Now modify the config.ru file:


require "./application"
require "rack-refresher"

# Reload the page completely every 5 minutes
use Rack::Refresher do |config|
  config.interval = 300_000
end

# Reload only the <body> tag via AJAX every 5 seconds
use Rack::Refresher do |config|
  config.ajax = true
  config.delay = 5000
end

run Application.new

I have defined two refreshers here. The first one will reload a page every 5 minutes in the case of something significant happend to the layout. The second one will refresh only the <body> tag (almost like the turbolinks gem does too) via an AJAX call every 5 seconds.

Start And Stop

To start the server on the 9292 port and store it's PID to the tmp/ directory run:


rackup -P tmp/server.pid -D

Now, thanks to the PID file, you can stop the server by killing the proper process:


kill `cat tmp/server.pid`

In Production

Don't forget about an authentification when using in production. The Rack::Auth::Base or the Rack::Auth::Digest middlewares can be used for this purpose.

Result

That's all! The main advantage of the tool we've built here is that it is fully configurable and extendable in case you have some knowing of Ruby.

Here's a link to a ready-to-use tool: https://github.com/zinovyev/monidock