Installing Graylog2 logging server with Ansible and configure Rails properly

5 minute read

Out Of Date Warning

This article was published on 28/02/2014, this means the content may be out of date or no longer relevant.
You should verify that the technical information in this article is still up to date before relying upon it for your own purposes.

Intrigued by the screencast video about the Graylog2 daemon , I decided to try Graylog2 as a central logging server for our application log files. In this post, I want to show, how I used Ansible to easily configure a server with all the necessary daemons. In the second part, I want to give some information on how to get the most of it using Rails, by including information like name of the current logged in user.

Installation with Ansible

Luckily, Ansible-Galaxy provides some roles, we can just include into our Ansible playbook. For this playbook, I assume, you want to run the logging server on a single host. Each of the four parts, ElasticSearch, MongoDB, Graylog-Server, and Graylog-Web, could live under a different host. However, for our use case, having everything on the same host should be fine for a long time.

1. playbook.yml and inventory

---
- hosts: logging
  # playbook.yml
  sudo: true
  vars:
    allow_ips: # All app-server ips, that should be allowed to connect to the daemon
      - 1.2.3.4
      - 2.3.4.5
  roles:
    - role: gpstathis.ansible-elasticsearch
      elasticsearch_version: 0.90.10
      elasticsearch_heap_size: 1g
      elasticsearch_max_open_files: 65535
      elasticsearch_timezone: "Europe/Berlin"
      elasticsearch_node_max_local_storage_nodes: 1
      elasticsearch_index_mapper_dynamic: "true"
      elasticsearch_cluster_name: graylog4
      elasticsearch_node_name: elasticsearch-ansible-node
      elasticsearch_memory_bootstrap_mlockall: "true"
      elasticsearch_plugins: []

    - role: bennojoy.mongo_mongod
      mongod_replication: false
      mongod_port: 2701
      mongod_datadir_prefix: "/data/"

    - role: ansible-graylog2
      graylog_version: 0.20.1
      graylog2_secret: "Put some very long random string here"
      # Generate a sha256 hashed password:echo -n mypassword | shasum -a 256
      graylog2_admin_password: "TODO: put a sha256 password here"

  tasks:
    - shell: 'iptables -L  | grep 12201'
      ignore_errors: true
      register: iptables_has_port

    - shell: 'iptables -A INPUT -p tcp --dport 12201 -s {{item}} -j ACCEPT && iptables -A INPUT -p udp --dport 12201 -s {{item}} -j ACCEPT'
      with_items: allow_ips
      when: iptables_has_port | failed

    - shell: 'iptables -A INPUT -p tcp --dport 12201 -j DROP && iptables -A INPUT -p udp --dport 12201 -j DROP'
      when: iptables_has_port | failed

Add the server to your inventory file (e.g. production):

# production
[logging]
logging.myserver.com ansible_ssh_user=root #... more options like ansible_ssh_port etc

2. Install missing dependencies

Make sure, your Ansible version is not too old (at least 1.4). As I am writing this, 1.5 should be released soon:

$ ansible-playbook --version
ansible-playbook 1.4.5

Install the missing Ansible Galaxy roles:

sudo ansible-galaxy install -i gpstathis.ansible-elasticsearch bennojoy.mongo_mongod

Install a third role, Graylog2, which is not in Ansible Galaxy right now:

mkdir -p roles
git clone https://github.com/nicolai86/ansible-graylog2.git roles/ansible-graylog2

3. Run the playbook + configure input

ansible-playbook -i production playbook.yml

Afterwards, navigate to http://logging.myserver.com:9000 and log in with admin and your password from above. There, under System -> Inputs, define a GELF UDP Input on the default port 12201. This port is protected by our iptables-Rules

If anything went wrong, have a look into the graylog2 log files on the server (/var/log/graylog2-server/console.log and /var/log/graylog2-web/console.log)

Getting logging input from Rails to Graylog2

Based on this Knowledge Base entry, we use gelf and lograge for the task:

# Gemfile
gem "gelf"
gem "lograge"

# bundle, of course

At the end of the config/environments/production.rb paste the logging configuration (I tried different kinds of initializer files and locations, but found it must be at the end of the environment-config. Otherwise the logger would still use stdout in development). Here, we depart from the knowledge base configuration, which is a bit thin and outdated.

# config/environments/production.rb
MyApp::Application.configure do
  # ...
  app_name = config.action_mailer.default_url_options.try(:fetch, :host) || Rails.application.class.name.underscore.split('/').tap{|i|i.pop}.join('/')
  # or set to whatever you want
  config.lograge.enabled = true
  config.lograge.formatter =  Lograge::Formatters::Graylog2.new
  # config.lograge.log_format =  :graylog2 # DEPRECATED
  config.lograge.custom_options = ->(event) {
    opts = event.payload.dup.delete_if{|k,v|v.nil?}.symbolize_keys
    params = event.payload[:params]
    if (params.present?) and (p = params.except("controller", "action",'utf8')) and (p.present?)
      opts[:params] = p
    else
      opts.delete :params
    end
    opts[:duration] = event.duration
    if event.payload[:exception]
      quoted_stacktrace = %Q('#{Array(event.payload[:stacktrace]).to_json}')
      opts[:stacktrace] = quoted_stacktrace
    end
    opts
  }
  config.lograge.ignore_custom = ->(event) {
    event.payload[:bot]
  }
  config.logger = GELF::Logger.new("logging.yourserver.com", 12201, "WAN", { :host => app_name, :environment => Rails.env })
  if Rails.env.production?
    ActiveRecord::Base.logger = Logger.new('/dev/null')
  end
end

As you can see, we:

  • Define an app_name (automatically by host-name or Rails application name)
  • Copy the event payload from the ActiveSupport notification into our log and do some parameter filtering. We don't need the supply the controller and action parameters, because we get those as top level payload information anyway.
  • Define a custom ignore function, e.g. ignore all requests matched as bot

This was only the first part. We need an ApplicationController method for providing all those parameters we care about, including IP, HTTP-refer(r)er, User-Agent, logged in user. Here a starting example:

class ApplicationController < ActionController::Base
  # ...
  def append_info_to_payload(payload)
    super
    payload[:ip] =  request.remote_ip
    payload[:user_name] =  current_user.try(:name) if defined?(current_user)
    payload[:user_agent] =  request.user_agent
    payload[:referer] =  request.env['HTTP_REFERER']
    payload[:session] =  session.to_hash.except("_csrf_token", "session_id")
    payload[:bot] = (request.user_agent.blank? ||
                      request.user_agent[/bot\W|index|spider|crawl|wget|slurp|Mediapartners-Google/i])
  end
end

Add whatever parameters you want and they will be delivered 1:1 to the logging interface. The bot matching is quite naive, and could be made better, but it gives you a good idea.

Commit, deploy and wait, and logs should fill your Graylog2!

Done! Happy logging.