# frozen_string_literal: true require 'logging' module Bolt module Logger LEVELS = %w[trace debug info warn error fatal].freeze # This module is treated as a global singleton so that multiple classes # in Bolt can log warnings with IDs. Access to the following variables # are controlled by a mutex. @mutex = Mutex.new @warnings = Set.new @disable_warnings = Set.new @message_queue = [] # This method provides a single point-of-entry to setup logging for both # the CLI and for tests. This is necessary because we define custom log # levels which create corresponding methods on the logger instances; # without first initializing the Logging system, calls to those methods # will fail. def self.initialize_logging # Initialization isn't idempotent and will result in warnings about const # redefs, so skip it if the log levels we expect are present. If it's # already been initialized with an insufficient set of levels, go ahead # and call init anyway or we'll have failures when calling log methods # for missing levels. unless levels & LEVELS == LEVELS Logging.init(*LEVELS) end # As above, only create the color scheme if we haven't already created it. unless Logging.color_scheme('bolt') Logging.color_scheme( 'bolt', lines: { warn: :yellow, error: :red, fatal: %i[white on_red] } ) end end def self.configure(destinations, color, disable_warnings = nil) root_logger = Bolt::Logger.logger(:root) root_logger.add_appenders Logging.appenders.stderr( 'console', layout: console_layout(color), level: default_console_level ) # We set the root logger's level so that it logs everything but we do # limit what's actually logged in every appender individually. root_logger.level = :all destinations.each_pair do |name, params| appender = Logging.appenders[name] if appender.nil? unless name.start_with?('file:') raise Bolt::Error.new("Unexpected log: #{name}", 'bolt/internal-error') end begin appender = Logging.appenders.file( name, filename: name[5..-1], # strip the "file:" prefix truncate: (params[:append] == false), layout: default_layout, level: default_file_level ) rescue ArgumentError => e raise Bolt::Error.new("Failed to open log #{name}: #{e.message}", 'bolt/log-error') end root_logger.add_appenders appender end appender.level = params[:level] if params[:level] end # Set the list of disabled warnings and mark the logger as configured. # Log all messages in the message queue and flush the queue. if disable_warnings @mutex.synchronize { @disable_warnings = disable_warnings } end end def self.configured? Logging.logger[:root].appenders.any? end def self.stream @stream end def self.stream=(stream) @stream = stream end # A helper to ensure the Logging library is always initialized with our # custom log levels before retrieving a Logger instance. def self.logger(name) initialize_logging Logging.logger[name] end def self.analytics=(analytics) @analytics = analytics end def self.console_layout(color) color_scheme = :bolt if color Logging.layouts.pattern( pattern: '%m\e[0m\n', color_scheme: color_scheme ) end def self.default_layout Logging.layouts.pattern( pattern: '%d %-6l [%T] [%c] %m\n', date_pattern: '%Y-%m-%dT%H:%M:%S.%6N' ) end def self.default_console_level :warn end def self.default_file_level :warn end # Explicitly check the log level names instead of the log level number, as levels # that are stringified integers (e.g. "level" => "42") will return a truthy value def self.valid_level?(level) Logging::LEVELS.include?(Logging.levelify(level)) end def self.levels Logging::LNAMES.map(&:downcase) end def self.reset_logging Logging.reset end # The following methods are used in place of the Logging.logger # methods of the same name when logging warning messages or logging # any messages prior to the logger being configured. If the logger # is not configured when any of these methods are called, the message # will be added to a queue, otherwise they are logged immediately. # The message queue is flushed by calling #flush_queue, which is # called from Bolt::CLI after configuring the logger. # def self.warn(id, msg) log(type: :warn, msg: "#{msg} [ID: #{id}]", id: id) end def self.warn_once(id, msg) log(type: :warn_once, msg: "#{msg} [ID: #{id}]", id: id) end def self.deprecate(id, msg) log(type: :deprecate, msg: "#{msg} [ID: #{id}]", id: id) end def self.deprecate_once(id, msg) log(type: :deprecate_once, msg: "#{msg} [ID: #{id}]", id: id) end def self.debug(msg) log(type: :debug, msg: msg) end def self.info(msg) log(type: :info, msg: msg) end # Logs a message. If the logger has not been configured, this will queue # the message to be logged later. Once the logger is configured, the # queue will be flushed of all messages and new messages will be logged # immediately. # # Logging with this method is controlled by a mutex, as the Bolt::Logger # module is treated as a global singleton to allow multiple classes # access to its methods. # private_class_method def self.log(type:, msg:, id: nil) @mutex.synchronize do if configured? log_message(type: type, msg: msg, id: id) else @message_queue << { type: type, msg: msg, id: id } end end end # Logs all messages in the message queue and then flushes the queue. # def self.flush_queue @mutex.synchronize do @message_queue.each do |message| log_message(message) end @message_queue.clear end end # Handles the actual logging of a message. # private_class_method def self.log_message(type:, msg:, id: nil) case type when :warn do_warn(msg, id) when :warn_once do_warn_once(msg, id) when :deprecate do_deprecate(msg, id) when :deprecate_once do_deprecate_once(msg, id) else logger(self).send(type, msg) end end # The following methods do the actual warning. # private_class_method def self.do_warn(msg, id) return if @disable_warnings.include?(id) logger(self).warn(msg) end private_class_method def self.do_warn_once(msg, id) return unless @warnings.add?(id) do_warn(msg, id) end private_class_method def self.do_deprecate(msg, id) @analytics&.event('Warn', 'deprecation', label: id) do_warn(msg, id) end private_class_method def self.do_deprecate_once(msg, id) @analytics&.event('Warn', 'deprecation', label: id) do_warn_once(msg, id) end end end