Project

cogger

0.0
The project is in a healthy, maintained state
A customizable and feature rich logger.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

~> 2.6
~> 1.0
~> 1.0
 Project Readme

Cogger

Cogger is a portmanteau for custom logger (i.e. [c]ustom + l[ogger] = cogger) which enhances Ruby’s native Logger functionality with additional features such as dynamic emojis, colorized text, structured JSON, multiple outputs, and much more. 🚀

Table of Contents
  • Features
  • Screenshots
  • Requirements
  • Setup
  • Usage
    • Initialization
    • Levels
    • Environment
    • Mutations
    • Templates
    • Emojis
    • Aliases
    • Formatters
      • Simple
      • Color
      • Emoji
      • JSON
      • Native
      • Custom
    • Tags
    • Filters
    • Streams
    • Abort
    • Rack
    • Defaults
    • Inspection
    • Testing
  • Development
  • Tests
  • License
  • Security
  • Code of Conduct
  • Contributions
  • Versions
  • Community
  • Credits

Features

  • Enhances Ruby’s default Logger with additional functionality and firepower.

  • Provides dynamic/specific emoji output.

  • Provides dynamic/specific colorized output via the Tone gem.

  • Provides customizable templates which leverage the native String Format Specification.

  • Provides customizable formatters for simple, color, JSON, and/or custom output.

  • Provides multiple streams so you can log the same information to several outputs at once.

  • Provides global and individual log entry tagging.

  • Provides filtering of sensitive information.

  • Provides Rack middleware for HTTP request logging.

Screenshots

Emoji

Emoji

Color

Color

Simple

Simple

Detail

Detail

JSON

JSON

Rack

Rack

Requirements

  1. Ruby.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install cogger --trust-policy HighSecurity

To install without security, run:

gem install cogger

You can also add the gem directly to your project:

bundle add cogger

Once the gem is installed, you only need to require it:

require "cogger"

Usage

All interaction is provided by Cogger which can be used as follows:

logger = Cogger.new
logger.info "Demo"  # "Demo"

If you set your logging level to debug, you can walk through each level:

logger = Cogger.new level: :debug

# Without blocks.
logger.debug "Demo"                  # "🔎 Demo"
logger.info "Demo"                   # "🟢 Demo"
logger.warn "Demo"                   # "⚠️ Demo"
logger.error "Demo"                  # "🛑 Demo"
logger.fatal "Demo"                  # "🔥 Demo"
logger.unknown "Demo"                # "⚫️ Demo"
logger.any "Demo"                    # "⚫️ Demo"
logger.add Logger::INFO, "Demo"      # "🟢 Demo"

# With blocks.
logger.debug { "Demo" }              # "🔎 Demo"
logger.info { "Demo" }               # "🟢 Demo"
logger.warn { "Demo" }               # "⚠️ Demo"
logger.error { "Demo" }              # "🛑 Demo"
logger.fatal { "Demo" }              # "🔥 Demo"
logger.unknown { "Demo" }            # "⚫️ Demo"
logger.any { "Demo" }                # "⚫️ Demo"
logger.add(Logger::INFO) { "Demo" }  # "🟢 Demo"

Initialization

When creating a new logger, you can configure behavior via the following attributes:

  • id: The program/process ID which shows up in the logs as your id. Default: $PROGRAM_NAME. For example, if run within a demo.rb script, the id would be "demo",

  • io: The input/output stream. This can be STDOUT/$stdout, a file/path, or nil. Default: $stdout.

  • level: The severity level you want to log at. Can be :debug, :info, :warn, :error, :fatal, or :unknown. Default: :info.

  • formatter: The formatter to use for formatting your log output. Default: Cogger::Formatter::Color. See the Formatters section for more info.

  • tags: Global tagging for every log entry which must be an array of objects you wish to use for tagging purposes.

  • mode: The binary mode which determines if your logs should be written in binary mode or not. Can be true or false and is identical to the binmode functionality found in the Logger class. Default: false.

  • age: The rotation age of your log. This only applies when logging to a file. This is equivalent to the shift_age as found with the Logger class. Default: 0.

  • size: The rotation size of your log. This only applies when logging to a file. This is equivalent to the shift_size as found with the Logger class. Default: 1,048,576 (i.e. 1 MB).

  • suffix: The rotation suffix. This only applies when logging to a file. This is equivalent to the shift_period_suffix as found with the Logger class and is used when creating new rotation files. Default: %Y-%m-%d.

