lib/timber/logger.rb in timberio-1.0.0.beta1 vs lib/timber/logger.rb in timberio-1.0.0
- old
+ new
@@ -1,20 +1,143 @@
require "logger"
module Timber
- # A simple interface to instantiate a logger. It does a couple of things:
- # 1. Simplifies Rails logger instantiation across Rails versions. This
- # helps with simplifying the Readme / install instructions.
- # 2. Serves as a placeholder should we want to extend the logger and add
- # Timber specific functionality.
- module Logger
- def self.new(logger_or_logdev = nil)
- logger = if logger_or_logdev.is_a?(::Logger)
- logger_or_logdev
- else
- Frameworks.logger(logger_or_logdev)
+ # The Timber Logger behaves exactly like `::Logger`, except that it supports a transparent API
+ # for logging structured messages. It ensures your log messages are communicated properly
+ # with the Timber.io API.
+ #
+ # To adhere to our no code debt / no lock-in promise, the Timber Logger will *never* deviate
+ # from the `::Logger` interface. That is, it will *never* add methods, or alter any
+ # method signatures. This ensures Timber can be removed without consequence.
+ #
+ # @example Basic example (the original ::Logger interface remains untouched):
+ # logger.info "Payment rejected for customer #{customer_id}"
+ #
+ # @example Using a map
+ # # The :message, :type, and :data keys are required
+ # logger.info message: "Payment rejected", type: :payment_rejected, data: {customer_id: customer_id, amount: 100}
+ #
+ # @example Using a Struct (a simple, more structured way, to define events)
+ # PaymentRejectedEvent = Struct.new(:customer_id, :amount, :reason) do
+ # def message; "Payment rejected for #{customer_id}"; end
+ # def type; :payment_rejected; end
+ # end
+ # Logger.info PaymentRejectedEvent.new("abcd1234", 100, "Card expired")
+ #
+ # @example Using typed Event classes
+ # # Event implementation is left to you. Events should be simple classes.
+ # # The only requirement is that it responds to #to_timber_event and return the
+ # # appropriate Timber::Events::* type.
+ # class Event
+ # def to_hash
+ # hash = {}
+ # instance_variables.each { |var| hash[var.to_s.delete("@")] = instance_variable_get(var) }
+ # hash
+ # end
+ # alias to_h to_hash
+ #
+ # def to_timber_event
+ # Timber::Events::Custom.new(type: type, message: message, data: to_hash)
+ # end
+ #
+ # def message; raise NotImplementedError.new; end
+ # def type; raise NotImplementedError.new; end
+ # end
+ #
+ # class PaymentRejectedEvent < Event
+ # attr_accessor :customer_id, :amount
+ # def initialize(customer_id, amount)
+ # @customer_id = customer_id
+ # @amount = amount
+ # end
+ # def message; "Payment rejected for customer #{customer_id}"; end
+ # def type; :payment_rejected_event; end
+ # end
+ #
+ # Logger.info PymentRejectedEvent.new("abcd1234", 100)
+ #
+ class Logger < ::Logger
+ # @private
+ class Formatter
+ # Formatters get the formatted level from the logger.
+ SEVERITY_MAP = {
+ "DEBUG" => :debug,
+ "INFO" => :info,
+ "WARN" => :warn,
+ "ERROR" => :error,
+ "FATAL" => :datal,
+ "UNKNOWN" => :unknown
+ }
+
+ private
+ def build_log_entry(severity, time, progname, msg)
+ level = SEVERITY_MAP.fetch(severity)
+ context = CurrentContext.instance.snapshot
+ event = Events.build(msg)
+ if event
+ LogEntry.new(level, time, progname, event.message, context, event)
+ else
+ LogEntry.new(level, time, progname, msg, context, nil)
+ end
+ end
+ end
+
+ # Structures your log messages into JSON.
+ #
+ # logger = Timber::Logger.new(STDOUT)
+ # logger.formatter = Timber::JSONFormatter.new
+ #
+ # Example message:
+ #
+ # {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00","message":"My log message"}
+ #
+ class JSONFormatter < Formatter
+ def call(severity, time, progname, msg)
+ # use << for concatenation for performance reasons
+ build_log_entry(severity, time, progname, msg).to_json() << "\n"
end
- logger.extend(self)
- logger
+ end
+
+ # Structures your log messages into Timber's hybrid format, which makes
+ # it easy to read while also appending the appropriate metadata.
+ #
+ # logger = Timber::Logger.new(STDOUT)
+ # logger.formatter = Timber::JSONFormatter.new
+ #
+ # Example message:
+ #
+ # My log message @timber.io {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
+ #
+ class HybridFormatter < Formatter
+ METADATA_CALLOUT = "@timber.io".freeze
+
+ def call(severity, time, progname, msg)
+ log_entry = build_log_entry(severity, time, progname, msg)
+ metadata = log_entry.to_json(:except => [:message])
+ # use << for concatenation for performance reasons
+ puts msg
+ log_entry.message << " " << METADATA_CALLOUT << " " << metadata << "\n"
+ end
+ end
+
+ # Creates a new Timber::Logger instances. Accepts the same arguments as `::Logger.new`.
+ # The only difference is that it default the formatter to {HybridFormatter}. Using
+ # a different formatter is easy. For example, if you prefer your logs in JSON.
+ #
+ # @example Changing your formatter
+ # logger = Timber::Logger.new(STDOUT)
+ # logger.formatter = Timber::Logger::JSONFormatter.new
+ def initialize(*args)
+ super(*args)
+ self.formatter = HybridFormatter.new
+ end
+
+ # Backwards compatibility with older ActiveSupport::Logger versions
+ Logger::Severity.constants.each do |severity|
+ class_eval(<<-EOT, __FILE__, __LINE__ + 1)
+ def #{severity.downcase}? # def debug?
+ Logger::#{severity} >= level # DEBUG >= level
+ end # end
+ EOT
end
end
end
\ No newline at end of file