
Cogger
0.11.0
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. đ
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 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 filtering of sensitive information.
Screenshots
Emoji

Color

Simple

Detail

JSON

Rack

Requirements
-
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"
# 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"
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 yourid
. Default:$PROGRAM_NAME
. For example, if run within ademo.rb
script, theid
would be"demo"
, -
io
: The input/output stream. This can beSTDOUT/$stdout
, a file/path, ornil
. 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. -
mode
: The binary mode which determines if your logs should be written in binary mode or not. Can betrue
orfalse
and is identical to thebinmode
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 theshift_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 theshift_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 theshift_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,
mode: false,
age: 5,
size: 1_000,
suffix: "%Y"
Environment
The default log level is INFO
but can be customized via your environment. For instance, you could
set the logging 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
By default, Cogger
will automatically use whatever is set via the LOG_LEVEL
environment variable unless overwritten during initialization.
Templates
Templates are used by all formatters and adhere to 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 Format Specification is further enhanced with the use of universal and individual directives which are primarily used by the color formatter but might 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. 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 => "âĢī¸"
# }
To add an emoji, use:
Cogger.add_emoji(:tada, "đ")
.add_emoji :favorite, "âī¸"
By default, the :emoji
formatter provides dynamic rendering of emojis based on severity level. Example:
logger = Cogger.new formatter: :emoji
logger.info "demo"
# đĸ demo
If you wanted to use a specific emoji, you could use the color formatter with a specific template:
logger = Cogger.new formatter: Cogger::Formatters::Color.new("%<emoji:tada>s %<message:dynamic>s")
logger.info "demo"
# đ demo
Keep in mind that using a specific, non-dynamic, emoji will always display no matter 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>"
Check out the Tone documentation 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::Color < Object,
# "%<emoji:dynamic>s% <message:dynamic>s"
# ],
# :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:
# Add
Cogger.add_formatter :basic, Cogger::Formatters::Simple, "%<severity>s %<message>s"
# Get
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::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 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 is enabled by default and is the equivalent of initializing with either of the following:
logger = Cogger.new
logger = Cogger.new formatter: Cogger::Formatters::Color.new
logger = Cogger.new formatter: Cogger::Formatters::Color.new("%<message:dynamic>s")
All three of the above examples are identical so you can start to see how different formatters can be used and customized further. 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 after the template has been formatted.
JSON
This formatter is similar in behavior to the simple formatter except the template allows you to order the layout of your keys only. All other information is ignored. To use:
# Default order
logger = Cogger.new formatter: :json
logger.info verb: "GET", path: "/"
# {"id":"console","severity":"INFO","at":"2023-04-10 09:03:55 -0600","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-04-10 09:05:03 -0600","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.
Original
Should you wish to use the original formatter as provided by original/native Logger, you can get that behavior by specifying it as your preferred formatter. Example:
require "logger"
logger = Cogger.new formatter: Logger::Formatter.new
logger.info "demo"
# I, [2023-04-11T19:35:51.175733 #84790] 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(*entry) = "#{format template, sanitizer.call(*entry)}\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 log entry into a hash 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
, message
) and answers back a formatted string. If you need more examples you can either read the Logger::Formatter documentation or look at any of the formatters provided within this gem.
Filters
Filters allow you to mask sensitive information you don’t want showing up in your logs. Here are the defaults:
Cogger.filters
# [
# :_csrf,
# :password,
# :password_confirmation
# ]
To add additional filters, use:
Cogger.add_filter(:login)
.add_filter "email"
# [
# :_csrf,
# :password,
# :password_confirmation,
# :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:
logger = Cogger.new formatter: :json
logger.info login: "jayne", password: "secret"
# {"id":"console","severity":"INFO","at":"2023-04-09 17:33:00 -0600","login":"[FILTERED]","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
(i.e. the default stream), to the tmp/demo.log
file, and to /dev/null
. All of the attributes you would use to construct your default logger apply to any stream. This also means any custom template/formatter can be applied to your streams. Here’s another 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.
Defaults
Should you ever need quick access to the defaults, you can use:
Cogger.defaults
This is primarily meant for display/inspection purposes, though.
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/show
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
-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.