Given the above description, here’s how’d you create a new logger instance with all attributes:

# Default
logger = Cogger.new

# Custom
logger = Cogger.new id: :demo,
                    io: "demo.log",
                    level: :debug,
                    formatter: :json,
                    tags: %w[DEMO DB],
                    mode: false,
                    age: 5,
                    size: 1_000,
                    suffix: "%Y"

Levels

Supported levels can be obtained via Cogger::LEVELS. Example:

Cogger::LEVELS
# ["debug", "info", "warn", "error", "fatal", "unknown"]

Environment

You can use your environment to define the desired default log level. The default log level is: "info". Although, you can set the log level to any of the following:

export LOG_LEVEL=debug
export LOG_LEVEL=info
export LOG_LEVEL=warn
export LOG_LEVEL=error
export LOG_LEVEL=fatal
export LOG_LEVEL=unknown

While downcase is preferred for the log level, you can use upcased values as well. If the LOG_LEVEL environment variable is not set, Cogger will fall back to "info" unless overwritten during initialization. Example: Cogger.new level: :debug. Otherwise, an invalid log level will result in an ArgumentError.

Mutations

Each instance can be mutated using the following messages:

logger = Cogger.new io: StringIO.new

logger.close                                       # nil
logger.reopen                                      # Logger
logger.debug!                                      # 0
logger.info!                                       # 1
logger.warn!                                       # 2
logger.error!                                      # 3
logger.fatal!                                      # 4
logger.formatter = Cogger::Formatters::Simple.new  # Cogger::Formatters::Simple
logger.level = Logger::WARN                        # 2

Please see the Logger documentation for more information.

Templates

Templates are used by all formatters and adhere to the String Format Specification as used by Kernel#format. All specifiers, flags, width, and precision are supported except for the following restrictions:

  • Use of reference by name is required which means %<demo>s is allowed but %{demo} is not. This is because reference by name is required for regular expressions and/or pattern matching.

  • Use of the n$ flag is prohibited because this isn’t compatible with the above.

In addition to the above, the String Format Specification is further enhanced with the use of universal and individual directives which are primarily used by the color formatter but can prove useful for other formatters. Example:

# Universal: Dynamic (color is determined by severity)
"<dynamic>%<severity>s %<at>s %<id>s %<message>s</dynamic>"

# Universal: Specific (uses the green color only)
"<green>%<severity>s %<at>s %<id>s %<message>s</green>"

# Individual: Dynamic (color is determined by severity)
"%<severity:dynamic>s %<at:dynamic>s %<id:dynamic>s %<message:dynamic>s"

# Individual: Specific (uses a rainbow of colors)
"%<severity:purple>s %<at:yellow>s %<id:cyan>s %<message:green>s"

Here’s a detailed breakdown of the above:

  • Universal: Applies color universally to the entire template and requires you to:

    • Wrap your entire template in a and start (<example>) and end tag (</example>) which works much like an HTML tag in this context.

    • Your tag names must either be <dynamic></dynamic>, any default color (example: <green></green>), or alias (i.e. <your_alias></your_alias>) as supported by the Tone gem.

  • Individual: Individual templates allow you to apply color to specific attributes and require you to:

    • Format your attributes as attribute:directive. The colon delimiter is required to separate your attribute for your color choice.

    • The color value (what follows after the colon) can be dynamic, any default color (example: green), or alias (i.e. your_alias) as supported by the Tone gem.

In addition to the general categorization of universal and individual tags, each support the following directives:

  • Dynamic: A dynamic directive means that color will be determined by severity level only. This means if info level is used, the associated color (alias) for info will be applied. Same goes for warn, error, etc.

  • Specific: A specific directive means the color you use will be applied without any further processing regardless of the severity level. This gives you the ability to customize your colors further in situations where dynamic coloring isn’t enough.

