tty-logger 0.0.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -1
- data/README.md +769 -4
- data/lib/tty/logger.rb +332 -0
- data/lib/tty/logger/config.rb +128 -0
- data/lib/tty/logger/data_filter.rb +118 -0
- data/lib/tty/logger/event.rb +40 -0
- data/lib/tty/logger/formatters/json.rb +63 -0
- data/lib/tty/logger/formatters/text.rb +130 -0
- data/lib/tty/logger/handlers/base.rb +73 -0
- data/lib/tty/logger/handlers/console.rb +178 -0
- data/lib/tty/logger/handlers/null.rb +16 -0
- data/lib/tty/logger/handlers/stream.rb +63 -0
- data/lib/tty/logger/levels.rb +68 -0
- data/lib/tty/logger/version.rb +3 -3
- metadata +28 -22
- data/Rakefile +0 -8
- data/spec/spec_helper.rb +0 -14
- data/spec/unit/log_spec.rb +0 -7
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-logger.gemspec +0 -33
data/lib/tty/logger.rb
CHANGED
@@ -1,10 +1,342 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "logger/config"
|
4
|
+
require_relative "logger/data_filter"
|
5
|
+
require_relative "logger/event"
|
6
|
+
require_relative "logger/formatters/json"
|
7
|
+
require_relative "logger/formatters/text"
|
8
|
+
require_relative "logger/handlers/console"
|
9
|
+
require_relative "logger/handlers/null"
|
10
|
+
require_relative "logger/handlers/stream"
|
11
|
+
require_relative "logger/levels"
|
3
12
|
require_relative "logger/version"
|
4
13
|
|
5
14
|
module TTY
|
6
15
|
class Logger
|
16
|
+
include Levels
|
17
|
+
|
18
|
+
# Error raised by this logger
|
7
19
|
class Error < StandardError; end
|
8
20
|
|
21
|
+
LOG_TYPES = {
|
22
|
+
debug: { level: :debug },
|
23
|
+
info: { level: :info },
|
24
|
+
warn: { level: :warn },
|
25
|
+
error: { level: :error },
|
26
|
+
fatal: { level: :fatal },
|
27
|
+
success: { level: :info },
|
28
|
+
wait: { level: :info }
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
# Macro to dynamically define log types
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
def self.define_level(name, log_level = nil)
|
35
|
+
const_level = (LOG_TYPES[name.to_sym] || log_level)[:level]
|
36
|
+
|
37
|
+
loc = caller_locations(0, 1)[0]
|
38
|
+
if loc
|
39
|
+
file, line = loc.path, loc.lineno + 7
|
40
|
+
else
|
41
|
+
file, line = __FILE__, __LINE__ + 3
|
42
|
+
end
|
43
|
+
class_eval(<<-EOL, file, line)
|
44
|
+
def #{name}(*msg, &block)
|
45
|
+
log(:#{const_level}, *msg, &block)
|
46
|
+
end
|
47
|
+
EOL
|
48
|
+
end
|
49
|
+
|
50
|
+
define_level :debug
|
51
|
+
define_level :info
|
52
|
+
define_level :warn
|
53
|
+
define_level :error
|
54
|
+
define_level :fatal
|
55
|
+
define_level :success
|
56
|
+
define_level :wait
|
57
|
+
|
58
|
+
# Logger configuration instance
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def self.config
|
62
|
+
@config ||= Config.new
|
63
|
+
end
|
64
|
+
|
65
|
+
# Global logger configuration
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def self.configure
|
69
|
+
yield config
|
70
|
+
end
|
71
|
+
|
72
|
+
# Instance logger configuration
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def configure
|
76
|
+
yield @config
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a logger instance
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# logger = TTY::Logger.new(output: $stdout)
|
83
|
+
#
|
84
|
+
# @param [IO] output
|
85
|
+
# the output object, can be stream
|
86
|
+
#
|
87
|
+
# @param [Hash] fields
|
88
|
+
# the data fields for each log message
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def initialize(output: nil, fields: {})
|
92
|
+
@fields = fields
|
93
|
+
@config = if block_given?
|
94
|
+
conf = Config.new
|
95
|
+
yield(conf)
|
96
|
+
conf
|
97
|
+
else
|
98
|
+
self.class.config
|
99
|
+
end
|
100
|
+
@level = @config.level
|
101
|
+
@handlers = @config.handlers
|
102
|
+
@output = output || @config.output
|
103
|
+
@ready_handlers = []
|
104
|
+
@data_filter = DataFilter.new(@config.filters.data,
|
105
|
+
mask: @config.filters.mask)
|
106
|
+
|
107
|
+
@types = LOG_TYPES.dup
|
108
|
+
@config.types.each do |name, log_level|
|
109
|
+
add_type(name, log_level)
|
110
|
+
end
|
111
|
+
|
112
|
+
@handlers.each do |handler|
|
113
|
+
add_handler(handler)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add new log type
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# add_type(:thanks, {level: :info})
|
121
|
+
#
|
122
|
+
# @api private
|
123
|
+
def add_type(name, log_level)
|
124
|
+
if @types.include?(name)
|
125
|
+
raise Error, "Already defined log type #{name.inspect}"
|
126
|
+
end
|
127
|
+
|
128
|
+
@types[name.to_sym] = log_level
|
129
|
+
self.class.define_level(name, log_level)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Add handler for logging messages
|
133
|
+
#
|
134
|
+
# @example
|
135
|
+
# add_handler(:console)
|
136
|
+
#
|
137
|
+
# @api public
|
138
|
+
def add_handler(handler)
|
139
|
+
h, options = *(handler.is_a?(Array) ? handler : [handler, {}])
|
140
|
+
name = coerce_handler(h)
|
141
|
+
global_opts = { output: @output, config: @config }
|
142
|
+
opts = global_opts.merge(options)
|
143
|
+
ready_handler = name.new(**opts)
|
144
|
+
@ready_handlers << ready_handler
|
145
|
+
end
|
146
|
+
|
147
|
+
# Remove log events handler
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# remove_handler(:console)
|
151
|
+
#
|
152
|
+
# @api public
|
153
|
+
def remove_handler(handler)
|
154
|
+
@ready_handlers.delete(handler)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Coerce handler name into object
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# coerce_handler(:console)
|
161
|
+
# # => TTY::Logger::Handlers::Console
|
162
|
+
#
|
163
|
+
# @raise [Error] when class cannot be coerced
|
164
|
+
#
|
165
|
+
# @return [Class]
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
def coerce_handler(name)
|
169
|
+
case name
|
170
|
+
when String, Symbol
|
171
|
+
Handlers.const_get(name.capitalize)
|
172
|
+
when Class
|
173
|
+
name
|
174
|
+
else
|
175
|
+
raise_handler_error
|
176
|
+
end
|
177
|
+
rescue NameError
|
178
|
+
raise_handler_error
|
179
|
+
end
|
180
|
+
|
181
|
+
# Raise error when unknown handler name
|
182
|
+
#
|
183
|
+
# @api private
|
184
|
+
def raise_handler_error
|
185
|
+
raise Error, "Handler needs to be a class name or a symbol name"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Copy this logger
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# logger = TTY::Logger.new
|
192
|
+
# child_logger = logger.copy(app: "myenv", env: "prod")
|
193
|
+
# child_logger.info("Deploying")
|
194
|
+
#
|
195
|
+
# @return [TTY::Logger]
|
196
|
+
# a new copy of this logger
|
197
|
+
#
|
198
|
+
# @api public
|
199
|
+
def copy(new_fields)
|
200
|
+
new_config = @config.to_proc.call(Config.new)
|
201
|
+
if block_given?
|
202
|
+
yield(new_config)
|
203
|
+
end
|
204
|
+
self.class.new(fields: @fields.merge(new_fields),
|
205
|
+
output: @output, &new_config)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Check current level against another
|
209
|
+
#
|
210
|
+
# @return [Symbol]
|
211
|
+
#
|
212
|
+
# @api public
|
213
|
+
def log?(level, other_level)
|
214
|
+
compare_levels(level, other_level) != :gt
|
215
|
+
end
|
216
|
+
|
217
|
+
# Logs streaming output.
|
218
|
+
#
|
219
|
+
# @example
|
220
|
+
# logger << "Example output"
|
221
|
+
#
|
222
|
+
# @api public
|
223
|
+
def write(*msg)
|
224
|
+
event = Event.new(filter(*msg))
|
225
|
+
|
226
|
+
@ready_handlers.each do |handler|
|
227
|
+
handler.(event)
|
228
|
+
end
|
229
|
+
|
230
|
+
self
|
231
|
+
end
|
232
|
+
alias << write
|
233
|
+
|
234
|
+
# Log a message given the severtiy level
|
235
|
+
#
|
236
|
+
# @example
|
237
|
+
# logger.log(:info, "Deployed successfully")
|
238
|
+
#
|
239
|
+
# @example
|
240
|
+
# logger.log(:info) { "Deployed successfully" }
|
241
|
+
#
|
242
|
+
# @api public
|
243
|
+
def log(current_level, *msg)
|
244
|
+
scoped_fields = msg.last.is_a?(::Hash) ? msg.pop : {}
|
245
|
+
fields_copy = scoped_fields.dup
|
246
|
+
if msg.empty? && block_given?
|
247
|
+
msg = []
|
248
|
+
Array[yield].flatten(1).each do |el|
|
249
|
+
el.is_a?(::Hash) ? fields_copy.merge!(el) : msg << el
|
250
|
+
end
|
251
|
+
end
|
252
|
+
top_caller = caller_locations(1, 1)[0]
|
253
|
+
loc = caller_locations(2, 1)[0] || top_caller
|
254
|
+
label = top_caller.label
|
255
|
+
metadata = {
|
256
|
+
level: current_level,
|
257
|
+
time: Time.now,
|
258
|
+
pid: Process.pid,
|
259
|
+
name: @types.include?(label.to_sym) ? label : current_level,
|
260
|
+
path: loc.path,
|
261
|
+
lineno: loc.lineno,
|
262
|
+
method: loc.base_label
|
263
|
+
}
|
264
|
+
event = Event.new(filter(*msg),
|
265
|
+
@data_filter.filter(@fields.merge(fields_copy)),
|
266
|
+
metadata)
|
267
|
+
|
268
|
+
@ready_handlers.each do |handler|
|
269
|
+
level = handler.respond_to?(:level) ? handler.level : @config.level
|
270
|
+
handler.(event) if log?(level, current_level)
|
271
|
+
end
|
272
|
+
self
|
273
|
+
end
|
274
|
+
|
275
|
+
# Change current log level for the duration of the block
|
276
|
+
#
|
277
|
+
# @example
|
278
|
+
# logger.log_at :debug do
|
279
|
+
# logger.debug("logged")
|
280
|
+
# end
|
281
|
+
#
|
282
|
+
# @param [String] tmp_level
|
283
|
+
# the temporary log level
|
284
|
+
#
|
285
|
+
# @api public
|
286
|
+
def log_at(tmp_level, &block)
|
287
|
+
@ready_handlers.each do |handler|
|
288
|
+
handler.log_at(tmp_level, &block)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Filter message parts for any sensitive information and
|
293
|
+
# replace with placeholder.
|
294
|
+
#
|
295
|
+
# @param [Array[Object]] objects
|
296
|
+
# the messages to filter
|
297
|
+
#
|
298
|
+
# @return [Array[String]]
|
299
|
+
# the filtered message
|
300
|
+
#
|
301
|
+
# @api private
|
302
|
+
def filter(*objects)
|
303
|
+
objects.map do |obj|
|
304
|
+
case obj
|
305
|
+
when Exception
|
306
|
+
backtrace = Array(obj.backtrace).map { |line| swap_filtered(line) }
|
307
|
+
copy_error(obj, swap_filtered(obj.message), backtrace)
|
308
|
+
else
|
309
|
+
swap_filtered(obj.to_s)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Create a new error instance copy
|
315
|
+
#
|
316
|
+
# @param [Exception] error
|
317
|
+
# @param [String] message
|
318
|
+
# @param [Array,nil] backtrace
|
319
|
+
#
|
320
|
+
# @return [Exception]
|
321
|
+
#
|
322
|
+
# @api private
|
323
|
+
def copy_error(error, message, backtrace = nil)
|
324
|
+
new_error = error.exception(message)
|
325
|
+
new_error.set_backtrace(backtrace)
|
326
|
+
new_error
|
327
|
+
end
|
328
|
+
|
329
|
+
# Swap string content for filtered content
|
330
|
+
#
|
331
|
+
# @param [String] obj
|
332
|
+
#
|
333
|
+
# @api private
|
334
|
+
def swap_filtered(obj)
|
335
|
+
obj.dup.tap do |obj_copy|
|
336
|
+
@config.filters.message.each do |text|
|
337
|
+
obj_copy.gsub!(text, @config.filters.mask)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
9
341
|
end # Logger
|
10
342
|
end # TTY
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "handlers/console"
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Logger
|
7
|
+
class Config
|
8
|
+
FILTERED = "[FILTERED]"
|
9
|
+
|
10
|
+
# The format used for date display. uses strftime format
|
11
|
+
attr_accessor :date_format
|
12
|
+
|
13
|
+
# The format used for time display. uses strftime format
|
14
|
+
attr_accessor :time_format
|
15
|
+
|
16
|
+
# The format used for displaying structured data
|
17
|
+
attr_accessor :formatter
|
18
|
+
|
19
|
+
# The handlers used to display logging info. Defaults to [:console]
|
20
|
+
attr_accessor :handlers
|
21
|
+
|
22
|
+
# The level to log messages at. Default to :info
|
23
|
+
attr_accessor :level
|
24
|
+
|
25
|
+
# The maximum message size to be logged in bytes. Defaults to 8192
|
26
|
+
attr_accessor :max_bytes
|
27
|
+
|
28
|
+
# The maximum depth for formatting array and hash objects. Defaults to 3
|
29
|
+
attr_accessor :max_depth
|
30
|
+
|
31
|
+
# The meta info to display, can be :date, :time, :file, :pid. Defaults to []
|
32
|
+
attr_accessor :metadata
|
33
|
+
|
34
|
+
# The output for the log messages. Defaults to `stderr`
|
35
|
+
attr_accessor :output
|
36
|
+
|
37
|
+
# The new custom log types. Defaults to `{}`
|
38
|
+
attr_accessor :types
|
39
|
+
|
40
|
+
# Create a configuration instance
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def initialize(**options)
|
44
|
+
@max_bytes = options.fetch(:max_bytes) { 2**13 }
|
45
|
+
@max_depth = options.fetch(:max_depth) { 3 }
|
46
|
+
@level = options.fetch(:level) { :info }
|
47
|
+
@metadata = options.fetch(:metadata) { [] }
|
48
|
+
@handlers = options.fetch(:handlers) { [:console] }
|
49
|
+
@formatter = options.fetch(:formatter) { :text }
|
50
|
+
@date_format = options.fetch(:date_format) { "%F" }
|
51
|
+
@time_format = options.fetch(:time_format) { "%T.%3N" }
|
52
|
+
@output = options.fetch(:output) { $stderr }
|
53
|
+
@types = options.fetch(:types) { {} }
|
54
|
+
end
|
55
|
+
|
56
|
+
class FiltersProvider
|
57
|
+
attr_accessor :message, :data, :mask
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
@message = []
|
61
|
+
@data = []
|
62
|
+
@mask = FILTERED
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_h
|
66
|
+
{ message: @message, data: @data, mask: @mask }
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
to_h.inspect
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# The filters to hide sensitive data from the message(s) and data.
|
75
|
+
#
|
76
|
+
# @return [FiltersProvider]
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def filters
|
80
|
+
@filters ||= FiltersProvider.new
|
81
|
+
end
|
82
|
+
|
83
|
+
# Allow to overwirte filters
|
84
|
+
attr_writer :filters
|
85
|
+
|
86
|
+
# Clone settings
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def to_proc
|
90
|
+
-> (config) {
|
91
|
+
config.date_format = @date_format.dup
|
92
|
+
config.time_format = @time_format.dup
|
93
|
+
config.filters = @filters.dup
|
94
|
+
config.formatter = @formatter
|
95
|
+
config.handlers = @handlers.dup
|
96
|
+
config.level = @level
|
97
|
+
config.max_bytes = @max_bytes
|
98
|
+
config.max_depth = @max_depth
|
99
|
+
config.metadata = @metadata.dup
|
100
|
+
config.output = @output.dup
|
101
|
+
config.types = @types.dup
|
102
|
+
config
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# Hash representation of this config
|
107
|
+
#
|
108
|
+
# @return [Hash[Symbol]]
|
109
|
+
#
|
110
|
+
# @api public
|
111
|
+
def to_h
|
112
|
+
{
|
113
|
+
date_format: date_format,
|
114
|
+
filters: filters.to_h,
|
115
|
+
formatter: formatter,
|
116
|
+
handlers: handlers,
|
117
|
+
level: level,
|
118
|
+
max_bytes: max_bytes,
|
119
|
+
max_depth: max_depth,
|
120
|
+
metadata: metadata,
|
121
|
+
output: output,
|
122
|
+
time_format: time_format,
|
123
|
+
types: types
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end # Config
|
127
|
+
end # Logger
|
128
|
+
end # TTY
|