tty-logger 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +69 -8
  4. data/lib/tty/logger.rb +14 -10
  5. data/lib/tty/logger/config.rb +34 -3
  6. data/lib/tty/logger/data_filter.rb +121 -0
  7. data/lib/tty/logger/handlers/console.rb +13 -3
  8. data/lib/tty/logger/version.rb +1 -1
  9. data/tty-logger.gemspec +4 -6
  10. metadata +4 -38
  11. data/Rakefile +0 -8
  12. data/examples/child.rb +0 -9
  13. data/examples/console.rb +0 -22
  14. data/examples/custom_type.rb +0 -27
  15. data/examples/error.rb +0 -11
  16. data/examples/handler.rb +0 -19
  17. data/examples/log.rb +0 -9
  18. data/examples/output.rb +0 -15
  19. data/examples/override.rb +0 -29
  20. data/examples/stream.rb +0 -22
  21. data/spec/perf/json_formatter_spec.rb +0 -29
  22. data/spec/perf/log_spec.rb +0 -21
  23. data/spec/perf/text_formatter_spec.rb +0 -29
  24. data/spec/spec_helper.rb +0 -31
  25. data/spec/unit/add_handler_spec.rb +0 -25
  26. data/spec/unit/config_spec.rb +0 -109
  27. data/spec/unit/copy_spec.rb +0 -27
  28. data/spec/unit/event_spec.rb +0 -22
  29. data/spec/unit/exception_spec.rb +0 -45
  30. data/spec/unit/filter_spec.rb +0 -32
  31. data/spec/unit/formatter_spec.rb +0 -70
  32. data/spec/unit/formatters/json_spec.rb +0 -41
  33. data/spec/unit/formatters/text_spec.rb +0 -82
  34. data/spec/unit/handler_spec.rb +0 -83
  35. data/spec/unit/handlers/custom_spec.rb +0 -26
  36. data/spec/unit/handlers/null_spec.rb +0 -15
  37. data/spec/unit/handlers/stream_spec.rb +0 -72
  38. data/spec/unit/levels_spec.rb +0 -40
  39. data/spec/unit/log_at_spec.rb +0 -34
  40. data/spec/unit/log_metadata_spec.rb +0 -55
  41. data/spec/unit/log_spec.rb +0 -203
  42. data/spec/unit/output_spec.rb +0 -40
  43. data/tasks/console.rake +0 -11
  44. data/tasks/coverage.rake +0 -11
  45. data/tasks/spec.rake +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31142537abe01781e199d8b551eea32bdc802c1e9a46bec7947b3240249df798
4
- data.tar.gz: 33b6816da36b2c41651452a9efa82b8e69b62eef6dbdfdc136c5195140b76d4b
3
+ metadata.gz: af3524110f6873609fc2086063371a728bd63d08d9f9160c8469cef4e5e50d94
4
+ data.tar.gz: 4991ed5e387b34fbe8c2e1887ba36ce53e5bc3bc120f957eda6a778202abcf35
5
5
  SHA512:
6
- metadata.gz: d5b65cbbe0a3c9cefcb3fe082242a47e6090626c41531fbd6f97dfc528e795b2e2f56f2ce5f27f1a0404620ce2a12139745e028a9d3a31a6bf630bb7c600f3fb
7
- data.tar.gz: 9e19ee528817ea3ef0d4ccf5c4a9601857b69e3b2120ad8ca7b5715ca57ea164a346ecd7cd90ad253cc81b707820098a019b77ea54dff547e12e8384b7154477
6
+ metadata.gz: 66c6892da31f674bcdee76a92334976f19f4447e341cf61c5fce106d5ad7085035d5a580c80f614bc564f4f6567ae6a446aacfe17d7499fb7c65fd4e800d0003
7
+ data.tar.gz: 45ee01d044d21a79a85fe55933ef3da2c3aefd58cea31453d61f7007768687e9256447e214de0f90973dc4de136f35eba735e9619c0c17aa9d74b05a32d84090
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.3.0] - 2020-01-01
4
+
5
+ ### Added
6
+ * Add ability to filter sensitive information out of structured data
7
+
8
+ ### Changed
9
+ * Remove the test and task files from the gemspec
10
+
11
+ ### Fixed
12
+ * Fix console handler highlighting of nested hash keys
13
+
3
14
  ## [v0.2.0] - 2019-09-30
4
15
 
5
16
  ### Added
@@ -16,5 +27,6 @@
16
27
 
17
28
  * Initial implementation and release