At this point, you might have gathered that there are specific keys you can use for the log event metadata in your template and everything else is up to you. This stems from the fact that Logger entries always have the following metadata:

  • id: This is the program/process ID you created your logger with (i.e. Cogger.new id: :demo).

  • severity: This is the severity at which you messaged your logger (i.e. logger.info).

  • at: This is the date/time as which your log event was created.

This also means if you pass in these same keys as a log event (example: logger.info id: :bad, at: Time.now, severity: :bogus) they will be ignored.

The last key (or keys) is variable and customizable to your needs which is the log event message. The only special key is the tags key which is explained later. Here a couple of examples to illustrate:

# Available as "%<message>s" in your template.
logger.info "demo"

# Available as "%<message>s" in your template.
logger.info message: "demo"

# Available as "%<verb>s" and "%<path>s" in your template.
logger.info verb: "GET", path: "/"`

💡 In situations where a message hash is logged but the keys of that hash don’t match the keys in the template, then an empty message will be logged. This applies to all formatters except the JSON formatter which will log any key/value that doesn’t have a nil value.

Emojis

In addition to coloring to your log output, you can add emojis as well. Here are the defaults:

Cogger.emojis

# {
#   :debug => "🔎",
#    :info => "🟢",
#    :warn => "⚠️ ",
#   :error => "🛑",
#   :fatal => "🔥",
#     :any => "⚫️"
# }

The :emoji formatter is the default formatter which provides dynamic rendering of emojis based on severity level. Example:

logger = Cogger.new
logger.info "Demo"

# 🟢 Demo

To add one or more emojis, you can chain messages together when registering them:

Cogger.add_emoji(:tada, "🎉")
      .add_emoji :favorite, "❇️"

If you always want to use the same emoji, you could use the emoji formatter with a specific template:

logger = Cogger.new formatter: Cogger::Formatters::Emoji.new("%<emoji:tada>s %<message:dynamic>s")

logger.info "Demo"
logger.warn "Demo"

# 🎉 Demo
# 🎉 Demo

As you can see, using a specific and non-dynamic emoji will always display regardless of the current severity level.

Aliases

Aliases are specific to the Tone gem which allows you alias specific colors/styles via a new name. Here’s how you can use them:

Cogger.add_alias :haze, :bold, :white, :on_purple
Cogger.aliases

The above would add a :haze alias which consists of bold white text on a purple background. Once added, you’d then be able to view a list of all default and custom aliases. You can also override an existing alias if you’d like something else.

Aliases are a powerful way to customize your colors and use short syntax in your templates. Building upon the alias, added above, you’d be able to use it in your templates as follows:

# Universal
"<haze>%<message></haze>"

# Individual
"%<message:haze>"

💡 These aliases are used by the color and emoji formatters but check out the Tone documentation and Formatters section below for further examples.

Formatters

Multiple formatters are provided for you which can be further customized as needed. Here’s what is provided by default:

Cogger.formatters

# {
#    :color => [
#     Cogger::Formatters::Color < Object,
#     nil
#   ],
#   :detail => [
#     Cogger::Formatters::Simple < Object,
#     "[%<id>s] [%<severity>s] [%<at>s] %<message>s"
#   ],
#    :emoji => [
#     Cogger::Formatters::Emoji < Cogger::Formatters::Color,
#     nil
#   ],
#     :json => [
#     Cogger::Formatters::JSON < Object,
#     nil
#   ],
#   :simple => [
#     Cogger::Formatters::Simple < Object,
#     nil
#   ],
#     :rack => [
#     Cogger::Formatters::Simple < Object,
#     "[%<id>s] [%<severity>s] [%<at>s] %<verb>s %<status>s %<duration>s %<ip>s %<path>s %<length>s # %<params>s"
#   ]
# }

You can add a formatter by providing a key, class, and optional template. If a template isn’t supplied, then the formatter’s default template will be used instead (more on that shortly). Example:

# Registration
Cogger.add_formatter :basic, Cogger::Formatters::Simple, "%<severity>s %<message>s"

# Usage
Cogger.get_formatter :basic
# [Cogger::Formatters::Simple, "%<severity>s %<message>s"]

Symbols or strings can be used interchangeably when adding/getting formatters. As mentioned above, a template doesn’t have to be supplied if you want to use the formatter’s default template which can be inspected as follows:

Cogger::Formatters::Color::TEMPLATE   # "%<message:dynamic>s"
Cogger::Formatters::Emoji::TEMPLATE   # "%<emoji:dynamic>s %<message:dynamic>s"
Cogger::Formatters::JSON::TEMPLATE    # nil
Cogger::Formatters::Simple::TEMPLATE  # "%<message>s"

💡 When you find yourself customizing any of the default formatters, you can reduce typing by adding your custom configuration to the registry and then referring to it via it’s associated key when initializing a new logger.

Simple

The simple formatter is a bare bones formatter that uses no color information, doesn’t support the universal/dynamic template syntax, and only supports the String Format Specification as mentioned in the Templates section earlier. This formatter can be used via the following template variations:

logger = Cogger.new formatter: :detail
logger = Cogger.new formatter: :simple
logger = Cogger.new formatter: :rack

ℹ️ Any leading or trailing whitespace is automatically removed after the template has been formatted in order to account for template attributes that might be nil or empty strings so you don’t have visual indentation in your output.

Color

The color formatter allows you to have color coded logs and can be configured as follows:

logger = Cogger.new formatter: :color

Please refer back to the Templates section on how to customize this formatter with more sophisticated templates. In addition to template customization, you can customize your color aliases as well. Default colors are provided by Tone which are aliased by log level:

Cogger.aliases

# {
#   debug: :white,
#   info: :green,
#   warn: :yellow,
#   error: :red,
#   fatal: %i[bold white on_red],
#   any: %i[dim bright_white]
# }

This allows a color — or combination of color styles (i.e. foreground + background) — to be dynamically applied based on log severity. You can add additional aliases via:

Cogger.add_alias :mystery, :white, :on_purple

Once an alias is added, it can be immediately applied via the template of your formatter. Example:

# Applies the `mystery` alias universally to your template.
logger = Cogger.new formatter: Cogger::Formatters::Color.new("<mystery>%<message>s</mystery>")

ℹ️ Much like the simple formatter, any leading or trailing whitespace is automatically removed after the template has been formatted.

Emoji

The emoji formatter is enabled by default and is the equivalent of initializing with either of the following:

logger = Cogger.new
logger = Cogger.new formatter: :emoji
logger = Cogger.new formatter: Cogger::Formatters::Emoji.new("%<emoji:dynamic>s %<message:dynamic>s")

All of the above examples are identical so you can see how different formatters can be used and customized further. The default emojis are registered as follows:

Cogger.emojis

# {
#   :debug => "🔎",
#    :info => "🟢",
#    :warn => "⚠️ ",
#   :error => "🛑",
#   :fatal => "🔥",
#     :any => "⚫️"
# }

This allows an emoji to be dynamically applied based on log severity. You can add or modify aliases as follows:

Cogger.add_emoji :warn, "🟡"

Once an alias is added/updated, it can be immediately applied via the template of your formatter. Example:

logger = Cogger.new
logger.warn "Demo"
# 🟡 Demo

ℹ️ Much like the simple and color formatters, any leading or trailing whitespace is automatically removed after the template has been formatted.

JSON

This formatter is similar in behavior to the simple formatter except that date/time defaults to UTC, is formattted according to RFC 3339 using millisecond precision, and the template allows you to order the layout of your keys. All other template information is ignored, only the order of your template keys matters. Example:

Default Order

logger = Cogger.new formatter: :json

logger.info verb: "GET", path: "/"
# {"id":"console","severity":"INFO","at":"2023-12-10T18:42:32.844+00:00","verb":"GET","path":"/"}

Custom Order

logger = Cogger.new formatter: Cogger::Formatters::JSON.new("%<severity>s %<verb>s")

logger.info verb: "GET", path: "/"
# {"severity":"INFO","verb":"GET","id":"console","at":"2023-12-10T18:43:03.805+00:00","path":"/"}

Your template can be a full or partial match of keys. If no keys match what is defined in the template, then the original order of the keys will be used instead.

You can always supply a message as your first argument — or specify it by using the :message key — but is removed if not supplied which is why the above doesn’t print a message in the output. To illustrate, the following are equivalent:

logger = Cogger.new formatter: :json

logger.info "Demo"
# {"id":"console","severity":"INFO","at":"2023-12-10T18:43:42.029+00:00","message":"Demo"}

logger.info message: "Demo"
# {"id":"console","severity":"INFO","at":"2023-12-10T18:44:14.568+00:00","message":"Demo"}

When tags are provided, the :tags key will appear in the output depending on whether you are using single tags. If hash tags are used, they’ll show up as additional attributes in the output. Here’s an example where a mix of single and hash keys are used:

logger = Cogger.new formatter: :json

logger.info "Demo", tags: ["WEB", "PRIMARY", {service: :api, demo: true}]
# {
#   "id":"console",
#   "severity":"INFO",
#   "at":"2023-12-10T18:44:32.723+00:00",
#   "message":"Demo",
#   "tags":["WEB",
#   "PRIMARY"],
#   "service":"api",
#   "demo":true
# }

Notice, with the above, that the single tags of WEB and PRIMARY show up in the tags array while the :service and :demo keys show up at the top level of the hash. Since the :tags, :service, :demo keys are normal keys, like any key in your JSON output, this means you can use a custom template to arrange the order of these keys if you don’t like the default.

Native

Should you wish to use the native formatter as provided by original/native Logger, it will work but not in the manner you might expect. Example:

require "logger"

logger = Cogger.new formatter: Logger::Formatter.new
logger.info "Demo"

# I, [2023-10-15T14:32:55.061777 #72801]  INFO -- console: #<data Cogger::Entry id="console", severity=:info, at=2023-10-15 14:32:55.061734 -0600, message="Demo", tags=[], payload={}>

While the above doesn’t cause an error, you only get a dump of the Cogger::Entry which is not what you want. To replicate native Logger functionality, you can do use the simple formatter as follows to produce the rough equivalent:

formatter = Cogger::Formatters::Simple.new(
  "%<severity>s, [%<at>s]  %<severity>s -- %<id>s: %<message>s"
)
logger = Cogger.new(formatter:)
logger.info "Demo"

# INFO, [2023-10-15 15:07:13 -0600]  INFO -- console: Demo

Custom

Should none of the built-in formatters be to your liking, you can implement, use, and/or register a custom formatter as well. The most minimum, bare bones, skeleton would be:

class MyFormatter
  TEMPLATE = "%<message>s"

  def initialize template = TEMPLATE, sanitizer: Kit::Sanitizer.new
    @template = template
    @sanitizer = sanitizer
  end

  def call(*input) = "#{format template, sanitizer.call(*input)}\n"

  private

  attr_reader :template, :sanitizer
end

There is no restriction on what dependency you might want to initialize your custom formatter with but — as a bare minimum — you’ll want to provide a default template and inject the sanitizer which sanitizes the raw input into a Cogger::Entry object you can interact with in your implementation. The only other requirement is that you must implement #call which takes a log entry which is an array of positional arguments (i.e. severity, at, id, entry) and answers back a formatted string. If you need more examples you can look at any of the formatters provided within this gem.

Tags

Tags allow you to tag your messages at both a global and local (i.e. per message) level. For example, here’s what tagging looks like when used globally:

logger = Cogger.new tags: %w[WEB]
logger.info "Demo"

# 🟢 [WEB] Demo

Each tag is wrapped in brackets (i.e. []) and you can use multiple tags:

logger = Cogger.new tags: %w[WEB EXAMPLE]
logger.info "Demo"

# 🟢 [WEB] [EXAMPLE] Demo

You are not limited to string-based tags. Any object will work:

logger = Cogger.new tags: ["ONE", :two, 3, {four: "FOUR"}, proc { "FIVE" }]
logger.info "Demo"

# 🟢 [ONE] [two] [3] [FIVE] [four=FOUR] Demo

With the above, we have string, symbol, integer, hash, and proc tags. With hashes, you’ll always get a the key/value pair formatted as: key=value. Procs/lambdas allow you to lazy evaluate your tag at time of logging which provides a powerful way to acquire the current process ID, thread ID, and so forth.

In addition to global tags, you can use local tags per log message. Example:

logger = Cogger.new
logger.info "Demo", tags: ["ONE", :two, 3, {four: "FOUR"}, proc { "FIVE" }]

# 🟢 [ONE] [two] [3] [FIVE] [four=FOUR] Demo

You can also combine global and local tags:

logger = Cogger.new tags: ["ONE", :two]
logger.info "Demo", tags: [3, proc { "FOUR" }]

# 🟢 [ONE] [two] [3] [FOUR] Demo

As you can see, tags are highly versatile. That said, the following guidelines are worth consideration when using them:

  • Prefer uppercase tag names to make them visually stand out.

  • Prefer short names, ideally 1-4 characters since long tags defeat the purpose of brevity.

  • Prefer consistent tag names by using tags that are not synonymous or ambiguous.

  • Prefer using tags by feature rather than things like environments. Examples: API, DB, MAILER.

  • Prefer the JSON formatter for structured metadata instead of tags. Logging JSON formatted messages with tags will work but sticking with a traditional hash, instead of tags, will probably serve you better.

Filters

Filters allow you to mask sensitive information you don’t want showing up in your logs. The default is an empty set:

Cogger.filters  # #<Set: {}>

To add filters, use:

Cogger.add_filter(:login)
      .add_filter "email"

Cogger.filters  # #<Set: {:login, :email}>

Symbols and strings can be used interchangeably but are stored as symbols since symbols are used when filtering log entries. Once your filters are in place, you can immediately see their effects:

Cogger.add_filter :password
logger = Cogger.new formatter: :json
logger.info login: "jayne", password: "secret"

# {
#   "id":"console",
#   "severity":"INFO",
#   "at":"2023-10-18 19:21:40 -0600",
#   "login":"jayne",
#   "password":"[FILTERED]"
# }

Streams

You can add multiple log streams (outputs) by using:

logger = Cogger.new
               .add_stream(io: "tmp/demo.log")
               .add_stream(io: nil)

logger.info "Demo."

The above would log the "Demo." message to $stdout — the default stream — to the tmp/demo.log file, and to /dev/null. All attributes used to construct your default logger apply to all additional streams unless customized further. This means any custom template/formatter can be applied to your streams. Example:

logger = Cogger.new.add_stream(io: "tmp/demo.log", formatter: :json)
logger.info "Demo."

In this situation, you’d get colorized output to $stdout and JSON output to the tmp/demo.log file.

There is a lot you can do with streams. For example, if you wanted to experiment with the same message formatted by multiple formatters, you could add a stream per format. Example:

logger = Cogger.new
               .add_stream(formatter: :color)
               .add_stream(formatter: :detail)
               .add_stream(formatter: :json)
               .add_stream(formatter: :simple)

logger.info "Demo"

# 🟢 Demo
# Demo
# [console] [INFO] [2023-10-15 15:17:27 -0600] Demo
# {"id":"console","severity":"INFO","at":"2023-10-15 15:17:27 -0600","message":"Demo"}
# Demo

Abort

Aborting a program is mostly syntax sugar for Command Line Interfaces (CLIs) which aids in situations where you need to log an error message and exit the program at the same time with an exit code of 1 (similar to how Kernel#abort behaves). This allows your CLI to log an error and ensure the exit status is correct when displaying status, piping commands together, etc. All of the arguments, when messaging #error directly, are the same. Here’s how it works:

logger = Cogger.new

# Not recommended since `Kernel#exit false` is faster.
logger.abort
# Logs no message and exits with status code: 1.

