tty-logger 0.1.0 → 0.6.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.
@@ -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