18
29
 
30
+ [v0.3.0]: https://github.com/piotrmurach/tty-logger/compare/v0.2.0..v0.3.0
19
31
  [v0.2.0]: https://github.com/piotrmurach/tty-logger/compare/v0.1.0..v0.2.0
20
32
  [v0.1.0]: https://github.com/piotrmurach/tty-logger/compare/v0.1.0
data/README.md CHANGED
@@ -375,15 +375,17 @@ The `:metdata` configuration option can include the following symbols:
375
375
 
376
376
  ### 2.4.2 Filters
377
377
 
378
- You can filter sensitive data out of log output with `filters` configuration option. As a value provide a list of sensitive data items:
378
+ You can filter sensitive data out of log output with `filters` configuration option. The `filters` can be further configured to remove info from log message with `message` or structured data with `data`. Both methods, as a value accept a list of sensitive items to search for.
379
+
380
+ If you want to filter sensitive information from log messages use `message`:
379
381
 
380
382
  ```ruby
381
383
  logger = TTY::Logger.new(output: output) do |config|
382
- config.filters = ["secret", "password"]
384
+ config.filters.message = %w[secret password]
383
385
  end
384
386
  ```
385
387
 
386
- Which by default will replace each data item with `[FILTERED]` placeholder:
388
+ Which by default will replace each matching string with `[FILTERED]` placeholder:
387
389
 
388
390
  ```ruby
389
391
  logger.info("Super secret info with password")
@@ -391,13 +393,14 @@ logger.info("Super secret info with password")
391
393
  # ℹ info Super [FILTERED] info with [FILTERED]
392
394
  ```
393
395
 
394
- You can also replace each data item with a custom placeholder. To do so use a hash with pairs of matched text and replacement placeholder.
396
+ You can also replace each data item with a custom placeholder. To do so use a `:mask` keyword with a replacement placeholder.
395
397
 
396
- For example, to replace "secret" content with placeholder "<SECRET>" do:
398
+ For example, to replace "secret" content with placeholder `"<SECRET>"` do:
397
399
 
398
400
  ```ruby
399
401
  logger = TTY::Logger.new do |config|
400
- config.filters = { "secret" => "<SECRET>" }
402
+ config.filters.message = %w[secret]
403
+ config.filters.mask = "<SECRET>"
401
404
  end
402
405
  ```
403
406
 
@@ -409,6 +412,64 @@ logger.info("Super secret info")
409
412
  # ℹ info Super <SECRET> info
