tty-logger 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "logger/config"
4
+ require_relative "logger/data_filter"
4
5
  require_relative "logger/event"
5
6
  require_relative "logger/formatters/json"
6
7
  require_relative "logger/formatters/text"
@@ -17,6 +18,43 @@ module TTY
17
18
  # Error raised by this logger
18
19
  class Error < StandardError; end
19
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
+
20
58
  # Logger configuration instance
21
59
  #
22
60
  # @api public
@@ -31,11 +69,24 @@ module TTY
31
69
  yield config
32
70
  end
33
71
 
72
+ # Instance logger configuration
73
+ #
74
+ # @api public
75
+ def configure
76
+ yield @config
77
+ end
78
+
34
79
  # Create a logger instance
35
80
  #
36
81
  # @example
37
82
  # logger = TTY::Logger.new(output: $stdout)
38
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
+ #
39
90
  # @api public
40
91
  def initialize(output: nil, fields: {})
41
92
  @fields = fields
@@ -50,24 +101,55 @@ module TTY
50
101
  @handlers = @config.handlers
51
102
  @output = output || @config.output
52
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
53
111
 
54
112
  @handlers.each do |handler|
55
113
  add_handler(handler)
56
114
  end
57
115
  end
58
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
+
59
132
  # Add handler for logging messages
60
133
  #
61
134
  # @example
62
135
  # add_handler(:console)
63
136
  #
137
+ # @example
138
+ # add_handler(:console, styles: { info: { color: :yellow } })
139
+ #
140
+ # @example
141
+ # add_handler([:console, message_format: "%-10s"])
142
+ #
143
+ # @param [Symbol, Handlers] handler
144
+ # the handler name or class
145
+ #
64
146
  # @api public
65
- def add_handler(handler)
66
- h, options = *(handler.is_a?(Array) ? handler : [handler, {}])
67
- name = coerce_handler(h)
147
+ def add_handler(handler, **options)
148
+ h, h_opts = *(handler.is_a?(Array) ? handler : [handler, options])
149
+ handler_type = coerce_handler(h)
68
150
  global_opts = { output: @output, config: @config }
69
- opts = global_opts.merge(options)
70
- ready_handler = name.new(opts)
151
+ opts = global_opts.merge(h_opts)
152
+ ready_handler = handler_type.new(**opts)
71
153
  @ready_handlers << ready_handler
72
154
  end
73
155
 
@@ -78,7 +160,8 @@ module TTY
78
160
  #
79
161
  # @api public
80
162
  def remove_handler(handler)
81
- @ready_handlers.delete(handler)
163
+ handler_type = coerce_handler(handler)
164
+ @ready_handlers.delete_if { |_handler| _handler.is_a?(handler_type) }
82
165
  end
83
166
 
84
167
  # Coerce handler name into object
@@ -112,18 +195,24 @@ module TTY
112
195
  raise Error, "Handler needs to be a class name or a symbol name"
113
196
  end
114
197
 
115
- # Add structured data
198
+ # Copy this logger
116
199
  #
117
200
  # @example
118
201
  # logger = TTY::Logger.new
119
- # logger.with(app: "myenv", env: "prod").debug("Deplying")
202
+ # child_logger = logger.copy(app: "myenv", env: "prod")
203
+ # child_logger.info("Deploying")
120
204
  #
121
205
  # @return [TTY::Logger]
122
206
  # a new copy of this logger
123
207
  #
124
208
  # @api public
125
- def with(new_fields)
126
- self.class.new(fields: @fields.merge(new_fields), output: @output)
209
+ def copy(new_fields)
210
+ new_config = @config.to_proc.call(Config.new)
211
+ if block_given?
212
+ yield(new_config)
213
+ end
214
+ self.class.new(fields: @fields.merge(new_fields),
215
+ output: @output, &new_config)
127
216
  end
128
217
 
129
218
  # Check current level against another
