lib/tty/logger.rb
# frozen_string_literal: true require_relative "logger/config"require_relative "logger/data_filter"require_relative "logger/event"require_relative "logger/formatters/json"require_relative "logger/formatters/text"require_relative "logger/handlers/console"require_relative "logger/handlers/null"require_relative "logger/handlers/stream"require_relative "logger/levels"require_relative "logger/version" module TTY class Logger include Levels # Error raised by this logger class Error < StandardError; end LOG_TYPES = { debug: { level: :debug }, info: { level: :info }, warn: { level: :warn }, error: { level: :error }, fatal: { level: :fatal }, success: { level: :info }, wait: { level: :info } }.freeze # Macro to dynamically define log types # # @api private def self.define_level(name, log_level = nil) const_level = (LOG_TYPES[name.to_sym] || log_level)[:level] loc = caller_locations(0, 1)[0] if loc file, line = loc.path, loc.lineno + 7 else file, line = __FILE__, __LINE__ + 3 end class_eval(<<-EOL, file, line) def #{name}(*msg, &block) log(:#{const_level}, *msg, &block) end EOL end define_level :debug define_level :info define_level :warn define_level :error define_level :fatal define_level :success define_level :wait # Logger configuration instance # # @api public def self.config @config ||= Config.new end # Global logger configuration # # @api public def self.configure yield config end # Instance logger configuration # # @api public def configure yield @config end # Create a logger instance # # @example # logger = TTY::Logger.new(output: $stdout) # # @param [IO] output # the output object, can be stream # # @param [Hash] fields # the data fields for each log message # # @api public def initialize(output: nil, fields: {}) @fields = fields @config = if block_given? conf = Config.new yield(conf) conf else self.class.config end @level = @config.level @handlers = @config.handlers @output = output || @config.output @ready_handlers = [] @data_filter = DataFilter.new(@config.filters.data, mask: @config.filters.mask) @types = LOG_TYPES.dup @config.types.each do |name, log_level| add_type(name, log_level) end @handlers.each do |handler| add_handler(handler) end end # Add new log type # # @example # add_type(:thanks, {level: :info}) # # @api private def add_type(name, log_level) if @types.include?(name) raise Error, "Already defined log type #{name.inspect}" end @types[name.to_sym] = log_level self.class.define_level(name, log_level) end # Add handler for logging messages # # @example # add_handler(:console) # # @example # add_handler(:console, styles: { info: { color: :yellow } }) # # @example # add_handler([:console, message_format: "%-10s"]) # # @param [Symbol, Handlers] handler # the handler name or class # # @api public def add_handler(handler, **options) h, h_opts = *(handler.is_a?(Array) ? handler : [handler, options]) handler_type = coerce_handler(h) global_opts = { output: @output, config: @config } opts = global_opts.merge(h_opts) ready_handler = handler_type.new(**opts) @ready_handlers << ready_handler end # Remove log events handler # # @example # remove_handler(:console) # # @api public def remove_handler(handler) handler_type = coerce_handler(handler) @ready_handlers.delete_if { |_handler| _handler.is_a?(handler_type) } end # Coerce handler name into object # # @example # coerce_handler(:console) # # => TTY::Logger::Handlers::Console # # @raise [Error] when class cannot be coerced # # @return [Class] # # @api private def coerce_handler(name) case name when String, Symbol Handlers.const_get(name.capitalize) when Class name else raise_handler_error end rescue NameError raise_handler_error end # Raise error when unknown handler name # # @api private def raise_handler_error raise Error, "Handler needs to be a class name or a symbol name" end # Copy this logger # # @example # logger = TTY::Logger.new # child_logger = logger.copy(app: "myenv", env: "prod") # child_logger.info("Deploying") # # @return [TTY::Logger] # a new copy of this logger # # @api public def copy(new_fields) new_config = @config.to_proc.call(Config.new) if block_given? yield(new_config) end self.class.new(fields: @fields.merge(new_fields), output: @output, &new_config) end # Check current level against another # # @return [Symbol] # # @api public def log?(level, other_level) compare_levels(level, other_level) != :gt end # Logs streaming output. # # @example # logger << "Example output" # # @api public def write(*msg) event = Event.new(filter(*msg)) @ready_handlers.each do |handler| handler.(event) end self end alias << write # Log a message given the severtiy level # # @example # logger.log(:info, "Deployed successfully") # # @example # logger.log(:info) { "Deployed successfully" } # # @api publicMethod `log` has a Cognitive Complexity of 16 (exceeds 5 allowed). Consider refactoring.
Method `log` has 28 lines of code (exceeds 25 allowed). Consider refactoring. def log(current_level, *msg) scoped_fields = msg.last.is_a?(::Hash) ? msg.pop : {} fields_copy = scoped_fields.dup if msg.empty? && block_given? msg = [] Array[yield].flatten(1).each do |el| el.is_a?(::Hash) ? fields_copy.merge!(el) : msg << el end end top_caller = caller_locations(1, 1)[0] loc = caller_locations(2, 1)[0] || top_caller label = top_caller.label metadata = { level: current_level, time: Time.now, pid: Process.pid, name: @types.include?(label.to_sym) ? label : current_level, path: loc.path, lineno: loc.lineno, method: loc.base_label } event = Event.new(filter(*msg), @data_filter.filter(@fields.merge(fields_copy)), metadata) @ready_handlers.each do |handler| level = handler.respond_to?(:level) ? handler.level : @config.level handler.(event) if log?(level, current_level) end self end # Change current log level for the duration of the block # # @example # logger.log_at :debug do # logger.debug("logged") # end # # @param [String] tmp_level # the temporary log level # # @api public def log_at(tmp_level, &block) @ready_handlers.each do |handler| handler.log_at(tmp_level, &block) end end # Filter message parts for any sensitive information and # replace with placeholder. # # @param [Array[Object]] objects # the messages to filter # # @return [Array[String]] # the filtered message # # @api private def filter(*objects) objects.map do |obj| case obj when Exception backtrace = Array(obj.backtrace).map { |line| swap_filtered(line) } copy_error(obj, swap_filtered(obj.message), backtrace) else swap_filtered(obj.to_s) end end end # Create a new error instance copy # # @param [Exception] error # @param [String] message # @param [Array,nil] backtrace # # @return [Exception] # # @api private def copy_error(error, message, backtrace = nil) new_error = error.exception(message) new_error.set_backtrace(backtrace) new_error end # Swap string content for filtered content # # @param [String] obj # # @api private def swap_filtered(obj) obj.dup.tap do |obj_copy| @config.filters.message.each do |text| obj_copy.gsub!(text, @config.filters.mask) end end end end # Loggerend # TTY