Permalink
Browse files

Coloured logger (#251)

  • Loading branch information...
oana-sipos authored and jodosha committed Mar 29, 2018
1 parent bfdabda commit c9738379c8ad83a4a8a8084b4714b459c1802a7b
Showing with 346 additions and 25 deletions.
  1. +159 −25 lib/hanami/logger.rb
  2. +41 −0 lib/hanami/utils/shell_color.rb
  3. +101 −0 spec/unit/hanami/logger_spec.rb
  4. +45 −0 spec/unit/hanami/utils/shell_color_spec.rb
@@ -6,6 +6,7 @@
require 'hanami/utils/hash'
require 'hanami/utils/class_attribute'
require 'hanami/utils/files'
require 'hanami/utils/shell_color'

module Hanami
# Hanami logger
@@ -119,11 +120,12 @@ class Formatter < ::Logger::Formatter
class_attribute :subclasses
self.subclasses = Set.new

def self.fabricate(formatter, application_name, filters)
def self.fabricate(formatter, application_name, filters, colorizer: NullColorizer.new)
fabricated_formatter = _formatter_instance(formatter)

fabricated_formatter.application_name = application_name
fabricated_formatter.hash_filter = HashFilter.new(filters)
fabricated_formatter.colorizer = colorizer

fabricated_formatter
end
@@ -165,18 +167,18 @@ def self._formatter_instance(formatter)
# @api private
attr_writer :hash_filter

# @since x.x.x
# @api private
attr_writer :colorizer

# @since 0.5.0
# @api private
#
# @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html#method-i-call
def call(severity, time, _progname, msg)
_format({
app: application_name,
severity: severity,
time: time
}.merge(
_message_hash(msg)
))
def call(severity, time, progname, msg)
colorized = @colorizer.call(application_name, severity, time, progname)
colorized.delete(:progname)
_format(colorized.merge(_message_hash(msg)))
end

private
@@ -201,25 +203,36 @@ def _message_hash(message) # rubocop:disable Metrics/MethodLength
# @since 0.8.0
# @api private
def _format(hash)
result = RESERVED_KEYS.map { |k| "[#{hash[k]}]" }.join(SEPARATOR)
return _format_error(result, hash) if hash.key?(:error)
[
_line_front_matter(hash[:app], hash[:severity], hash[:time]),
_format_message(hash)
].join(SEPARATOR)
end

values = hash.each_with_object([]) do |(k, v), memo|
memo << v unless RESERVED_KEYS.include?(k)
end
# @since x.x.x
# @api private
def _line_front_matter(*args)
args.map { |string| "[#{string}]" }.join(SEPARATOR)
end

result << " #{values.join(SEPARATOR)}#{NEW_LINE}"
result
# @since x.x.x
# @api private
def _format_message(hash)
if hash.key?(:error)
_format_error(hash)
else
hash.each_with_object([]) do |(k, v), memo|
memo << v unless RESERVED_KEYS.include?(k)
end.join(SEPARATOR).concat(NEW_LINE)
end
end

# @since x.x.x
# @api private
def _format_error(result, hash)
result << " #{hash[:error]}:" if hash.key?(:error)
result << " #{hash[:message]}#{NEW_LINE}"
if hash.key?(:backtrace)
hash[:backtrace].each do |line|
result << "from #{line}#{NEW_LINE}"
end
def _format_error(hash)
result = [hash[:error], hash[:message]].compact.join(": ").concat(NEW_LINE)
hash[:backtrace].each do |line|
result << "from #{line}#{NEW_LINE}"
end

result
@@ -308,6 +321,113 @@ def _format(hash)
end
end

# Hanami::Logger NullColorizer
# This colorizer does nothing. It should be used when writing to a file, or
# whenever you're not using a tty.
#
# When creating a new Colorizer, you should inherit from this class,
# and define your own `_severity`, `_datetime`, `_progname` and `_msg`
# methods.
#
# @since x.x.x
# @api private
class NullColorizer
# @since x.x.x
# @api private
def call(app, severity, datetime, progname)
{
app: _app(app),
severity: _severity(severity),
time: _datetime(datetime),
progname: _progname(progname)
}
end

private

# @since x.x.x
# @api public
def _app(input)
input
end

# @since x.x.x
# @api public
def _severity(input)
input
end

# @since x.x.x
# @api public
def _datetime(input)
input
end

# @since x.x.x
# @api public
def _progname(input)
input
end
end

# Hanami::Logger DEfault Colorizer
# This colorizer takes in parts of the log message and returns them with
# proper shellcode to colorize when displayed to a tty.
#
# @since x.x.x
# @api private
class DefaultColorizer < NullColorizer
# The colors defined for the four parts of the log message
#
# These can be overridden, keeping the name keys, with acceptable values
# being any from Hanami::Utils::ShellColor::COLORS
#
# @since x.x.x
# @api public
COLORS = Hash[
app: :yellow,
severity: :cyan,
datetime: :green,
progname: nil,
].freeze

private

# @since x.x.x
# @api private
def _app(input)
_colorized(input, color: COLORS.fetch(:app))
end

# @since x.x.x
# @api private
def _severity(input)
_colorized(input, color: COLORS.fetch(:severity))
end

# @since x.x.x
# @api private
def _datetime(input)
_colorized(input, color: COLORS.fetch(:datetime))
end

# @since x.x.x
# @api private
def _progname(input)
_colorized(input, color: COLORS.fetch(:progname))
end

# @since x.x.x
# @api private
def _colorized(message, color:)
if color
Hanami::Utils::ShellColor.colorize(message, color: color)
else
message
end
end
end

# Default application name.
# This is used as a fallback for tagging purposes.
#
@@ -456,19 +576,33 @@ def _format(hash)
# # => {"app":"Hanami","severity":"DEBUG","time":"2017-03-30T13:57:59Z","message":"Hello World"}
# rubocop:disable Lint/HandleExceptions
# rubocop:disable Metrics/ParameterLists
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil, filter: [])
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil, filter: [], colorizer: nil)
begin
Utils::Files.mkdir_p(stream)
rescue TypeError
end

super(stream, *args)

colorizer ||= default_colorizer
@level = _level(level)
@stream = stream
@application_name = application_name
@formatter = Formatter.fabricate(formatter, self.application_name, filter)
@formatter = Formatter.fabricate(formatter, self.application_name, filter, colorizer: colorizer)
end

# @since x.x.x
# @api private
def tty?
@logdev.dev.tty?
end

# @since x.x.x
# @api private
def default_colorizer
(tty? && DefaultColorizer.new) || NullColorizer.new
end

# rubocop:enable Metrics/ParameterLists
# rubocop:enable Lint/HandleExceptions

@@ -0,0 +1,41 @@
module Hanami
module Utils
# Shellcode helper for colorizing STDOUT
#
# It doesn't check if you're writing to a file or anything, so you have to
# check that yourself before using this module.
module ShellColor
# Escape codes for terminals to output strings in colors
#
# @since x.x.x
# @api private
COLORS = ::Hash[
black: 30,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
gray: 37,
].freeze

# Colorize output
# 8 colors available: black, red, green, yellow, blue, magenta, cyan, and gray
#
# @api public
# @since x.x.x
def self.colorize(input, color:)
"\e[#{color_code(color)}m#{input}\e[0m"
end

# Helper method to translate between color names and terminal escape codes
#
# @api private
# @since x.x.x
def self.color_code(name)
COLORS.fetch(name)
end
end
end
end
Oops, something went wrong.

0 comments on commit c973837

Please sign in to comment.