Project

General

Profile

« Previous | Next » 

Revision 9d460533

Added by Lukas Zapletal over 5 years ago

Fixes #25100 - Ruby logging stack replaced with logging gem

View differences:

bundler.d/journald.rb
# disable to avoid journald native gem in development setup
group :journald do
gem 'logging-journald', '~> 2.0', :platforms => [:ruby]
end
config/settings.yml.example
# Uncomment and modify if you want to change the log level
# WARN, DEBUG, ERROR, FATAL, INFO, UNKNOWN
#:log_level: INFO
# The maximum size of a log file before it's rolled (in MiB)
#:file_rolling_size: 100
# The maximum age of a log file before it's rolled (in seconds). Also accepts 'daily', 'weekly', or 'monthly'.
#:file_rolling_age: weekly
# Number of log files to keep
#:file_rolling_keep: 6
# Logging pattern for file-based loging
#:file_logging_pattern: '%d %.8X{request} [%.1l] %m'
# Logging pattern for syslog or journal loging
#:system_logging_pattern: '%.8X{request} [%.1l] %m'
# Log buffer size and extra buffer size (for errors). Defaults to 3000 messages in total,
# which is about 500 kB request.
lib/launcher.rb
require 'proxy/sd_notify'
require 'proxy/settings'
require 'proxy/signal_handler'
require 'proxy/log_buffer/trace_decorator'
require 'thread'
module Proxy
......
:server => :webrick,
:DoNotListen => true,
:Port => http_port, # only being used to correctly log http port being used
:Logger => ::Proxy::LogBuffer::Decorator.instance,
:Logger => ::Proxy::LogBuffer::TraceDecorator.instance,
:AccessLog => [],
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
:daemonize => false
}
lib/proxy/log.rb
require 'logger'
require 'logging'
require 'proxy/log_buffer/decorator'
# ::Rack::CommonLogger expects loggers to implement 'write' method
Logger.class_eval { alias_method :write, :info }
module Proxy
module Log
def logger
......
end
class LoggerFactory
class LogFormatter < ::Logger::Formatter
Format = "%s, [%s%s] %5s -- %s: %s\n".freeze
def call(severity, time, progname, msg)
Format % [severity[0..0], format_datetime(time), request_id[0..7], severity, progname, msg2str(msg)]
end
def request_id
Thread.current.thread_variable_get(:request_id).to_s
end
end
BASE_LOG_SIZE = 1024 * 1024 # 1 MiB
begin
require 'syslog/logger'
::Syslog::Logger.class_eval { alias_method :write, :info }
class SyslogFormatter < Syslog::Logger::Formatter
Format = "<%s> %s\n".freeze
def call(severity, time, progname, msg)
request_id.empty? ? clean(msg) : Format % [request_id[0..7], clean(msg)]
end
def request_id
Thread.current.thread_variable_get(:request_id).to_s
end
end
@syslog_available = true
rescue LoadError
# ignore, syslog isn't available on this platform
@syslog_available = false
end
def self.logger
logger_name = 'foreman-proxy'
layout = Logging::Layouts.pattern(pattern: ::Proxy::SETTINGS.file_logging_pattern + "\n")
notime_layout = Logging::Layouts.pattern(pattern: ::Proxy::SETTINGS.system_logging_pattern + "\n")
logger = Logging.logger.root
if log_file.casecmp('STDOUT').zero?
if SETTINGS.daemon
puts "Settings log_file=STDOUT and daemon=true are incompatible, exiting..."
puts "Settings log_file=STDOUT and daemon=true cannot be used together"
exit(1)
end
logger = stdout_logger
logger.add_appenders(Logging.appenders.stdout(logger_name, layout: layout))
elsif log_file.casecmp('SYSLOG').zero?
unless syslog_available?
puts "'SYSLOG' logger is not supported on this platform, please use STDOUT or a file-based logger. Exiting..."
puts "Syslog is not supported on this platform, use STDOUT or a file"
exit(1)
end
logger = syslog_logger
logger.add_appenders(Logging.appenders.syslog(
logger_name, layout: notime_layout, facility: ::Syslog::Constants::LOG_LOCAL5))
elsif log_file.casecmp('JOURNAL').zero? || log_file.casecmp('JOURNALD').zero?
begin
logger.add_appenders(Logging.appenders.journald(
logger_name, logger_name: :proxy_logger, layout: notime_layout, facility: ::Syslog::Constants::LOG_LOCAL5))
rescue NoMethodError
logger.add_appenders(Logging.appenders.stdout(logger_name, layout: layout))
logger.warn "Journald is not available on this platform. Falling back to STDOUT."
end
else
logger = default_logger(log_file)
begin
keep = ::Proxy::SETTINGS.file_rolling_keep
size = BASE_LOG_SIZE * ::Proxy::SETTINGS.file_rolling_size
age = ::Proxy::SETTINGS.file_rolling_age
logger.add_appenders(Logging.appenders.rolling_file(
logger_name, layout: layout, filename: log_file, keep: keep, size: size, age: age, roll_by: 'date'))
rescue ArgumentError => ae
logger.add_appenders(Logging.appenders.stdout(logger_name, layout: layout))
logger.warn "Log file #{log_file} cannot be opened. Falling back to STDOUT: #{ae}"
end
end
logger.level = ::Logger.const_get(::Proxy::SETTINGS.log_level.upcase)
logger
end
def self.default_logger(log_file)
# We keep the last 6 10MB log files
logger = ::Logger.new(log_file, 6, 1024*1024*10)
logger.formatter = LogFormatter.new
logger
rescue Exception => e
puts "Unable to configure file-based logger: #{e.message}. Exiting..."
exit(1)
end
def self.stdout_logger
logger = ::Logger.new(STDOUT)
logger.formatter = LogFormatter.new
logger
end
def self.syslog_logger
logger = ::Syslog::Logger.new 'foreman-proxy'
logger.formatter = SyslogFormatter.new
logger.level = ::Logging.level_num(::Proxy::SETTINGS.log_level)
logger
end
......
end
def self.log_file
::Proxy::SETTINGS.log_file
@log_file ||= ::Proxy::SETTINGS.log_file
end
end
......
end
def call(env)
status = 500
env['rack.logger'] = logger
@app.call(env)
logger.info { "Started #{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['QUERY_STRING']}" }
logger.trace { 'Headers: ' + env.select {|k,v| k.start_with? 'HTTP_'}.inspect }
logger.trace { (body = env['rack.input'].read).empty? ? '' : 'Body: ' + body }
before = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, _, _ = @app.call(env)
rescue Exception => e
logger.exception "Error processing request '#{::Logging.mdc['request']}", e
raise e
ensure
logger.info do
after = Process.clock_gettime(Process::CLOCK_MONOTONIC)
duration = (after - before) * 1000
"Finished #{env["REQUEST_METHOD"]} #{env["PATH_INFO"]} with #{status} (#{duration.round(2)} ms)"
end
end
end
end
lib/proxy/log_buffer/decorator.rb
message = progname
end
end
return if message == ''
# add to the logger first
@logger.add(severity, message)
@logger.add(::Logger::Severity::DEBUG, backtrace) if backtrace
......
end
end
def debug(msg_or_progname, exception_or_backtrace = nil, &block)
def trace?
@trace ||= !!ENV['FOREMAN_PROXY_TRACE']
end
def trace(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::DEBUG, nil, msg_or_progname, exception_or_backtrace, &block) if trace?
end
def debug(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::DEBUG, nil, msg_or_progname, exception_or_backtrace, &block)
end
def info(msg_or_progname, exception_or_backtrace = nil, &block)
def info(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::INFO, nil, msg_or_progname, exception_or_backtrace, &block)
end
alias_method :write, :info
def warn(msg_or_progname, exception_or_backtrace = nil, &block)
def warn(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::WARN, nil, msg_or_progname, exception_or_backtrace, &block)
end
alias_method :warning, :warn
def error(msg_or_progname, exception_or_backtrace = nil, &block)
def error(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::ERROR, nil, msg_or_progname, exception_or_backtrace, &block)
end
def fatal(msg_or_progname, exception_or_backtrace = nil, &block)
def fatal(msg_or_progname = nil, exception_or_backtrace = nil, &block)
add(::Logger::Severity::FATAL, nil, msg_or_progname, exception_or_backtrace, &block)
end
def request_id
(r = Thread.current.thread_variable_get(:request_id)).nil? ? r : r.to_s[0..7]
(r = ::Logging.mdc['request']).nil? ? r : r.to_s[0..7]
end
# Structured fields to log in addition to log messages. Every log line created within given block is enriched with these fields.
# Fields appear in joruand and/or JSON output (hash named 'ndc').
def with_fields(fields = {})
::Logging.ndc.push(fields) do
yield
end
end
# Standard way for logging exceptions to get the most data in the log. By default
# it logs via warn level, this can be changed via options[:level]
def exception(context_message, exception, options = {})
level = options[:level] || :warn
unless ::Logging::LEVELS.keys.include?(level.to_s)
raise "Unexpected log level #{level}, expected one of #{::Logging::LEVELS.keys}"
end
# send class, message and stack as structured fields in addition to message string
backtrace = exception.backtrace ? exception.backtrace : []
extra_fields = {
exception_class: exception.class.name,
exception_message: exception.message,
exception_backtrace: backtrace
}
extra_fields[:foreman_code] = exception.code if exception.respond_to?(:code)
with_fields(extra_fields) do
public_send(level) do
([context_message, "#{exception.class}: #{exception.message}"] + backtrace).join("\n")
end
end
end
def method_missing(symbol, *args);
lib/proxy/log_buffer/trace_decorator.rb
require 'proxy/log_buffer/buffer'
require 'thread'
module Proxy::LogBuffer
class TraceDecorator
def self.instance
@@instance ||= new(::Proxy::LogBuffer::Decorator.instance)
end
def initialize(logger)
@logger = logger
end
def debug(msg_or_progname = nil, exception_or_backtrace = nil, &block)
@logger.trace(msg_or_progname, exception_or_backtrace, &block)
end
def info(msg_or_progname = nil, exception_or_backtrace = nil, &block)
@logger.trace(msg_or_progname, exception_or_backtrace, &block)
end
def warn(msg_or_progname = nil, exception_or_backtrace = nil, &block)
@logger.trace(msg_or_progname, exception_or_backtrace, &block)
end
def error(msg_or_progname = nil, exception_or_backtrace = nil, &block)
@logger.trace(msg_or_progname, exception_or_backtrace, &block)
end
def fatal(msg_or_progname = nil, exception_or_backtrace = nil, &block)
@logger.trace(msg_or_progname, exception_or_backtrace, &block)
end
def method_missing(symbol, *args);
@logger.send(symbol, *args)
end
end
end
lib/proxy/request_id_middleware.rb
end
def call(env)
Thread.current.thread_variable_set(:request_id, env['HTTP_X_REQUEST_ID']) if env.has_key?('HTTP_X_REQUEST_ID')
::Logging.mdc['remote_ip'] = env['REMOTE_ADDR']
if env.has_key?('HTTP_X_REQUEST_ID')
::Logging.mdc['request'] = env['HTTP_X_REQUEST_ID']
else
::Logging.mdc['request'] = SecureRandom.uuid
end
status, header, body = @app.call(env)
[status, header, ::Rack::BodyProxy.new(body) { Thread.current.thread_variable_set(:request_id, nil) }]
[status, header, ::Rack::BodyProxy.new(body) { ::Logging.mdc.clear }]
end
end
end
lib/proxy/settings/global.rb
:settings_directory => Pathname.new(__FILE__).join("..","..","..","..","config","settings.d").expand_path.to_s,
:https_port => 8443,
:log_file => "/var/log/foreman-proxy/proxy.log",
:file_rolling_keep => 6,
:file_rolling_size => 100,
:file_rolling_age => 'weekly',
:file_logging_pattern => '%d %.8X{request} [%.1l] %m',
:system_logging_pattern => '%.8X{request} [%.1l] %m',
:log_level => "INFO",
:daemon => false,
:daemon_pid => "/var/run/foreman-proxy/foreman-proxy.pid",
lib/smart_proxy_main.rb
::Sinatra::Base.set :run, false
::Sinatra::Base.set :root, APP_ROOT
::Sinatra::Base.set :logging, false # we are not going to use Rack::Logger
::Sinatra::Base.set :logging, false
::Sinatra::Base.use ::Proxy::RequestIdMiddleware
::Sinatra::Base.use ::Proxy::LoggerMiddleware # instead, we have our own logging middleware
::Sinatra::Base.use ::Rack::CommonLogger, ::Proxy::LogBuffer::Decorator.instance
::Sinatra::Base.use ::Proxy::LoggerMiddleware
::Sinatra::Base.set :env, :production
::Sinatra::Base.register ::Sinatra::Authorization
modules/dhcp_common/free_ips.rb
require 'concurrent'
require 'logger'
require 'set'
require 'dhcp_common/pingable'
smart_proxy.gemspec
s.add_dependency 'json'
s.add_dependency 'rack', '>= 1.1'
s.add_dependency 'sinatra'
s.add_dependency 'logging'
s.description = <<EOF
Foreman Proxy is used via The Foreman Project, it allows Foreman to manage
Remote DHCP, DNS, TFTP and Puppet servers via a REST API
test/bmc/bmc_api_test.rb
assert_equal(expected, data["available_resources"])
end
def test_api_uses_default_smart_proxy_logger
Proxy::BMC::IPMI.logger = nil
Proxy::BMC::Plugin.load_test_settings(:provider_log_level => nil)
Proxy::BMC::IPMI.any_instance.stubs(:poweron?).returns(true)
get "/#{host}/chassis/power/on", args
assert_not_equal 'Rubyipmi', Proxy::BMC::IPMI.logger.progname
def test_api_can_use_default_smart_proxy_logger
Proxy::BMC::IPMI.logger = Proxy::LogBuffer::Decorator.instance
Proxy::BMC::IPMI.logger.error "TEST ERROR"
found = false
Proxy::LogBuffer::Buffer.instance.iterate_descending{|x| found = true if x.message == "TEST ERROR" }
assert found
end
def test_api_can_put_power_action
test/bmc/bmc_test.rb
require 'test_helper'
require 'bmc/ipmi'
require 'logger'
class BmcTest < Test::Unit::TestCase
test/log_buffer/decorator_test.rb
def test_should_keep_request_id_in_buffer_when_available
request_id = '12345678'
Thread.current.thread_variable_set(:request_id, request_id)
::Logging.mdc['request'] = request_id
@decorator.error('error message')
assert_false @buffer.to_a.empty?
assert_equal request_id, @buffer.to_a.first.request_id
ensure
Thread.current.thread_variable_set(:request_id, nil)
::Logging.mdc['request'] = nil
end
def test_should_not_roll_log_if_stdout_is_used
test/log_buffer/logger_factory_test.rb
require 'test_helper'
require 'tempfile'
require 'proxy/log'
class LoggerFactoryTest < Test::Unit::TestCase
def setup
@factory = ::Proxy::LoggerFactory
end
def test_should_configure_logger_when_stdout_is_used
@factory.stubs(:log_file).returns('STDOUT')
logger = @factory.logger
assert logger.is_a?(::Logger)
assert logger.formatter.is_a?(::Proxy::LoggerFactory::LogFormatter)
end
def test_should_configure_logger_by_default
tmp_logfile = Tempfile.new('logfactory-test').path
@factory.stubs(:log_file).returns(tmp_logfile)
logger = @factory.logger
assert logger.is_a?(::Logger)
assert logger.formatter.is_a?(::Proxy::LoggerFactory::LogFormatter)
end
def test_should_configure_syslog
@factory.stubs(:log_file).returns('SYSLOG')
@factory.expects(:syslog_available?).returns(true)
logger = @factory.logger
assert logger.is_a?(::Syslog::Logger)
assert logger.formatter.is_a?(::Proxy::LoggerFactory::SyslogFormatter)
end
end

Also available in: Unified diff