-
Notifications
You must be signed in to change notification settings - Fork 997
/
Copy pathlogging.rb
291 lines (249 loc) · 10 KB
/
logging.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
require 'logging'
require 'fileutils'
module Foreman
class LoggingImpl
private_class_method :new
attr_reader :config, :log_directory
def configure(options = {})
fail 'logging can be configured only once' if @configured
@configured = true
@log_directory = options.fetch(:log_directory, './log')
ensure_log_directory(@log_directory)
load_config(options.fetch(:environment), options.fetch(:config_overrides, {}))
# override via Rails env var
@config[:type] = 'stdout' if ENV['RAILS_LOG_TO_STDOUT']
configure_color_scheme
configure_root_logger(options)
add_console_appender if @config[:console_inline]
# we need to postpone loading of the silenced logger
# to the time the Logging::LEVELS is initialized
require File.expand_path('silenced_logger', __dir__)
end
def add_loggers(loggers = {})
return unless loggers.is_a?(Hash)
loggers.each do |name, config|
add_logger(name, config)
end
end
def add_logger(logger_name, logger_config)
logger = ::Logging.logger[logger_name]
if logger_config.key?(:enabled)
if logger_config[:enabled]
logger.additive = true
logger.level = logger_config[:level] if logger_config.key?(:level)
else
# set high level for disabled logger
logger.level = :fatal
end
end
# TODO: Remove once only Logging 2.0 is supported
if logger.respond_to?(:caller_tracing)
logger.caller_tracing = logger_config[:log_trace] || @config[:log_trace]
else
logger.trace = logger_config[:log_trace] || @config[:log_trace]
end
end
def loggers
::Logging::Repository.instance.children('root').map(&:name)
end
def logger(name)
return Foreman::SilencedLogger.new(::Logging.logger[name]) if ::Logging::Repository.instance.has_logger?(name)
fail "Trying to use logger #{name} which has not been configured."
end
def logger_level(name)
level_int = logger(name).level
::Logging::LEVELS.find { |n, i| i == level_int }.first
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 = {}, &block)
::Logging.ndc.push(fields, &block)
end
# Standard way for logging exceptions to get the most data in the log.
# The behaviour can be influenced by this options:
# * :logger - the name of the logger to put the exception in ('app' by default)
# * :level - the logging level (:warn by default)
def exception(context_message, exception, options = {})
options.assert_valid_keys :level, :logger
logger_name = options[:logger] || 'app'
level = options[:level] || :warn
backtrace_level = options[:backtrace_level] || :info
unless ::Logging::LEVELS.key?(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 || []
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
logger(logger_name).public_send(level) { context_message }
end
# backtrace have its own separate level to prevent flooding logs with backtraces
logger(logger_name).public_send(backtrace_level) do
"Backtrace for '#{context_message}' error (#{exception.class}): #{exception.message}\n" + backtrace.join("\n")
end
end
def blob(message, contents, extra_fields = {})
logger_name = extra_fields[:logger] || 'blob'
with_fields(extra_fields) do
logger(logger_name).info do
message + "\n" + contents
end
end
contents
end
# Helper method useful for investigation when some code portions are slow.
# Logs a message into WARN app logger with log_duration tag for easier
# grepping. Only use for ad-hoc code investigation. For long-term
# performance metrics, use the Telemetry API. On Rails 7+ consider
# using Rails.benchmark instead of this method.
def log_duration(message)
before = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
ensure
after = Process.clock_gettime(Process::CLOCK_MONOTONIC)
duration = ((after - before) * 1000).round(4)
Rails.logger.warn("log_duration: #{message}: #{duration} ms")
end
private
def load_config(environment, overrides = {})
config_file = Rails.root.join('config', 'logging.yaml')
fail "Logging configuration 'config/logging.yaml' not present" unless File.exist?(config_file)
overrides ||= {}
overrides.deep_merge!(overrides[environment.to_sym]) if overrides.has_key?(environment.to_sym)
@config = YAML.load_file(config_file)
@config = @config[:default].deep_merge(@config[environment.to_sym]).deep_merge(overrides)
end
def ensure_log_directory(log_directory)
return true if File.directory?(log_directory)
begin
FileUtils.mkdir_p(log_directory)
rescue Errno::EACCES
warn "Insufficient privileges for #{log_directory}"
end
end
# we also set fallback appender to STDOUT in case a developer asks for unusable appender
def configure_root_logger(options)
::Logging.logger.root.level = @config[:level]
::Logging.logger.root.appenders = build_root_appender(options)
# TODO: Remove once only Logging 2.0 is supported
if ::Logging.logger.root.respond_to?(:caller_tracing)
::Logging.logger.root.caller_tracing = @config[:log_trace]
else
::Logging.logger.root.trace = @config[:log_trace]
end
# fallback to log to STDOUT if there is any @config problem
if ::Logging.logger.root.appenders.empty?
::Logging.logger.root.appenders = ::Logging.appenders.stdout
::Logging.logger.root.warn 'No appender set, logging to STDOUT'
end
end
def build_root_appender(options)
name = "foreman"
options[:facility] = self.class.const_get("::Syslog::Constants::#{options[:facility] || :LOG_LOCAL6}")
case @config[:type]
when 'stdout'
build_console_appender(name, options)
when 'syslog'
build_syslog_appender(name, options)
when 'journal', 'journald'
build_journald_appender(name, options)
when 'file'
build_file_appender(name, options)
else
fail 'unsupported logger type, please choose stdout, file, syslog or journald'
end
end
def build_console_appender(name, options = {})
::Logging.appenders.stdout(name, options.reverse_merge(:layout => build_layout(false)))
end
def add_console_appender
return if @config[:type] == 'stdout'
::Logging.logger.root.add_appenders(build_console_appender("foreman"))
end
def build_syslog_appender(name, options)
::Logging.appenders.syslog(name, options.reverse_merge(:layout => build_layout(false)))
end
def build_journald_appender(name, options)
::Logging.appenders.journald(name, options.reverse_merge(:logger_name => :foreman_logger, :layout => build_layout(false)))
end
def build_file_appender(name, options)
log_filename = File.expand_path(@config[:filename], @log_directory)
File.truncate(log_filename, 0) if @config[:truncate] && File.exist?(log_filename)
begin
::Logging.appenders.file(
name,
options.reverse_merge(
:filename => log_filename,
:layout => build_layout)
)
rescue ArgumentError
warn "Log file #{log_filename} cannot be opened. Falling back to STDOUT"
nil
end
end
def build_layout(enable_colors = true)
pattern, colorize = @config[:pattern], @config[:colorize]
pattern = @config[:sys_pattern] if @config[:type] =~ /^(journald?|syslog)$/i
colorize = nil unless enable_colors
case @config[:layout]
when 'json'
::Logging::Layouts::Parseable.json(:items => @config[:json_items])
when 'pattern'
::Logging::Layouts.pattern(:pattern => pattern, :color_scheme => colorize ? 'bright' : nil)
when 'multiline_pattern'
pattern += " Log trace: %F:%L method: %M\n" if @config[:log_trace]
MultilinePatternLayout.new(:pattern => pattern, :color_scheme => colorize ? 'bright' : nil)
when 'multiline_request_pattern'
pattern += " Log trace: %F:%L method: %M\n" if @config[:log_trace]
MultilineRequestPatternLayout.new(:pattern => pattern, :color_scheme => colorize ? 'bright' : nil)
end
end
def configure_color_scheme
::Logging.color_scheme(
'bright',
:levels => {
:info => :green,
:warn => :yellow,
:error => :red,
:fatal => [:white, :on_red],
},
:date => :green,
:logger => :cyan,
:line => :yellow,
:file => :yellow,
:method => :yellow
)
end
# Custom pattern layout that indents multiline strings and adds | symbol to beginning of each
# following line hence you can see what belongs to the same message
class MultilinePatternLayout < ::Logging::Layouts::Pattern
def format_obj(obj)
obj.is_a?(String) ? indent_lines(obj) : super
end
private
# all new lines will be indented
def indent_lines(string)
string.gsub("\n", "\n | ")
end
end
class MultilineRequestPatternLayout < MultilinePatternLayout
private
def indent_lines(string)
if request_id
string.gsub("\n", "\n #{request_id.split('-').first} | ")
else
super(string)
end
end
def request_id
::Logging.mdc['request']
end
end
end
Logging = LoggingImpl.send :new
end