tty-logger 0.2.0 → 0.3.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.
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)