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.
@@ -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