@@ -135,24 +224,57 @@ module TTY
135
224
  compare_levels(level, other_level) != :gt
136
225
  end
137
226
 
227
+ # Logs streaming output.
228
+ #
229
+ # @example
230
+ # logger << "Example output"
231
+ #
232
+ # @api public
233
+ def write(*msg)
234
+ event = Event.new(filter(*msg))
235
+
236
+ @ready_handlers.each do |handler|
237
+ handler.(event)
238
+ end
239
+
240
+ self
241
+ end
242
+ alias << write
243
+
138
244
  # Log a message given the severtiy level
139
245
  #
246
+ # @example
247
+ # logger.log(:info, "Deployed successfully")
248
+ #
249
+ # @example
250
+ # logger.log(:info) { "Deployed successfully" }
251
+ #
140
252
  # @api public
141
- def log(current_level, *msg, **scoped_fields)
253
+ def log(current_level, *msg)
254
+ scoped_fields = msg.last.is_a?(::Hash) ? msg.pop : {}
255
+ fields_copy = scoped_fields.dup
142
256
  if msg.empty? && block_given?
143
- msg = [yield]
257
+ msg = []
258
+ Array[yield].flatten(1).each do |el|
259
+ el.is_a?(::Hash) ? fields_copy.merge!(el) : msg << el
260
+ end
144
261
  end
145
- loc = caller_locations(2,1)[0]
262
+ top_caller = caller_locations(1, 1)[0]
263
+ loc = caller_locations(2, 1)[0] || top_caller
264
+ label = top_caller.label
146
265
  metadata = {
147
266
  level: current_level,
148
267
  time: Time.now,
149
268
  pid: Process.pid,
150
- name: caller_locations(1,1)[0].label,
269
+ name: @types.include?(label.to_sym) ? label : current_level,
151
270
  path: loc.path,
152
271
  lineno: loc.lineno,
153
272
  method: loc.base_label
154
273
  }
155
- event = Event.new(msg, @fields.merge(scoped_fields), metadata)
274
+ event = Event.new(filter(*msg),
275
+ @data_filter.filter(@fields.merge(fields_copy)),
276
+ metadata)
277
+
156
278
  @ready_handlers.each do |handler|
157
279
  level = handler.respond_to?(:level) ? handler.level : @config.level
158
280
  handler.(event) if log?(level, current_level)
@@ -160,57 +282,71 @@ module TTY
160
282
  self
161
283
  end
162
284
 
163
- # Log a message at :debug level
285
+ # Change current log level for the duration of the block
164
286
  #
165
- # @api public
166
- def debug(*msg, &block)
167
- log(:debug, *msg, &block)
168
- end
169
-
170
- # Log a message at :info level
287
+ # @example
288
+ # logger.log_at :debug do
289
+ # logger.debug("logged")
290
+ # end
171
291
  #
172
- # @examples
173
- # logger.info "Successfully deployed"
174
- # logger.info { "Dynamically generated info" }
292
+ # @param [String] tmp_level
293
+ # the temporary log level
175
294
  #
176
295
  # @api public
177
- def info(*msg, &block)
178
- log(:info, *msg, &block)
296
+ def log_at(tmp_level, &block)
297
+ @ready_handlers.each do |handler|
298
+ handler.log_at(tmp_level, &block)
299
+ end
179
300
  end
180
301
 
181
- # Log a message at :warn level
302
+ # Filter message parts for any sensitive information and
303
+ # replace with placeholder.
182
304
  #
183
- # @api public
184
- def warn(*msg, &block)
185
- log(:warn, *msg, &block)
186
- end
187
-
188
- # Log a message at :error level
305
+ # @param [Array[Object]] objects
306
+ # the messages to filter
189
307
  #
190
- # @api public
191
- def error(*msg, &block)
192
- log(:error, *msg, &block)
193
- end
194
-
195
- # Log a message at :fatal level
308
+ # @return [Array[String]]
309
+ # the filtered message
196
310
  #
