V1.3: Logging


Each project has a global logger available at Hanami.logger that can be used like this: Hanami.logger.debug "Hello"

It can be configured in config/environment.rb

# config/environment.rb
# ...

Hanami.configure do
  # ...

  environment :development do
    logger level: :info
  end

  environment :production do
    logger level: :info, formatter: :json

    # ...
  end
end

By default it uses standard output because it’s a best practice that most hosting SaaS companies suggest using.

If you want to use a file, pass stream: 'path/to/file.log' as an option.

Filter sensitive information

Hanami automatically logs the body of non-GET HTTP requests.

When a user submits a form, all the fields and their values will appear in the log:

[bookshelf] [INFO] [2017-08-11 18:17:54 +0200] HTTP/1.1 POST 302 ::1 /signup 5 {"signup"=>{"username"=>"jodosha", "password"=>"secret", "password_confirmation"=>"secret", "bio"=>"lorem"}} 0.00593

To prevent sensitive information from being logged, you can filter it:

# config/environment.rb
# ...

Hanami.configure do
  # ...
  environment :development do
    logger level: :debug, filter: %w[password password_confirmation]
  end
end

Now the output will be:

[bookshelf] [INFO] [2017-08-11 18:17:54 +0200] HTTP/1.1 POST 302 ::1 /signup 5 {"signup"=>{"username"=>"jodosha", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "bio"=>"lorem"}} 0.00593

It also supports fine grained patterns to disambiguate params with the same name. For instance, we have a billing form with street number and credit card number, and we want only to filter the credit card:

# config/environment.rb
# ...

Hanami.configure do
  # ...
  environment :development do
    logger level: :debug, filter: %w[credit_card.number]
  end
end
[bookshelf] [INFO] [2017-08-11 18:43:04 +0200] HTTP/1.1 PATCH 200 ::1 /billing 2 {"billing"=>{"name"=>"Luca", "address"=>{"street"=>"Centocelle", "number"=>"23", "city"=>"Rome"}, "credit_card"=>{"number"=>"[FILTERED]"}}} 0.009782

Note that billing => address => number wasn’t filtered while billing => credit_card => number was filtered instead.

If you want to disable logging of the body completely, it can be easily achieved with custom formatter:

class NoParamsFormatter < ::Hanami::Logger::Formatter
  def _format(hash)
    hash.delete :params
    super hash
  end
end

and then just tell logger to use our new formatter for logging

logger level: :debug, formatter: NoParamsFormatter.new

Arbitrary Arguments

You can specify arbitrary arguments, that are compatible with Ruby’s Logger.

Here’s how to setup daily log rotation:

# config/environment.rb
# ...

Hanami.configure do
  # ...

  environment :production do
    logger 'daily', level: :info, formatter: :json, stream: 'log/production.log'

    # ...
  end
end

Alternatively, you can decide to put a limit to the number of files (let’s say 10) and the size of each file (eg 1,024,000 bytes, aka 1 megabyte):

# config/environment.rb
# ...

Hanami.configure do
  # ...

  environment :production do
    logger 10, 1_024_000, level: :info, formatter: :json, stream: 'log/production.log'

    # ...
  end
end

Automatic Logging

All HTTP requests, SQL queries, and database operations are automatically logged.

When a project is used in development mode, the logging format is human readable:

[bookshelf] [INFO] [2017-02-11 15:42:48 +0100] HTTP/1.1 GET 200 127.0.0.1 /books/1  451 0.018576
[bookshelf] [INFO] [2017-02-11 15:42:48 +0100] (0.000381s) SELECT "id", "title", "created_at", "updated_at" FROM "books" WHERE ("book"."id" = '1') ORDER BY "books"."id"

For production environment, the default format is JSON. JSON is parseable and more machine-oriented. It works great with log aggregators or SaaS logging products.

{"app":"bookshelf","severity":"INFO","time":"2017-02-10T22:31:51Z","http":"HTTP/1.1","verb":"GET","status":"200","ip":"127.0.0.1","path":"/books/1","query":"","length":"451","elapsed":0.000391478}

Custom Loggers

You can specify a custom logger in cases where you desire different logging behaviour. For example, the Timber logger:

# config/environment.rb
# ...

Hanami.configure do
  # ...

  environment :production do
    logger Timber::Logger.new(STDOUT)

    # ...
  end
end

Use this logger as normal via Hanami.logger. It’s important to note that any logger chosen must conform to the default ::Logger interface.

Colorization

Disable colorization

In order to disable the colorization:

# config/environment.rb
# ...

Hanami.configure do
  # ...

  environment :development do
    logger level: :info, colorizer: false
  end
end

Custom colorizer

You can build your own colorization strategy

# config/environment.rb
# ...
require_relative "./logger_colorizer"

Hanami.configure do
  # ...

  environment :development do
    logger level: :info, colorizer: LoggerColorizer.new
  end
end
# config/logger_colorizer.rb
require "hanami/logger"
require "paint" # gem install paint

class LoggerColorizer < Hanami::Logger::Colorizer
  def initialize(colors: { app: [:red, :bright], severity: [:red, :blue], datetime: [:italic, :yellow] })
    super
  end

  private

  def colorize(message, color:)
    Paint[message, *color]
  end
end