# encoding: utf-8 require 'logger' require 'singleton' require 'one_apm/support/hostname' module OneApm module Agent class AgentLogger attr_reader :already_logged def initialize(root = "", override_logger=nil) clear_already_logged create_log(root, override_logger) set_log_level! set_log_format! gather_startup_logs end def fatal(*msgs, &blk) format_and_send(:fatal, msgs, &blk) end def error(*msgs, &blk) format_and_send(:error, msgs, &blk) end def warn(*msgs, &blk) format_and_send(:warn, msgs, &blk) end def info(*msgs, &blk) format_and_send(:info, msgs, &blk) end def debug(*msgs, &blk) format_and_send(:debug, msgs, &blk) end NUM_LOG_ONCE_KEYS = 1000 def log_once(level, key, *msgs) # Since `already_logged` might change between calls, just grab it once # and use it throughout this method. logged = already_logged return if logged.include?(key) if logged.size >= NUM_LOG_ONCE_KEYS && key.kind_of?(String) # The reason for preventing too many keys in `logged` is for # memory concerns. # The reason for checking the type of the key is that we always want # to allow symbols to log, since there are very few of them. # The assumption here is that you would NEVER pass dynamically-created # symbols, because you would never create symbols dynamically in the # first place, as that would already be a memory leak in most Rubies, # even if we didn't hang on to them all here. return end logged[key] = true self.send(level, *msgs) end def is_startup_logger? @log.is_a?(NullLogger) end # Use this when you want to log an exception with explicit control over # the log level that the backtrace is logged at. If you just want the # default behavior of backtraces logged at debug, use one of the methods # above and pass an Exception as one of the args. def log_exception(level, e, backtrace_level=level) @log.send(level, "%p: %s" % [ e.class, e.message ]) @log.send(backtrace_level) do backtrace = backtrace_from_exception(e) if backtrace "Debugging backtrace:\n" + backtrace.join("\n ") else "No backtrace available." end end end def backtrace_from_exception(e) # We've seen that often the backtrace on a SystemStackError is bunk # so massage the caller instead at a known depth. # # Tests keep us honest about minmum method depth our log calls add. return caller.drop(5) if e.is_a?(SystemStackError) e.backtrace end # Allows for passing exceptions in explicitly, which format with backtrace def format_and_send(level, *msgs, &block) if block if @log.send("#{level}?") msgs = Array(block.call) else msgs = [] end end msgs.flatten.each do |item| case item when Exception then log_exception(level, item, :debug) else @log.send(level, item) end end end def create_log(root, override_logger) if !override_logger.nil? @log = override_logger elsif ::OneApm::Agent.config[:agent_enabled] == false create_null_logger else if wants_stdout? @log = ::Logger.new(STDOUT) else create_log_to_file(root) end end end def create_log_to_file(root) path = find_or_create_file_path(::OneApm::Agent.config[:log_file_path], root) if path.nil? @log = ::Logger.new(STDOUT) warn("Error creating log directory #{::OneApm::Agent.config[:log_file_path]}, using standard out for logging.") else file_path = "#{path}/#{::OneApm::Agent.config[:log_file_name]}" begin @log = ::Logger.new(file_path) rescue => e @log = ::Logger.new(STDOUT) warn("Failed creating logger for file #{file_path}, using standard out for logging.", e) end end end def create_null_logger @log = ::OneApm::Agent::NullLogger.new end def clear_already_logged @already_logged = {} end def wants_stdout? ::OneApm::Agent.config[:log_file_path].upcase == "STDOUT" end def find_or_create_file_path(path_setting, root) for abs_path in [ File.expand_path(path_setting), File.expand_path(File.join(root, path_setting)) ] do if File.directory?(abs_path) || (Dir.mkdir(abs_path) rescue nil) return abs_path[%r{^(.*?)/?$}] end end nil end def set_log_level! @log.level = AgentLogger.log_level_for(::OneApm::Agent.config[:log_level]) end LOG_LEVELS = { "debug" => ::Logger::DEBUG, "info" => ::Logger::INFO, "warn" => ::Logger::WARN, "error" => ::Logger::ERROR, "fatal" => ::Logger::FATAL, } def self.log_level_for(level) LOG_LEVELS.fetch(level.to_s.downcase, ::Logger::INFO) end def set_log_format! @hostname = OneApm::Agent::Hostname.get @prefix = wants_stdout? ? '** [OneApm]' : '' @log.formatter = Proc.new do |severity, timestamp, progname, msg| "#{@prefix}[#{timestamp.strftime("%m/%d/%y %H:%M:%S %z")} #{@hostname} (#{$$})] #{severity} : #{msg}\n" end end def gather_startup_logs StartupLogger.instance.dump(self) end def log_formatter=(formatter) @log.formatter = formatter end end # In an effort to not lose messages during startup, we trap them in memory # The real logger will then dump its contents out when it arrives. class StartupLogger < MemoryLogger include Singleton end end end