# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'logger' cs__scoped_require 'ougai' cs__scoped_require 'singleton' cs__scoped_require 'contrast/extensions/ruby_core/module' cs__scoped_require 'contrast/components/interface' module Contrast module Ougai # Our decorator for the Ougai logger allowing for timing and with_level # methods. module Logger # Log the message at the given level. # # @param level [String] the name of the method to use. Should be one of # trace, debug, info, warn, error # @param message [String] the message to log def with_level level, message send(level.to_sym, message) end # Log, at the debug level, the action with a message including the time # it took for the wrapped function to complete. # # @param msgs [Array] the arguments to pass to the logger. # msgs[0] will be modified to include the elapsed time. # @param block [Block, Proc] the block to execute def debug_with_time *msgs, &block log_with_time(:debug, *msgs, &block) end # Log, at the trace level, the action with a message including the time # it took for the wrapped function to complete. # # @param msgs [Array] the arguments to pass to the logger. # msgs[0] will be modified to include the elapsed time. # @param block [Block, Proc] the block to execute def trace_with_time *msgs, &block log_with_time(:trace, *msgs, &block) end private def log_with_time level, *msgs a = Contrast::Utils::Timer.now_ms ret = yield if block_given? z = Contrast::Utils::Timer.now_ms msgs[0] = "#{ msgs[0] }: pid=#{ Process.pid }, elapsed=#{ z - a }ms" send(level, *msgs) ret end end end end module Contrast module Agent # This class functions to serve as a wrapper around our logging, as we need # to be able to dynamically update level based on updates to TeamServer. class Logger include Singleton include Contrast::Components::Interface access_component :config DEFAULT_NAME = 'contrast.log' DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO VALID_LEVELS = ::Ougai::Logging::Severity::SEV_LABEL STDOUT_STR = 'STDOUT' STDERR_STR = 'STDERR' attr_reader :previous_path, :previous_level def initialize update rescue StandardError => e logger.error('Unable to initialize regular logger in LoggerManager.', e) end # Given new settings from TeamServer, update our logging to use the new # file and level, assuming they weren't set by local configuration. # # @param log_file [String] the file to which to log # @param log_level [String] the level at which to log def update log_file = nil, log_level = nil config = CONFIG.root.agent.logger config_path = config.path&.length.to_i.positive? ? config.path : nil config_level = config.level&.length&.positive? ? config.level : nil # config > settings > default path = valid_path(config_path || log_file) level_const = valid_level(config_level || log_level) # don't needlessly recreate logger return if @_logger && (path == previous_path) && (level_const == previous_level) @previous_path = path @previous_level = level_const @_logger = build(path: path, level_const: level_const) logger.debug('Initialized new contrast agent logger') rescue StandardError => e logger.error('Unable to process update to LoggerManager.', e) end def logger @_logger end # StringIO is a valid path because it logs directly to a string buffer def write_permission? path return false if path.nil? return true if path.is_a?(StringIO) return File.writable?(path) if File.exist?(path) dir_name = File.dirname(File.absolute_path(path)) File.writable?(dir_name) end private def build path: STDOUT_STR, level_const: DEFAULT_LEVEL logger = case path when STDOUT_STR, STDERR_STR ::Ougai::Logger.new(Object.cs__const_get(path)) else ::Ougai::Logger.new(path) end logger.extend(Contrast::Ougai::Logger) logger.progname = 'Contrast Agent' logger.level = level_const logger.formatter.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z' logger end def valid_path path path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path return path if path == STDOUT_STR return path if path == STDERR_STR path = DEFAULT_NAME if path.empty? if write_permission?(path) path else if File.directory?(path) logger.error('Unable to create log in directory. Writing to standard out instead', path: path) else logger.error('Unable to write to log file. Writing to standard out instead', path: path) end STDOUT_STR end end def valid_level level level ||= DEFAULT_LEVEL level = level.upcase if VALID_LEVELS.include?(level) Object.cs__const_get("::Ougai::Logging::Severity::#{ level }") else DEFAULT_LEVEL end rescue StandardError DEFAULT_LEVEL end end end end