410
413
  ```
411
414
 
415
+ To filter out sensitive information out of structured data use `data` method. By default any value matching a parameter name will be filtered regardless of the level of nesting. If you wish to filter only a specific deeply nested key use a dot notation like `params.card.password` to only filter `{params: {card: {password: "Secret123"}}}`.
416
+
417
+ For example to filter out a `:password` from data do:
418
+
419
+ ```ruby
420
+ logger = TTY::Logger.new do |config|
421
+ config.filters.data = %i[password]
422
+ end
423
+ ```
424
+
425
+ This will filter out any key matching password:
426
+
427
+ ```ruby
428
+ logger.info("Secret info", password: "Secret123", email: "")
429
+ # =>
430
+ # ℹ info Secret info password="[FILTERED]" email="secret@example.com"
431
+ ```
432
+
433
+ But also any nested data item:
434
+
435
+ ```ruby
436
+ logger.info("Secret info", params: {password: "Secret123", email: ""})
437
+ # =>
438
+ # ℹ info Secret info params={password="[FILTERED]" email="secret@example.com"}
439
+ ```
440
+
441
+ You're not limited to using only direct string comparison. You can also match based on regular expressions. For example, to match keys starting with `ba` we can add a following filter:
442
+
443
+ ```ruby
444
+ logger = TTY::Logger.new do |config|
445
+ config.filters.data = [/ba/]
446
+ end
447
+ ```
448
+
449
+ Then appropriate values will be masked:
450
+
451
+ ```ruby
452
+ logger.info("Filtering data", {"foo" => {"bar" => "val", "baz" => "val"}})
453
+ # =>
454
+ # ℹ info Filtering data foo={bar="[FILTERED]" baz="[FILTERED]"}
455
+ ```
456
+
457
+ You can mix and match. To filter keys based on pattern inside a deeply nested hash use dot notation with regular expression. For example, to find keys for the `:foo` parent key that starts with `:b` character, we could do:
458
+
459
+ ```ruby
460
+ logger = TTY::Logger.new do |config|
461
+ config.filters.data = [/^foo\.b/]
462
+ end
463
+ ```
464
+
465
+ Then only keys under the `:foo` key will be filtered:
466
+
467
+ ```ruby
468
+ logger.info("Filtering data", {"foo" => {"bar" => "val"}, "baz" => {"bar" => val"}})
469
+ # =>
470
+ # ℹ info Filtering data foo={bar="[FILTERED]"} baz={bar=val}
471
+ ```
472
+
412
473
  ### 2.5 Cloning
413
474
 
414
475
  You can create a copy of a logger with the current configuration using the `copy` method.
@@ -547,7 +608,7 @@ end
547
608
  By default, the output will be a plain text streamed to console. The text contains key and value pairs of all the metadata and the message of the log event.
548
609
 
549
610
  ```ruby
550
- loggger.info("Info about the deploy", app:"myap", env:"prod")
611
+ logger.info("Info about the deploy", app:"myap", env:"prod")
551
612
  # =>
552
613
  # pid=18315 date="2019-07-21" time="15:42:12.463" path="examples/stream.rb:17:in`<main>`"
553
614
  # level=info message="Info about the deploy" app=myapp env=prod
@@ -565,7 +626,7 @@ end
565
626
  This will output JSON formatted text streamed to console.
566
627
 
567
628
  ```ruby
568
- loggger.info("Info about the deploy", app="myap", env="prod")
629
+ logger.info("Info about the deploy", app="myap", env="prod")
569
630
  # =>
570
631
  # {"pid":18513,"date":"2019-07-21","time":"15:54:09.924","path":"examples/stream.rb:17:in`<main>`",
571
632
  # "level":"info","message":"Info about the deploy","app":"myapp","env":"prod"}
data/lib/tty/logger.rb CHANGED
@@ -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,8 +18,6 @@ module TTY
17
18
  # Error raised by this logger
18
19
  class Error < StandardError; end
19
20
 
20
- FILTERED = "[FILTERED]"
21
-
22
21
  LOG_TYPES = {
23
22
  debug: { level: :debug },
24
23
  info: { level: :info },
@@ -35,7 +34,7 @@ module TTY
35
34
  def self.define_level(name, log_level = nil)
36
35
  const_level = (LOG_TYPES[name.to_sym] || log_level)[:level]
37
36
 
38
- loc = caller_locations(0,1)[0]
37
+ loc = caller_locations(0, 1)[0]
39
38
  if loc
40
39
  file, line = loc.path, loc.lineno + 7
41
40
  else
@@ -95,6 +94,8 @@ module TTY
95
94
  @handlers = @config.handlers
96
95
  @output = output || @config.output
97
96
  @ready_handlers = []
97
+ @data_filter = DataFilter.new(@config.filters.data,
98
+ mask: @config.filters.mask)
98
99
 
99
100
  @config.types.each do |name, log_level|
100
101
  add_type(name, log_level)
@@ -129,7 +130,7 @@ module TTY
129
130
  name = coerce_handler(h)
130
131
  global_opts = { output: @output, config: @config }
131
132
  opts = global_opts.merge(options)
132
- ready_handler = name.new(opts)
133
+ ready_handler = name.new(**opts)
133
134
  @ready_handlers << ready_handler
134
135
  end
135
136
 
@@ -212,7 +213,8 @@ module TTY
212
213
  # logger.log(:info) { "Deployed successfully" }
213
214
  #
214
215
  # @api public
215
- def log(current_level, *msg, **scoped_fields)
216
+ def log(current_level, *msg)
217
+ scoped_fields = msg.last.is_a?(::Hash) ? msg.pop : {}
216
218
  fields_copy = scoped_fields.dup
217
219
  if msg.empty? && block_given?
218
220
  msg = []
@@ -220,8 +222,8 @@ module TTY
220
222
  el.is_a?(::Hash) ? fields_copy.merge!(el) : msg << el
221
223
  end
222
224
  end
223
- top_caller = caller_locations(1,1)[0]
224
- loc = caller_locations(2,1)[0] || top_caller
225
+ top_caller = caller_locations(1, 1)[0]
226
+ loc = caller_locations(2, 1)[0] || top_caller
225
227
  label = top_caller.label
226
228
  metadata = {
227
229
  level: current_level,
@@ -232,7 +234,9 @@ module TTY
232
234
  lineno: loc.lineno,
233
235
  method: loc.base_label
234
236
  }
235
- event = Event.new(filter(*msg), @fields.merge(fields_copy), metadata)
237
+ event = Event.new(filter(*msg),
238
+ @data_filter.filter(@fields.merge(fields_copy)),
239
+ metadata)
236
240
 
237
241
  @ready_handlers.each do |handler|
238
242
  level = handler.respond_to?(:level) ? handler.level : @config.level
@@ -271,8 +275,8 @@ module TTY
271
275
  def filter(*messages)
272
276
  messages.reduce([]) do |acc, msg|
273
277
  acc << msg.dup.tap do |msg_copy|
274
- @config.filters.each do |text, placeholder|
275
- msg_copy.gsub!(text, placeholder || FILTERED)
278
+ @config.filters.message.each do |text|
279
+ msg_copy.gsub!(text, @config.filters.mask)
276
280
  end
277
281
  end
278
282
  acc
@@ -5,13 +5,15 @@ require_relative "handlers/console"
5
5
  module TTY
6
6
  class Logger
7
7
  class Config
8
+ FILTERED = "[FILTERED]"
9
+
8
10
  # The format used for date display
9
11
  attr_accessor :date_format
10
12
 
11
13
  # The format used for time display
12
14
  attr_accessor :time_format
13
15
 
14
- # The storage of placholders to filter sensitive data out from the logs. Defaults to {}
16
+ # The filters to hide sensitive data from the messages and data.
15
17
  attr_accessor :filters
16
18
 
17
19
  # The format used for displaying structured data
@@ -46,7 +48,6 @@ module TTY
46
48
  @max_depth = options.fetch(:max_depth) { 3 }
47
49
  @level = options.fetch(:level) { :info }
48
50
  @metadata = options.fetch(:metadata) { [] }
49
- @filters = options.fetch(:filters) { {} }
50
51
  @handlers = options.fetch(:handlers) { [:console] }
51
52
  @formatter = options.fetch(:formatter) { :text }
52
53
  @date_format = options.fetch(:date_format) { "%F" }
@@ -55,6 +56,36 @@ module TTY
55
56
  @types = options.fetch(:types) { {} }
56
57
  end
57
58
 
59
+ class FiltersProvider
60
+ attr_accessor :message, :data, :mask
61
+
62
+ def initialize
63
+ @message = []
64
+ @data = []
65
+ @mask = FILTERED
66
+ end
67
+
68
+ def to_h
69
+ {message: @message, data: @data, mask: @mask}
70
+ end
71
+
72
+ def to_s
73
+ to_h.inspect
74
+ end
75
+ end
76
+
77
+ # The filters to hide sensitive data from the message(s) and data.
78
+ #
79
+ # @return [FiltersProvider]
80
+ #
81
+ # @api public
82
+ def filters
83
+ @filters ||= FiltersProvider.new
84
+ end
85
+
86
+ # Allow to overwirte filters
87
+ attr_writer :filters
88
+
58
89
  # Clone settings
59
90
  #
60
91
  # @api public
@@ -83,7 +114,7 @@ module TTY
83
114
  def to_h
84
115
  {
85
116
  date_format: date_format,
86
- filters: filters,
117
+ filters: filters.to_h,
87
118
  formatter: formatter,
88
119
  handlers: handlers,
89
120
  level: level,
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ class DataFilter
6
+ FILTERED = "[FILTERED]"
7
+ DOT = "."
8
+
9
+ attr_reader :filters, :compiled_filters, :mask
10
+
11
+ # Create a data filter instance with filters.
12
+ #
13
+ # @example
14
+ # TTY::Logger::DataFilter.new(%w[foo], mask: "<SECRET>")
15
+ #
16
+ # @param [String] mask
17
+ # the mask to replace object with. Defaults to `"[FILTERED]"`
18
+ #
19
+ # @api private
20
+ def initialize(filters = [], mask: nil)
21
+ @mask = mask || FILTERED
22
+ @filters = filters
23
+ @compiled_filters = compile(filters)
24
+ end
25
+
26
+ # Filter object for keys matching provided filters.
27
+ #
28
+ # @example
29
+ # data_filter = TTY::Logger::DataFilter.new(%w[foo])
30
+ # data_filter.filter({"foo" => "bar"})
31
+ # # => {"foo" => "[FILTERED]"}
32
+ #
33
+ # @param [Object] obj
34
+ # the object to filter
35
+ #
36
+ # @api public
37
+ def filter(obj)
38
+ return obj if filters.empty?
39
+
40
+ hash = obj.reduce({}) do |acc, (k, v)|
41
+ acc[k] = filter_val(k, v)
42
+ acc
43
+ end
44
+ hash
45
+ end
46
+
47
+ private
48
+
49
+ def compile(filters)
50
+ compiled = {
51
+ regexps: [],
52
+ nested_regexps: [],
53
+ blocks: [],
54
+ }
55
+ strings = []
56
+ nested_strings = []
57
+
58
+ filters.each do |filter|
59
+ case filter
60
+ when Proc
61
+ compiled[:blocks] << filter
62
+ when Regexp
63
+ if filter.to_s.include?(DOT)
64
+ compiled[:nested_regexps] << filter
65
+ else
66
+ compiled[:regexps] << filter
67
+ end
68
+ else
69
+ exp = Regexp.escape(filter)
70
+ if exp.include?(DOT)
71
+ nested_strings << exp
72
+ else
73
+ strings << exp
74
+ end
75
+ end
76
+ end
77
+
78
+ if !strings.empty?
79
+ compiled[:regexps] << /^(#{strings.join("|")})$/
80
+ end
81
+
82
+ if !nested_strings.empty?
83
+ compiled[:nested_regexps] << /^(#{nested_strings.join("|")})$/
84
+ end
85
+
86
+ compiled
87
+ end
88
+
89
+ def filter_val(key, val, composite = [])
90
+ return mask if filtered?(key, composite)
91
+
92
+ case val
93
+ when Hash then filter_obj(key, val, composite << key)
94
+ when Array then filter_arr(key, val, composite)
95
+ else val
96
+ end
97
+ end
98
+
99
+ def filtered?(key, composite)
100
+ composite_key = composite + [key]
101
+ joined_key = composite_key.join(DOT)
102
+ @compiled_filters[:regexps].any? { |reg| !!reg.match(key.to_s) } ||
103
+ @compiled_filters[:nested_regexps].any? { |reg| !!reg.match(joined_key) } ||
104
+ @compiled_filters[:blocks].any? { |block| block.(composite_key.dup) }
105
+ end
106
+
107
+ def filter_obj(_key, obj, composite)
108
+ obj.reduce({}) do |acc, (k, v)|
109
+ acc[k] = filter_val(k, v, composite)
110
+ acc
111
+ end
112
+ end
113
+
114
+ def filter_arr(key, obj, composite)
115
+ obj.reduce([]) do |acc, v|
116
+ acc << filter_val(key, v, composite)
117
+ end
118
+ end
119
+ end # DataFilter
120
+ end # Logger
121
+ end # TTY
@@ -57,6 +57,14 @@ module TTY
57
57
  }
58
58
  }
59
59
 
60
+ TEXT_REGEXP = /([{}()\[\]])?(["']?)(\S+?)(["']?=)/.freeze
61
+ JSON_REGEXP = /\"([^,]+?)\"(?=:)/.freeze
62
+
63
+ COLOR_PATTERNS = {
64
+ text: [TEXT_REGEXP, ->(c) { "\\1\\2" + c.("\\3") + "\\4" }],
65
+ json: [JSON_REGEXP, ->(c) { "\"" + c.("\\1") + "\"" }]
66
+ }.freeze
67
+
60
68
  attr_reader :output
61
69
 
62
70
  attr_reader :config
@@ -67,6 +75,8 @@ module TTY
67
75
  styles: {})
68
76
  @output = Array[output].flatten
69
77
  @formatter = coerce_formatter(formatter || config.formatter).new
78
+ @formatter_name = @formatter.class.name.split("::").last.downcase
79
+ @color_pattern = COLOR_PATTERNS[@formatter_name.to_sym]
70
80
  @config = config
71
81
  @styles = styles
72
82
  @level = level || @config.level
@@ -108,10 +118,10 @@ module TTY
108
118
  fmt << color.(style[:label]) + (" " * style[:levelpad])
109
119
  fmt << "%-25s" % event.message.join(" ")
110
120
  unless event.fields.empty?
121
+ pattern, replacement = *@color_pattern
111
122
  fmt << @formatter.dump(event.fields, max_bytes: config.max_bytes,
112
- max_depth: config.max_depth).
113
- gsub(/(\S+)(?=\=)/, color.("\\1")).
114
- gsub(/\"([^,]+?)\"(?=:)/, "\"" + color.("\\1") + "\"")
123
+ max_depth: config.max_depth)
124
+ .gsub(pattern, replacement.(color))
115
125
  end
116
126
  unless event.backtrace.empty?
117
127
  fmt << "\n" + format_backtrace(event)