Home

Awesome

<img align="right" width="200" title="logo: a beaver with an ax (because it's a logger, just like Paul Bunyan)" src="./assets/images/beaver_PNG57.png">

Bunyan: An Elixir Logger

Summary

Summary of the Summary

{ :bunyan, ">= 0.0.0" }

Basic logging:

require Bunyan

Bunyan.info "message or function"
Bunyan.info "message or function", «extra»

Message can have embedded newlines, which will be honored. «extra» can be any Elixir term: maps are encouraged, as they are formatted nicely.

API Overview

Logging Functions

You must require Bunyan before using any of these four functions.

If the first parameter is a function, it will only be invoked if the source log level is at or below the level of the message being generated.

The second parameter is optional Elixir term. It will be displayed beneath the main text of the message. If it is a map, it will be shown in a tabular structure.

Runtime Configuration

Architecture

<img title="architecture diagram showing sources, collector, and writers" src="./assets/images/overall_architecture.png">

Bunyan takes logging input from a variety of sources. It is distributed with three:

These sources are all plugins: you have to add them as dependencies (or use the batteries-included hex package) if you want to use them.

The sources send log messages to the collector. This in turn forwards the messages to the various log writers. Two writers come as standard:

Again, these are both optional.

You can configure multiple instances of each type of writer. This would allow you to (for example) log everything to standard error, errors only to a log file, and warnings and errors to two separate remote loggers.

You can easily add new sources and new writers to Bunyan.

Log Levels

The log levels are debug, info, warn, and error, with debug being the lowest and error the highest.

You can set log levels indpendently for each of the sources and each of the writers.

The level set on a source determines which messages are sent on to the writters. The level set on a writer determines which messages get written.

In addition, the API source has an additional option to set the compile time log level. Calls to logging functions will not be compiled into your code if they are below this level.

Configuration

Configuration can be specified in the regular config.xxx files. Much of it can also be set at runtime using the Bunyan.config function.

The top level configuration looks like this:

[
  read_from: [
    «list of sources»
  ],
  write_to: [
    «list of writers»
  ]
]

Source Configuration

Each source configuration entry can be either a module name, or a tuple containing a module name and a keyword list of options:

Bunyan.Source.API or { Bunyan.Source.API, compile_time_log_level: :info }

Each source module has its own set of configuration options.

Source: Bunyan.Source.API

Provides a programmatic API that lets applications create log messages and configure the logger.

Options:

Source: Bunyan.Source.ErlangErrorLogger

Handles messages and reports generated by the Erlang error_logger. It also handles SASL and OTP messages that the error logger forwards.

Writer Configuration

Bunyan comes with two writers (but you can add your own—see below).

Writer: Bunyan.Writer.Device

Writes log messages to standard error after formatting them for human consumption.

Writer: Bunyan.Writer.Remote

:send_to,         # name of the remote logger process
:send_to_node,    # the node or nodes when the remote reader lives. If nil, send to all
:send_to_nodes,   # alias for `send_to_node`
:min_log_level,   # only send >= this,
:name,

Used to forward log messages to another instance of Bunyan.

Log Message Format Specifications

The Device writer tries to create nicely formatted output. For example, it will try to indent multi-line messages so the start of the text of the message lines up, and it recognizes things such as maps when laying out nontext data.

What it writes is under your control. You specify this using format strings. Each message is potentially formatted using two formats. The first of these, the main_format_string is used to write the first line of the message. Typically this string will include some kind of time stamp and a log level, as well as the first line of the actual log message.

The additional_format_string is used to format the rest of the log message (if any). The output generated under the control of this format will automatically be indented to line up with the start of the message in the first line.

Newlines in the message or in the format string will automatically cause the message to be split and indented.

A format string consists of regular text and field names. The regular text is simply copied into the resulting message. The contents of the corresponding fields are substituted for the field names.

A Sample Configuration

This is probably way more than you'd ever need to specify, but I wanted to show all the options:

[
  read_from:              [
    Bunyan.Source.Api,
    Bunyan.Source.ErlangErrorLogger,
  ],
  write_to:               [
    {
      Bunyan.Writer.Device, [
        name:  :stdout_logging,

        main_format_string:        "$time [$level] $remote_info$message_first_line",
        additional_format_string:  "$message_rest\n$extra",

        level_colors:   %{
          @debug => faint(),
          @info  => green(),
          @warn  => yellow(),
          @error => light_red() <> bright(),
        },
        message_colors: %{
          @debug => faint(),
          @info  => reset(),
          @warn  => yellow(),
          @error => light_red(),
        },
        timestamp_color: faint(),
        extra_color:     italic() <> faint(),

        use_ansi_color?: true
      ]
    },
    {
      Bunyan.Writer.Device, [
        name:  :critical_errors,
        device:            "/var/log/myapp_errors.log",
        pid_file_name:     "/var/run/myapp.pid",
        rumtime_log_level: :error,
        use_ansi_color?:   false,
      ]
    },
    {
       Bunyan.Writer.Remote, [

          # assumes there's a Bunyan.Source.GlobalReader with
          # `global_name: MyApp.GlobalLogger` running on
          # the given two nodes

          send_to: MyApp.GlobalLogger,
          send_to_nodes:  [
            :"main_logger@192.168.1.2",
            :"backup_logger@192.68.1.2",
          ],
          runtime_log_level: :warn,
      ]
    },
  ]
]

To Do

[ ] Runtime configuration (hooks are in place) [ ] Guides for creating your own sources and writers [ ] Finish off reformatting of Erlang error logger and sasl messages (framework in place)

Why Another Logger?

I needed a distributed logger as part of the Toyland project, and couldn't find what I needed. I also wanted to experiment with something more decoupled than the available options.