ConsoleGuidesGetting Started

Getting Started

This guide explains how to use console for logging.

Installation

Add the gem to your project:

$ bundle add console

Core Concepts

console has several core concepts:

Basic Logging

Out of the box, Console.logger is a globally shared logger that outputs to the current terminal via stderr.

require 'console'

Console.info("Hello World")
  0.0s     info: Hello World [pid=219113] [2020-08-08 12:21:26 +1200]

The method name info indicates the severity level of the log message. You can filter out severity levels, and by default, debug messages are filtered out. Here are some examples of the different log levels:

require 'console'

Console.debug("The input voltage has stabilized.")
Console.info("Making a request to the machine.")
Console.warn("The machine has detected a temperature anomaly.")
Console.error("The machine was unable to complete the request!")
Console.fatal("Depressurisation detected, evacuate the area!")

From the terminal, you can control the log level using the CONSOLE_LEVEL environment variable. To log all messages including debug:

$ CONSOLE_LEVEL=debug ./machine

Alternatively to restrict log messages to warnings and above:

$ CONSOLE_LEVEL=warn ./machine

If otherwise unspecified, Ruby's standard $DEBUG and $VERBOSE global variables will be checked and adjust the log level appropriately.

Metadata

You can add any options you like to a log message and they will be included as part of the log output:

duration = measure{...}
Console.info("Execution completed!", duration: duration)

Subject Logging

The first argument to the log statement becomes the implicit subject of the log message.

require 'console'

class Machine
	def initialize(voltage)
		@voltage = voltage.floor
		Console.info(self, "The input voltage has stabilized.")
	end
end

Machine.new(5.5)

The given subject, in this case self, is used on the first line, along with associated metadata, while the message itself appears on subsequent lines:

  0.0s     info: Machine [oid=0x3c] [pid=219041] [2020-08-08 12:17:33 +1200]
               | The input voltage has stabilized.

If you want to disable log messages which come from this particular class, you can execute your command:

$ CONSOLE_FATAL=Machine ./machine

This will prevent any log message which has a subject of class Machine from logging messages of a severity less than fatal.

Exception Logging

If your code has an unhandled exception, you may wish to log it. In order to log a full backtrace, you must log the subject followed by the exception.

require 'console'

class Cache
	def initialize
		@entries = {}
	end
	
	def fetch(key)
		@entries.fetch(key)
	rescue => error
		Console::Event::Failure.for(error).emit(self, "Cache fetch failure!")
	end
end

Cache.new.fetch(:foo)

This will produce the following output:

  0.0s    error: Cache [oid=0x848] [ec=0x85c] [pid=571936] [2024-05-03 10:55:11 +1200]
               | Cache fetch failure!
               |   KeyError: key not found: :foo (KeyError)
               |   → test.rb:15 in `fetch'
               |     test.rb:15 in `fetch'
               |     test.rb:21 in `<top (required)>'

Program Structure

Generally, programs should use the global Console interface to log messages.

Individual classes should not be catching and logging exceptions. It makes for simpler code if this is handled in a few places near the top of your program. Exceptions should collect enough information such that logging them produces a detailed backtrace leading to the failure.

Multiple Outputs

Use Console::Split to log to multiple destinations.

require 'console'
require 'console/output/split'

terminal = Console::Output::Terminal.new($stderr)
serialized = Console::Output::Serialized.new(File.open("log.json", "a"))
Console.logger = Console::Logger.new(Console::Output::Split[terminal, serialized])

Console.info "I can go everywhere!"

Custom Log Levels

Console::Filter implements support for multiple log levels.

require 'console'

MyLogger = Console::Filter[noise: 0, stuff: 1, broken: 2]

# verbose: true - log severity/name/pid etc.
logger = MyLogger.new(Console.logger, name: "Java", verbose: true)

logger.broken("It's so janky.")