logger.abort "Danger!"
# 🛑 Danger!
# Exits with status code: 1.

logger.abort { "Danger!" }
# 🛑 Danger!
# Exits with status code: 1.

logger.abort message: "Danger!"
# 🛑 Danger!
# Exits with status code: 1.

Rack

Rack is implicitly supported which means your middleware must be Rack-based and must require the Rack gem since Cogger::Rack::Logger doesn’t explicitly require Rack by default. If these requirements are met then, to add HTTP request logging, you only need to use it. Example:

use Rails::Rack::Logger

Like any other Rack middleware, Rails::Rack::Logger is initialized with your current application along with any custom options. Example:

middleware = Cogger::Rack::Logger.new application
middleware.call environment

The following defaults are supported:

Cogger::Rack::Logger::DEFAULTS

# {
#   logger: Cogger.new(formatter: :json),
#   timer: Cogger::Time::Span.new,
#   :key_map => {
#       :verb => "REQUEST_METHOD",
#         :ip => "REMOTE_ADDR",
#       :path => "PATH_INFO",
#     :params => "QUERY_STRING",
#     :length => "CONTENT_LENGTH"
#   }
# }

The defaults can be customized. Example:

Cogger::Rack::Logger.new application, {logger: Cogger.new}

In the above example, we see Cogger.new overrides the default Cogger.new(formatter: :json). In practice, you’ll want to customize the logger and key map. Here’s how each default is configured to be used:

  • logger: Defaults to JSON formatted logging but you’ll want to pass in the same logger as globally configured for your application in order to reduce duplication and save on memory.

  • timer: The timer calculates the total duration of the request and defaults to nanosecond precision but you can swap this out with your own timer if desired. When providing your own timer, the only requirement is that the timer respond to the #call message with a block.

  • key_map: The key map is used to map the HTTP Headers to keys (i.e. tags) used in the log output. You can use the existing key map, provide your own, or use a hybrid.

