Home

Awesome

<img align="right" width="200" title="logo: a beaver with an ax (because it's a logger, just like Paul Bunyan). Modification of BY-NC image http://pngimg.com/download/31356 (color reduction)" src="./assets/images/beaver_PNG57.png">

Bunyan: An Elixir Logger

Note

This is the omnibus version of Bunyan, which includes the API, Erlang error logger, and remote logging plugins, along with the writers for consoles, files, and remote nodes. If you want to configure a more minimal installation, have a look at Bunyan Core.

Show Me!

Here's some code that writes log messages. Obviously, in real code the values wouldn't be hardcoded...

require Bunyan
import Bunyan

info("Starting database compaction", %{
  database: "prod_orders",
  mode:     :concurrent,
})

# ...

info("1,396 orphaned carts found")

# ...

warn("These carts have referrers and cannot be deleted:",
     [ 48792, 49352, 49720, 50155, 57782 ])

# ...

error("This cart is locked and cannot be deleted", cart)

# ...

info("1,390 ophaned carts removed")

And here's the result if you've configured Bunyan to log to a console with ANSI color support.

Example of Bunyan console output

Try It

There's a simple demo of networked, hierarchical logging at https://github.com/bunyan-logger/demo-remote-logging.

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

     SOURCES                    CORE                     WRITERS
     ‾‾‾‾‾‾‾                    ‾‾‾‾                     ‾‾‾‾‾‾‾
╭──────────────╮                                    ╭────────────────╮
│ Programmatic │ \                             ---->│ Write to stdout│
│     API      │  \                           /     │ files, etc     │
╰──────────────╯   \                         /      ╰────────────────╯
                    \      ╭──────────────╮ /
╭──────────────╮     ----> │   Collect    │/        ╭────────────────╮
│ Erlang error │ ╌╌╌╌╌╌╌╌> │      &       │╌╌╌╌╌╌╌> │   Write to     │
│   logger     │     ----> │ Distribute   │\        │  remote node   │
╰──────────────╯    /      ╰──────────────╯ \       ╰────────────────╯
                   /                         \
╭──────────────╮  /                           \     ╭────────────────╮
│    Remote    │ /                             \    │     etc        │
│    reader    │                                --->│                |
╰──────────────╯                                    ╰────────────────╯

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. Y

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.

Components

Bunyan is organized as a core module, bunyan_core, and a set of plugins. If you just use this project (bunyan) as a dependency, you'll automatically get them all.

Alternatively, you can use bunyan_core as a dependency, and then add just the plugins that you want to use. You only have to add a plugin as a dependency if you declare it in the configuration (see below).

The various components are:

NameFunction
bunyan_coreThe Collector component, which distributes incoming messages to writers
buyan_source_erlang_error_loggerInjects Erlang, OTP, and SASL errors
bunyan_source_remote_readerInjects log messages sent from a remote node
bunyan_writer_deviceWrite messages to a console, file, or other device.
bunyan_writer_remoteWrite messages to a remote node

There are two additional components, bunyan_shared and bunyan_formatter, that are used by the plugins. You do not need to declare these as dependencies.

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

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.

A Big, Big Thank You!

To Benjamin Coppock, who let me take over the project name Bunyan on Hex. His original project is still available as

{ bunyan: "0.1.0" }