Klogger is an opinionated logger for Ruby applications to allow consistent and structured logging.
- Output can be sent to STDOUT or a file. The logger is backed by the standard Ruby
Logger
class. - Ouput can be presented in various formats.
- Output can be highlighted with appropriate colours based on severity (optional).
- Additional destinations can easily be added for shipping log data to other services.
Add the gem to your Gemfile.
gem "klogger-logger", '~> 1.1'
This shows some typical usage of the logger along with example output. The Klogger::Logger
instance will output to STDOUT by default but can be redirectly.
To begin, you need an instance of your logger. Loggers are thread-safe so you can use a single instance across multiple classes and requests. There are a few options that you can control about a logger. These are documented below.
# The most basic logger includes a name and nothing else. This will log to STDOUT and use
# the default formatter without colouring.
Klogger.new(name)
# You can customise where log output goes using the destination argument. You can provide a device
# that response to write & close or a path to a file. The same as Ruby's logger class.
Klogger.new(name, destination: $stderr)
Klogger.new(name, destination: 'log/events.log')
Klogger.new(name, destination: StringIO.new)
# To customise the formatting of the log output, you can provide a formatter.
Klogger.new(name, formatter: :json)
Klogger.new(name, formatter: :simple)
Klogger.new(name, formatter: :go)
# You can also enable colouring/highlighting.
Klogger.new(name, highlight: true)
Klogger.new(name, highlight: Rails.env.development?)
# You can add tags to be included in all log lines for this logger.
Klogger.new(name, tags: { app: 'example-app' })
When you want to log something, you have the option of 5 severities (debug
, info
, warn
, error
and fatal
). For this example, we'll use info
but it is interchangable with any of the other severities.
# The most basic way to log is to provide a message.
logger.info("Hello world!")
# To add additional tags to the line, you can do so by passing a hash.
logger.info("Sending e-mail", recipient: "adam@example.com", subject: "Hello world!")
# The message is optional and you can just pass a hash too
logger.info(ip_address: "1.2.3.4", method: "POST", path: "/login")
# Blocks can also be passed to the logger to log the result of that block
logger.info { "Hello world!" }
logger.info('Result of 1 + 1') { 1 + 1 } # Logs with a message of "Result: 2"
Exceptions happen and when they do, you want to know about them. Klogger provides a helper method to log exceptions. These will automatically be logged with the error
severity.
begin
# Do somethign bad
rescue => e
# Just log the exception
logger.exception(e)
# You can also provide a message
logger.exception(e, "Something went wrong")
# You can also provide a hash of additional tags
logger.exception(e, "Something went wrong", tags: { user_id: 123 })
end
Groups allow you to group related log entries together. They do two things:
- They allow you to add tags to all logs which are within the group
- They assign a random ID to the group which is included with all logs within the group
Here's an example of how they work.
# In this example, both log entries within the block will be tagged with the `url` tag from the group.
logger.group(url: "https://example.com/my-files.zip") do # 92b1b62c
logger.info("Download starting")
file = download_file('...')
logger.info("Download complete", size: file.size)
end
You'll notice in that example the comment 92b1b62c
. This is the group ID for this block. This is a random ID which is generated when the group is created. It is included with all logs within the group thus allowing you to search for that reference to find all logs related to that group. If groups are nested, you'll have multiple IDs. By default, these group IDs are not shown in your output.
# If you wish for group IDs to be included in your output, you can enable that in the logger
Klogger.new(name, include_group_ids: true)
When executed like this groups will only apply to the logger in question. If you want to group messages from different loggers, you can use the Klogger.group
method where groups and their data will apply to all Klogger loggers.
logger1 = Klogger.new(:logger1)
logger2 = Klogger.new(:logger2)
Klogger.group(ip: '1.2.3.4') do
logger1.info('Hello world!') # will be tagged with ip=1.2.3.4
Klogger.group(user: 'user_abcdef')
logger2.info('Example') # will be tagged with ip=1.2.3.4 and user=user_abcdef
end
end
# If you can't use a block you can manually open and close a group but you'll need to be sure to close it
# when you're finished.
group_id = Klogger.global_groups.add(ip: '1.2.3.4')
# ... do anything that you want - everything will be tagged as appropriate
Klogger.global_groups.pop
Finally, you can use groups without IDs to simply add tags to all logs within the group using the tagged
method.
logger.tagged(name: 'steve') do
logger.info("Download starting")
end
If you wish to apply tags to a series of log entries but you don't wish to use blocks, you can create a "sub" logger which will always include those tags for all messages sent to it.
logger = Klogger.new(:logger)
tagged_logger = logger.create_tagged_logger(tag: 'my-tag')
tagged_logger.info "Hello world!" # => will be tagged with tag=my-tag
Sometimes you don't want to log for a little while. You can use the silence
method to temporarily disable logging.
# Calling this will silence the logs until you unsilence again
logger.silence!
logger.unsilence!
# Alternative, you can use the block option which will unsilence when complete.
logger.silence! do
# Logs will be silenced here
end
In many cases you won't want to keep your log data on a local disk or within STDOUT. You can use this additional option to have data dispatched automatically to other services which you decide upon.
# This is just an example class. You can create whatever class you want here and it'll be called
# with the call method.
class GraylogDestination
def initialize(host, port)
@notifier = GELF::Notifier.new(host, port)
end
def call(logger, payload, group_ids)
message = payload.delete(:message)
@notifer.notify!(facility: "my-app", short_message: message, group_ids: group_ids, **payload)
end
end
# Create a logger and add the destination
logger = Klogger.new(name)
logger.add_destination(GraylogDestination.new('graylog.example.com', 12201))
# If you only want to send certain data to another block, you can do so
logger.with_destination(other_destination) do
# ...
end