Once this middleware is configured and used within your application, you’ll start seeing the following kinds of log entries (depending on your specific settings and tags used):

{
  "id":"demo",
  "severity":"INFO",
  "at":"2023-12-10T22:37:06.341+00:00",
  "verb":"GET",
  "ip":"127.0.0.1",
  "path":"/dashboard",
  "status":200,
  "duration":83,
  "unit":"ms"
}

Rails

To build upon the above — and if using the Rails framework — you could configure your application as follows:

# demo/config/application.rb
module Demo
  class Application < Rails::Application
    config.logger = Cogger.new id: "demo", formatter: :json,
    config.middleware.swap Rails::Rack::Logger, Cogger::Rack::Logger, {logger: config.logger}
  end
end

The above defines Cogger as the default logger for the entire application, ensures Cogger::Rack::Logger is configured to use it and swaps itself with the default Rails::Rack::Logger so you don’t have two pieces of middleware logging the same HTTP requests.

Defaults

Should you ever need quick access to the defaults, you can use:

Cogger.defaults

This is primarily meant for display/inspection purposes, though.

Inspection

Each instance can be inspected via the #inspect message:

logger = Cogger.new
logger.inspect

# "#<Cogger::Hub @id=console,
#                @io=IO,
#                @level=1,
#                @formatter=Cogger::Formatters::Emoji,
#                @tags=[],
#                @mode=false,
#                @age=0,
#                @size=1048576,
#                @suffix=\"%Y-%m-%d\",
#                @entry=Cogger::Entry,
#                @logger=Logger>"

