Integrate Kibana Dashboard With Rails

The goal of this post is enable very simple integration of a Rails application and Kibana (v5.2.1) dashboard.

And by dashboard I mean custom dashboard created in Kibana, not the Kibana interface. The main idea is to allow users to view a dashboard without even knowing it is served by Kibana - no access to Kibana’s 5601 port!.

You could do this by simply using a reverse proxy like Nginx, to carefully reverse route certain Kibana paths to your main Rails application. But the following also allows you to control access to Kibana via the Rails app itself, so you can for example restrict access to the dashboard for logged-in users only. Please take note thought, if your dashboard will be accessed by many users, it might not be a performant solution, as you will see later.

Let’s say you already have a view (app/views/dashboard) for embedding a dashboard:

<h1>Dashboard</h1>
<div>
  <iframe src="http://localhost:5601/app/kibana#/dashboard/444f4b50-f651-11e6-b857-87673ff0eadc?embed=true&_g=()" height="600" width="100%"></iframe>
</div>

The goal is to replace http://localhost:5601 with your custom domain, eg: http://dashboard.example.com so you won’t have to expose your Kibana directly to your users.

Nginx Configuration

Here is dashboard.example.com server configuration for nginx. As I said earlier, you could re-route paths to the Kibana server but I want to be able to control some access via the Rails app itself. As such, we keep the nginx configuration minimal, only to redirect static assets (location ~ ^\/(bundles|ui\/fonts|plugins\/.*\.svg) block below).

An Nginx named location directive (location @reproxy) is used to handle Kibana requests from the Rails app. You use it with internal redirection. Essentially, any requests from the backend (Rails app) with X-Accel-Redirect set to @reproxy will be redirected to a path defined in X-Reproxy-URL header.

server {
  # Rails backend
  listen 127.0.0.1:80;
  server_name dashboard.example.com;

  # Proxy all the request to the backend;
  location / {
    proxy_buffering off;
    proxy_cache off;
    proxy_http_version 1.1;
    chunked_transfer_encoding off;

    proxy_pass http://localhost:3000;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }

  # Paths that Kibana access, you might want to restrict more here
  # or in routes.rb
  location ~ ^\/(bundles|ui\/fonts|plugins\/.*\.svg) {
    proxy_pass http://localhost:5601;
  }

  # The backend redirects the request to /reproxy;
  location @reproxy {
    # make this location internal-use only;
    internal;

    # set $reproxy variable to the value of X-Reproxy-URL header;
    set $reproxy $upstream_http_x_reproxy_url;

    # proxy request to the received X-Reproxy-URL value;
    proxy_pass $reproxy;
    proxy_redirect $reproxy /;
    client_max_body_size    200m;
    proxy_read_timeout      60;
    proxy_connect_timeout   60;
  }
}

Dashboard Controller

The dashboard controller simply sets the X-Accel-Redirect and X-Reproxy-URL I mentioned in the previous section. I had to add custom lua script in my project to redirect POST requests with the body intact, but that doesn’t seem needed with my local installation (Nginx 1.10.2), see this post if you face that problem).

class DashboardController < ApplicationController
  def index
  end

  def kibana_proxy
    # Kibana accesses serveral paths
    # 1. /bundles|/ui/fonts|*.svg|etc -> static assets, shouldn't reach here, let the reverse proxy handles it
    # 2. /app/kibana -> the kibana itself, we map from our own /kibana path, so rewrite it
    # 3. /elasticsearch|/es_admin|etc -> other kibana, elasticsearch paths
    #
    # For 2 & 3 You might want, fine tune routes.rb to define your own access control

    path = "http://localhost:5601#{request.fullpath}"
    response.headers["X-Reproxy-URL"] = path
    # If you use that custom lua script, uncomment the following
    # response.headers["X-Accel-Post-Body"] = request.body.read
    response.headers["X-Accel-Redirect"] = "@reproxy"

    render nothing: true
  end
end

Every Kibana requests have to go to Rails first, so it could affect the performance of your application. It works fine for my project since only a few users access the dashboard at any time.

Rails routes

And the minimal routes.rb you need to proxy Kibana dashboard.

Rails.application.routes.draw do
  get 'dashboard', to: 'dashboard#index'

  # Kibana and Elasticsearch paths
  get '/app/kibana', to: 'dashboard#kibana_proxy'
  get '/api/console/api_server', to: 'dashboard#kibana_proxy'
  get '/es_admin', to: 'dashboard#kibana_proxy'
  post '/es_admin/_mget', to: 'dashboard#kibana_proxy'
  get '/es_admin/.kibana/_mapping/*/field/_source', to: 'dashboard#kibana_proxy'
  post '/es_admin/.kibana/index-pattern/_search', to: 'dashboard#kibana_proxy'
  post '/elasticsearch/_msearch', to: 'dashboard#kibana_proxy'
end

Conclusion

Be sure to note the integration with Nginx or any other reverse proxy in your project documentation - even more reason to keep the Nginx configuration minimal, and handle other Kibana paths in routes.rb.

My setup is slightly different from above as I use an old version of Kibana, which differ mostly in routes.rb. If you use Elasticsearch in your project and need to present some simple dashboards, it is easy to just proxy access to Kibana. Else, you might need to look into Elasticsearch Shield for example, but then you might need to do more work to integrate it with your Rails app.