197
- # @api public
198
- def fatal(*msg, &block)
199
- log(:fatal, *msg, &block)
311
+ # @api private
312
+ def filter(*objects)
313
+ objects.map do |obj|
314
+ case obj
315
+ when Exception
316
+ backtrace = Array(obj.backtrace).map { |line| swap_filtered(line) }
317
+ copy_error(obj, swap_filtered(obj.message), backtrace)
318
+ else
319
+ swap_filtered(obj.to_s)
320
+ end
321
+ end
200
322
  end
201
323
 
202
- # Log a message with a success label
324
+ # Create a new error instance copy
203
325
  #
204
- # @api public
205
- def success(*msg, &block)
206
- log(:info, *msg, &block)
326
+ # @param [Exception] error
327
+ # @param [String] message
328
+ # @param [Array,nil] backtrace
329
+ #
330
+ # @return [Exception]
331
+ #
332
+ # @api private
333
+ def copy_error(error, message, backtrace = nil)
334
+ new_error = error.exception(message)
335
+ new_error.set_backtrace(backtrace)
336
+ new_error
207
337
  end
208
338
 
209
- # Log a message with a wait label
339
+ # Swap string content for filtered content
210
340
  #
211
- # @api public
212
- def wait(*msg, &block)
213
- log(:info, *msg, &block)
341
+ # @param [String] obj
342
+ #
343
+ # @api private
344
+ def swap_filtered(obj)
345
+ obj.dup.tap do |obj_copy|
346
+ @config.filters.message.each do |text|
347
+ obj_copy.gsub!(text, @config.filters.mask)
348
+ end
349
+ end
214
350
  end
215
351
  end # Logger
216
352
  end # TTY
@@ -5,10 +5,12 @@ require_relative "handlers/console"
5
5
  module TTY
6
6
  class Logger
7
7
  class Config
8
- # The format used for date display
8
+ FILTERED = "[FILTERED]"
9
+
10
+ # The format used for date display. uses strftime format
9
11
  attr_accessor :date_format
10
12
 
11
- # The format used for time display
13
+ # The format used for time display. uses strftime format
12
14
  attr_accessor :time_format
13
15
 
14
16
  # The format used for displaying structured data
@@ -23,15 +25,18 @@ module TTY
23
25
  # The maximum message size to be logged in bytes. Defaults to 8192
24
26
  attr_accessor :max_bytes
25
27
 
26
- # The maximum depth for formattin array and hash objects. Defaults to 3
28
+ # The maximum depth for formatting array and hash objects. Defaults to 3
27
29
  attr_accessor :max_depth
28
30
 
29
31
  # The meta info to display, can be :date, :time, :file, :pid. Defaults to []
30
32
  attr_accessor :metadata
31
33
 
32
- # The output for the log messages. Default to `stderr`
34
+ # The output for the log messages. Defaults to `stderr`
33
35
  attr_accessor :output
34
36
 
37
+ # The new custom log types. Defaults to `{}`
38
+ attr_accessor :types
39
+
35
40
  # Create a configuration instance
36
41
  #
37
42
  # @api private
@@ -45,6 +50,57 @@ module TTY
45
50
  @date_format = options.fetch(:date_format) { "%F" }
46
51
  @time_format = options.fetch(:time_format) { "%T.%3N" }
47
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
+ }
48
104
  end
49
105
 
50
106
  # Hash representation of this config
@@ -55,6 +111,7 @@ module TTY
55
111
  def to_h
56
112
  {
57
113
  date_format: date_format,
114
+ filters: filters.to_h,
58
115
  formatter: formatter,
59
116
  handlers: handlers,
60
117
  level: level,
@@ -62,7 +119,8 @@ module TTY
62
119
  max_depth: max_depth,
63
120
  metadata: metadata,
64
121
  output: output,
65
- time_format: time_format
122
+ time_format: time_format,
123
+ types: types
66
124
  }
67
125
  end
68
126
  end # Config