You can also look at individual attributes:

logger = Cogger.new

logger.id      # "console"
logger.io      # #<IO:<STDOUT>>
logger.tags    # []
logger.mode    # false
logger.age     # 0
logger.size    # 1048576
logger.suffix  # "%Y-%m-%d"

logger.level      # 1
logger.formatter  # Cogger::Formatters::Emoji
logger.debug?     # false
logger.info?      # true
logger.warn?      # true
logger.error?     # true
logger.fatal?     # true

Testing

When testing, you might find it convenient to rewind and read from the stream you are writing too (i.e. IO, StringIO, File). For instance, here is an example where I inject the default logger into my Demo class and then, for testing purposes, create a new logger to be injected which only logs to StringIO so I can buffer and read for test verification:

class Demo
  def initialize logger: Cogger.new
    @logger = logger
  end

  def say(text) = logger.info { text }

  private

  attr_reader :logger
end

RSpec.describe Demo do
  subject(:demo) { described_class.new logger: }

  let(:logger) { Cogger.new io: StringIO.new }

  describe "#say" do
    it "logs text" do
      demo.say "test"
      expect(logger.reread).to include("test")
    end
  end
end

The ability to #reread is only available for the default (first) stream and doesn’t work with any additional streams that you add to your logger. That said, this does make it easy to test the Demo implementation while also keeping your test suite output clean at the same time. 🎉

Development

To contribute, run:

git clone https://github.com/bkuhlmann/cogger
cd cogger
bin/setup

You can also use the IRB console for direct access to all objects:

bin/console

Lastly, there is a bin/demo script which displays multiple log formats for quick visual reference. This is the same script used to generate the screenshots shown at the top of this document.

Tests

To test, run:

bin/rake

Credits