# encoding: ascii-8bit # Copyright 2022 Ball Aerospace & Technologies Corp. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it # under the terms of the GNU Affero General Public License # as published by the Free Software Foundation; version 3 with # attribution addendums as found in the LICENSE.txt # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # This program may also be used under the terms of a commercial or # enterprise edition license of COSMOS if purchased from the # copyright holder require 'cosmos/core_ext/class' require 'cosmos/core_ext/time' require 'cosmos/topics/topic' require 'socket' require 'logger' require 'time' require 'json' module Cosmos # Supports different levels of logging and only writes if the level # is exceeded. class Logger # @return [Boolean] Whether to output the message to stdout instance_attr_accessor :stdout # @return [Integer] The logging level instance_attr_accessor :level # @return [String] Additional detail to add to messages instance_attr_accessor :detail_string # @return [String] Fluent tag instance_attr_accessor :tag # @return [String] Microservice name instance_attr_accessor :microservice_name # @return [String] Scope instance_attr_accessor :scope @@mutex = Mutex.new @@instance = nil # DEBUG only prints DEBUG messages DEBUG = ::Logger::DEBUG # INFO prints INFO, DEBUG messages INFO = ::Logger::INFO # WARN prints WARN, INFO, DEBUG messages WARN = ::Logger::WARN # ERROR prints ERROR, WARN, INFO, DEBUG messages ERROR = ::Logger::ERROR # FATAL prints FATAL, ERROR, WARN, INFO, DEBUG messages FATAL = ::Logger::FATAL DEBUG_SEVERITY_STRING = 'DEBUG' INFO_SEVERITY_STRING = 'INFO' WARN_SEVERITY_STRING = 'WARN' ERROR_SEVERITY_STRING = 'ERROR' FATAL_SEVERITY_STRING = 'FATAL' # @param level [Integer] The initial logging level def initialize(level = Logger::INFO) @stdout = true @level = level @scope = nil @detail_string = nil @container_name = Socket.gethostname @microservice_name = nil @tag = @container_name + ".log" @mutex = Mutex.new @no_store = ENV['COSMOS_NO_STORE'] end # @param message [String] The message to print if the log level is at or # below the method name log level. # @param block [Proc] Block to call which should return a string to append # to the log message def debug(message = nil, scope: @scope, user: nil, &block) log_message(DEBUG_SEVERITY_STRING, message, scope: scope, user: user, &block) if @level <= DEBUG end # (see #debug) def info(message = nil, scope: @scope, user: nil, &block) log_message(INFO_SEVERITY_STRING, message, scope: scope, user: user, &block) if @level <= INFO end # (see #debug) def warn(message = nil, scope: @scope, user: nil, &block) log_message(WARN_SEVERITY_STRING, message, scope: scope, user: user, &block) if @level <= WARN end # (see #debug) def error(message = nil, scope: @scope, user: nil, &block) log_message(ERROR_SEVERITY_STRING, message, scope: scope, user: user, &block) if @level <= ERROR end # (see #debug) def fatal(message = nil, scope: @scope, user: nil, &block) log_message(FATAL_SEVERITY_STRING, message, scope: scope, user: user, &block) if @level <= FATAL end # (see #debug) def self.debug(message = nil, scope: nil, user: nil, &block) args = {} args[:scope] = scope if scope args[:user] = user if user self.instance.debug(message, **args, &block) end # (see #debug) def self.info(message = nil, scope: nil, user: nil, &block) args = {} args[:scope] = scope if scope args[:user] = user if user self.instance.info(message, **args, &block) end # (see #debug) def self.warn(message = nil, scope: nil, user: nil, &block) args = {} args[:scope] = scope if scope args[:user] = user if user self.instance.warn(message, **args, &block) end # (see #debug) def self.error(message = nil, scope: nil, user: nil, &block) args = {} args[:scope] = scope if scope args[:user] = user if user self.instance.error(message, **args, &block) end # (see #debug) def self.fatal(message = nil, scope: nil, user: nil, &block) args = {} args[:scope] = scope if scope args[:user] = user if user self.instance.fatal(message, **args, &block) end # @return [Logger] The logger instance def self.instance return @@instance if @@instance @@mutex.synchronize do @@instance ||= self.new end @@instance end protected def log_message(severity_string, message, scope:, user:) @mutex.synchronize do data = { time: Time.now.to_nsec_from_epoch, '@timestamp' => Time.now.xmlschema(3), severity: severity_string } data[:microservice_name] = @microservice_name if @microservice_name data[:detail] = @detail_string if @detail_string data[:user] = user['name'] || 'Unknown' if user # EE: If a user is passed, put its name ('Unknown' if it doesn't have a name). Don't include user data if no user was passed if block_given? message = yield end data[:container_name] = @container_name data[:log] = message puts data.to_json if @stdout unless @no_store if scope Topic.write_topic("#{scope}__cosmos_log_messages", data) else # The base cosmos_log_messages doesn't have an associated logger # so it must be limited to prevent unbounded stream growth Topic.write_topic("cosmos_log_messages", data, '*', 1000) end end end end end end