lib/hanami/logger.rb in hanami-utils-0.7.2 vs lib/hanami/logger.rb in hanami-utils-0.8.0
- old
+ new
@@ -1,36 +1,53 @@
+require 'set'
+require 'json'
require 'logger'
require 'hanami/utils/string'
+require 'hanami/utils/json'
+require 'hanami/utils/class_attribute'
module Hanami
# Hanami logger
#
# Implement with the same interface of Ruby std lib `Logger`.
- # It uses `STDOUT` as output device.
+ # It uses `STDOUT`, `STDERR`, file name or open file as output stream.
#
#
- #
# When a Hanami application is initialized, it creates a logger for that specific application.
# For instance for a `Bookshelf::Application` a `Bookshelf::Logger` will be available.
#
- # This is useful for auto-tagging the output. Eg (`[Booshelf]`).
+ # This is useful for auto-tagging the output. Eg (`app=Booshelf`).
#
- # When used stand alone (eg. `Hanami::Logger.info`), it tags lines with `[Shared]`.
+ # When used stand alone (eg. `Hanami::Logger.info`), it tags lines with `app=Shared`.
#
#
- #
# The available severity levels are the same of `Logger`:
#
- # * debug
- # * error
- # * fatal
- # * info
- # * unknown
- # * warn
+ # * DEBUG
+ # * INFO
+ # * WARN
+ # * ERROR
+ # * FATAL
+ # * UNKNOWN
#
# Those levels are available both as class and instance methods.
#
+ # Also Hanami::Logger support different formatters. Now available only two:
+ #
+ # * Formatter (default)
+ # * JSONFormatter
+ #
+ # And if you want to use custom formatter you need create new class inherited from
+ # `Formatter` class and define `_format` private method like this:
+ #
+ # class CustomFormatter < Formatter
+ # private
+ # def _format(hash)
+ # # ...
+ # end
+ # end
+ #
# @since 0.5.0
#
# @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html
# @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Severity.html
#
@@ -45,74 +62,180 @@
# # Initialize the application with the following code:
# Bookshelf::Application.load!
# # or
# Bookshelf::Application.new
#
- # Bookshelf::Logger.info('Hello')
- # # => I, [2015-01-10T21:55:12.727259 #80487] INFO -- [Bookshelf] : Hello
- #
# Bookshelf::Logger.new.info('Hello')
- # # => I, [2015-01-10T21:55:12.727259 #80487] INFO -- [Bookshelf] : Hello
+ # # => app=Bookshelf severity=INFO time=1988-09-01 00:00:00 UTC message=Hello
#
# @example Standalone usage
- # require 'hanami'
+ # require 'hanami/logger'
#
- # Hanami::Logger.info('Hello')
- # # => I, [2015-01-10T21:55:12.727259 #80487] INFO -- [Hanami] : Hello
- #
# Hanami::Logger.new.info('Hello')
- # # => I, [2015-01-10T21:55:12.727259 #80487] INFO -- [Hanami] : Hello
+ # # => app=Hanami severity=INFO time=2016-05-27 10:14:42 UTC message=Hello
#
# @example Custom tagging
- # require 'hanami'
+ # require 'hanami/logger'
#
# Hanami::Logger.new('FOO').info('Hello')
- # # => I, [2015-01-10T21:55:12.727259 #80487] INFO -- [FOO] : Hello
+ # # => app=FOO severity=INFO time=2016-05-27 10:14:42 UTC message=Hello
+ #
+ # @example Write to file
+ # require 'hanami'
+ #
+ # Hanami::Logger.new(stream: 'logfile.log').info('Hello')
+ # # in logfile.log
+ # # => app=FOO severity=INFO time=2016-05-27 10:14:42 UTC message=Hello
+ #
+ # @example Use JSON formatter
+ # require 'hanami'
+ #
+ # Hanami::Logger.new(formatter: Hanami::Logger::JSONFormatter).info('Hello')
+ # # => "{\"app\":\"Hanami\",\"severity\":\"INFO\",\"time\":\"1988-09-01 00:00:00 UTC\",\"message\":\"Hello\"}"
class Logger < ::Logger
- # Hanami::Logger default formatter
+ # Hanami::Logger default formatter.
+ # This formatter returns string in key=value format.
#
# @since 0.5.0
# @api private
#
# @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html
class Formatter < ::Logger::Formatter
+ # @since 0.8.0
+ # @api private
+ SEPARATOR = ' '.freeze
+
+ # @since 0.8.0
+ # @api private
+ NEW_LINE = $/
+
+ include Utils::ClassAttribute
+
+ class_attribute :subclasses
+ self.subclasses = Set.new
+
+ def self.fabricate(formatter, application_name)
+ case formatter
+ when Symbol
+ (subclasses.find { |s| s.eligible?(formatter) } || self).new
+ when nil
+ new
+ else
+ formatter
+ end.tap { |f| f.application_name = application_name }
+ end
+
+ def self.inherited(subclass)
+ super
+ subclasses << subclass
+ end
+
+ def self.eligible?(name)
+ name == :default
+ end
+
# @since 0.5.0
# @api private
attr_writer :application_name
# @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)
- progname = "[#{@application_name}] #{progname}"
- super(severity, time.utc, progname, msg)
+ def call(severity, time, _progname, msg)
+ _format({
+ app: @application_name,
+ severity: severity,
+ time: time.utc
+ }.merge(
+ _message_hash(msg)
+ ))
end
+
+ private
+
+ # @since 0.8.0
+ # @api private
+ def _message_hash(message) # rubocop:disable Metrics/MethodLength
+ case message
+ when Hash
+ message
+ when Exception
+ Hash[
+ message: message.message,
+ backtrace: message.backtrace || [],
+ error: message.class
+ ]
+ else
+ Hash[message: message]
+ end
+ end
+
+ # @since 0.8.0
+ # @api private
+ def _format(hash)
+ hash.map { |k, v| "#{k}=#{v}" }.join(SEPARATOR) + NEW_LINE
+ end
end
+ # Hanami::Logger JSON formatter.
+ # This formatter returns string in JSON format.
+ #
+ # @since 0.5.0
+ # @api private
+ class JSONFormatter < Formatter
+ def self.eligible?(name)
+ name == :json
+ end
+
+ private
+
+ # @since 0.8.0
+ # @api private
+ def _format(hash)
+ Hanami::Utils::Json.dump(hash)
+ end
+ end
+
# Default application name.
# This is used as a fallback for tagging purposes.
#
# @since 0.5.0
# @api private
DEFAULT_APPLICATION_NAME = 'Hanami'.freeze
+ # @since 0.8.0
+ # @api private
+ LEVELS = Hash[
+ 'debug' => DEBUG,
+ 'info' => INFO,
+ 'warn' => WARN,
+ 'error' => ERROR,
+ 'fatal' => FATAL,
+ 'unknown' => UNKNOWN
+ ].freeze
+
# @since 0.5.0
# @api private
attr_writer :application_name
# Initialize a logger
#
# @param application_name [String] an optional application name used for
# tagging purposes
#
+ # @param stream [String, IO, StringIO, Pathname] an optional log stream. This is a filename
+ # (String) or IO object (typically STDOUT, STDERR, or an open file).
+ #
# @since 0.5.0
- def initialize(application_name = nil)
- super(STDOUT)
+ def initialize(application_name = nil, stream: STDOUT, level: DEBUG, formatter: nil)
+ super(stream)
+ @level = _level(level)
+ @stream = stream
@application_name = application_name
- @formatter = Hanami::Logger::Formatter.new.tap { |f| f.application_name = self.application_name }
+ @formatter = Formatter.fabricate(formatter, self.application_name)
end
# Returns the current application name, this is used for tagging purposes
#
# @return [String] the application name
@@ -120,22 +243,47 @@
# @since 0.5.0
def application_name
@application_name || _application_name_from_namespace || _default_application_name
end
+ # @since 0.8.0
+ # @api private
+ def level=(value)
+ super _level(value)
+ end
+
+ # Close the logging stream if this stream isn't an STDOUT
+ #
+ # @since 0.8.0
+ def close
+ super unless [STDOUT, $stdout].include?(@stream)
+ end
+
private
+
# @since 0.5.0
# @api private
def _application_name_from_namespace
class_name = self.class.name
namespace = Utils::String.new(class_name).namespace
- class_name != namespace and return namespace
+ class_name != namespace and return namespace # rubocop:disable Style/AndOr
end
# @since 0.5.0
# @api private
def _default_application_name
DEFAULT_APPLICATION_NAME
+ end
+
+ # @since 0.8.0
+ # @api private
+ def _level(level)
+ case level
+ when DEBUG..UNKNOWN
+ level
+ else
+ LEVELS.fetch(level.to_s.downcase, DEBUG)
+ end
end
end
end