lumberjack 1.2.10 → 1.3.1

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.
@@ -21,7 +21,7 @@ module Lumberjack
21
21
  # monitoring thread, but its use is highly recommended.
22
22
  #
23
23
  # Each log entry records the log message and severity along with the time it was logged, the
24
- # program name, process id, and unit of work id. The message will be converted to a string, but
24
+ # program name, process id, and an optional hash of tags. The message will be converted to a string, but
25
25
  # otherwise, it is up to the device how these values are recorded. Messages are converted to strings
26
26
  # using a Formatter associated with the logger.
27
27
  class Logger
@@ -36,8 +36,8 @@ module Lumberjack
36
36
  # Set the name of the program to attach to log entries.
37
37
  attr_writer :progname
38
38
 
39
- # The device being written to
40
- attr_accessor :device
39
+ # The Formatter used only for log entry messages.
40
+ attr_accessor :message_formatter
41
41
 
42
42
  # The TagFormatter used for formatting tags for output
43
43
  attr_accessor :tag_formatter
@@ -51,31 +51,30 @@ module Lumberjack
51
51
  # If it is :null, it will be a Null device that won't record any output.
52
52
  # Otherwise, it will be assumed to be file path and wrapped in a Device::LogFile class.
53
53
  #
54
- # This method can take the following options:
55
- #
56
- # * :level - The logging level below which messages will be ignored.
57
- # * :formatter - The formatter to use for outputting messages to the log.
58
- # * :datetime_format - The format to use for log timestamps.
59
- # * :tag_formatter - The TagFormatter to use for formatting tags.
60
- # * :progname - The name of the program that will be recorded with each log entry.
61
- # * :flush_seconds - The maximum number of seconds between flush calls.
62
- # * :roll - If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
63
- # * :max_size - If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
64
- #
65
54
  # All other options are passed to the device constuctor.
66
55
  #
67
56
  # @param [Lumberjack::Device, Object, Symbol, String] device The device to log to.
68
57
  # @param [Hash] options The options for the logger.
58
+ # @option options [Integer, Symbol, String] :level The logging level below which messages will be ignored.
59
+ # @option options [Lumberjack::Formatter] :formatter The formatter to use for outputting messages to the log.
60
+ # @option options [String] :datetime_format The format to use for log timestamps.
61
+ # @option options [Lumberjack::Formatter] :message_formatter The MessageFormatter to use for formatting log messages.
62
+ # @option options [Lumberjack::TagFormatter] :tag_formatter The TagFormatter to use for formatting tags.
63
+ # @option options [String] :progname The name of the program that will be recorded with each log entry.
64
+ # @option options [Numeric] :flush_seconds The maximum number of seconds between flush calls.
65
+ # @option options [Boolean] :roll If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
66
+ # @option options [Integer] :max_size If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
69
67
  def initialize(device = $stdout, options = {})
70
68
  options = options.dup
71
69
  self.level = options.delete(:level) || INFO
72
70
  self.progname = options.delete(:progname)
73
71
  max_flush_seconds = options.delete(:flush_seconds).to_f
74
72
 
75
- @device = open_device(device, options) if device
73
+ @logdev = open_device(device, options) if device
76
74
  self.formatter = (options[:formatter] || Formatter.new)
77
- @tag_formatter = (options[:tag_formatter] || TagFormatter.new)
78
- time_format = (options[:datetime_format] || options[:time_format])
75
+ @message_formatter = options[:message_formatter] || Formatter.empty
76
+ @tag_formatter = options[:tag_formatter] || TagFormatter.new
77
+ time_format = options[:datetime_format] || options[:time_format]
79
78
  self.datetime_format = time_format if time_format
80
79
  @last_flushed_at = Time.now
81
80
  @silencer = true
@@ -85,6 +84,23 @@ module Lumberjack
85
84
  create_flusher_thread(max_flush_seconds) if max_flush_seconds > 0
86
85
  end
87
86
 
87
+ # Get the logging device that is used to write log entries.
88
+ #
89
+ # @return [Lumberjack::Device] The logging device.
90
+ def device
91
+ @logdev
92
+ end
93
+
94
+ # Set the logging device to a new device.
95
+ #
96
+ # @param [Lumberjack::Device] device The new logging device.
97
+ # @return [void]
98
+ def device=(device)
99
+ @logdev = if device
100
+ open_device(device, options)
101
+ end
102
+ end
103
+
88
104
  # Get the timestamp format on the device if it has one.
89
105
  #
90
106
  # @return [String, nil] The timestamp format or nil if the device doesn't support it.
@@ -194,25 +210,30 @@ module Lumberjack
194
210
  Thread.current[:lumberjack_logging] = true
195
211
 
196
212
  time = Time.now
213
+
197
214
  message = message.call if message.is_a?(Proc)
198
- message = formatter.format(message)
215
+ msg_class_formatter = message_formatter&.formatter_for(message.class)
216
+ if msg_class_formatter
217
+ message = msg_class_formatter.call(message)
218
+ elsif formatter
219
+ message = formatter.format(message)
220
+ end
221
+ message_tags = nil
222
+ if message.is_a?(Formatter::TaggedMessage)
223
+ message_tags = message.tags
224
+ message = message.message
225
+ end
226
+
199
227
  progname ||= self.progname
200
228
 
201
229
  current_tags = self.tags
202
230
  tags = nil unless tags.is_a?(Hash)
203
- if current_tags.empty?
204
- tags = Tags.stringify_keys(tags) unless tags.nil?
205
- else
206
- tags = if tags.nil?
207
- current_tags.dup
208
- else
209
- current_tags.merge(Tags.stringify_keys(tags))
210
- end
211
- end
231
+ tags = merge_tags(current_tags, tags)
232
+ tags = merge_tags(tags, message_tags) if message_tags
212
233
  tags = Tags.expand_runtime_values(tags)
213
234
  tags = tag_formatter.format(tags) if tag_formatter
214
235
 
215
- entry = LogEntry.new(time, severity, message, progname, $$, tags)
236
+ entry = LogEntry.new(time, severity, message, progname, Process.pid, tags)
216
237
  write_to_device(entry)
217
238
  ensure
218
239
  Thread.current[:lumberjack_logging] = nil
@@ -441,6 +462,14 @@ module Lumberjack
441
462
  end
442
463
  end
443
464
 
465
+ # Provided for compatibility with ActiveSupport::LoggerThreadSafeLevel to temporarily set the log level.
466
+ #
467
+ # @param [Integer, String, Symbol] level The log level to use inside the block.
468
+ # @return [Object] The result of the block.
469
+ def log_at(level, &block)
470
+ silence(level, &block)
471
+ end
472
+
444
473
  # Set the program name that is associated with log messages. If a block
445
474
  # is given, the program name will be valid only within the block.
446
475
  #
@@ -462,10 +491,11 @@ module Lumberjack
462
491
  end
463
492
 
464
493
  # Set a hash of tags on logger. If a block is given, the tags will only be set
465
- # for the duration of the block. If this method is called inside such a block,
466
- # the tags will only be defined on the tags in that block. When the parent block
467
- # exits, all the tags will be reverted. If there is no block, then the tags will
468
- # be defined as global and apply to all log statements.
494
+ # for the duration of the block. Otherwise the tags will be applied on the current
495
+ # logger context for the duration of that context.
496
+ #
497
+ # If there is no block or context, the tags will be applied to the global context.
498
+ # This behavior is deprecated. Use the `tag_globally` method to set global tags instead.
469
499
  #
470
500
  # @param [Hash] tags The tags to set.
471
501
  # @return [void]
@@ -479,11 +509,31 @@ module Lumberjack
479
509
  thread_tags.merge!(tags)
480
510
  nil
481
511
  else
482
- @tags.merge!(tags)
483
- nil
512
+ Lumberjack::Utils.deprecated("Lumberjack::Logger#tag", "Lumberjack::Logger#tag must be called with a block or inside a context block. In version 2.0 it will no longer be used for setting global tags. Use Lumberjack::Logger#tag_globally instead.") do
513
+ tag_globally(tags)
514
+ end
484
515
  end
485
516
  end
486
517
 
518
+ # Set up a context block for the logger. All tags added within the block will be cleared when
519
+ # the block exits.
520
+ #
521
+ def context(&block)
522
+ thread_tags = thread_local_value(:lumberjack_logger_tags)&.dup
523
+ thread_tags ||= {}
524
+ push_thread_local_value(:lumberjack_logger_tags, thread_tags, &block)
525
+ end
526
+
527
+ # Add global tags to the logger that will appear on all log entries.
528
+ #
529
+ # @param [Hash] tags The tags to set.
530
+ # @return [void]
531
+ def tag_globally(tags)
532
+ tags = Tags.stringify_keys(tags)
533
+ @tags.merge!(tags)
534
+ nil
535
+ end
536
+
487
537
  # Remove a tag from the current tag context. If this is called inside a block to a
488
538
  # call to `tag`, the tags will only be removed for the duration of that block. Otherwise
489
539
  # they will be removed from the global tags.
@@ -513,6 +563,31 @@ module Lumberjack
513
563
  tags
514
564
  end
515
565
 
566
+ # Get the value of a tag by name from the current tag context.
567
+ #
568
+ # @param [String, Symbol] name The name of the tag to get.
569
+ # @return [Object, nil] The value of the tag or nil if the tag does not exist.
570
+ def tag_value(name)
571
+ all_tags = tags
572
+ return nil if tags.empty?
573
+
574
+ name = name.join(".") if name.is_a?(Array)
575
+ name = name.to_s
576
+ return all_tags[name] if all_tags.include?(name)
577
+
578
+ flattened_tags = Lumberjack::Utils.flatten_tags(all_tags)
579
+ return flattened_tags[name] if flattened_tags.include?(name)
580
+
581
+ flattened_tags.keys.select { |key| key.include?(".") }.each do |key|
582
+ parts = key.split(".")
583
+ while (subkey = parts.pop)
584
+ flattened_tags[parts.join(".")] = {subkey => flattened_tags[(parts + [subkey]).join(".")]}
585
+ end
586
+ end
587
+
588
+ flattened_tags[name]
589
+ end
590
+
516
591
  # Remove all tags on the current logger and logging context within a block.
517
592
  # You can still set new block scoped tags within theuntagged block and provide
518
593
  # tags on individual log methods.
@@ -533,6 +608,14 @@ module Lumberjack
533
608
  end
534
609
  end
535
610
 
611
+ # Return true if the thread is currently in a Lumberjack::Context block.
612
+ # When the logger is in a context block, tagging will only apply to that block.
613
+ #
614
+ # @return [Boolean]
615
+ def in_tag_context?
616
+ !!thread_local_value(:lumberjack_logger_tags)
617
+ end
618
+
536
619
  private
537
620
 
538
621
  # Dereference arguments to log calls so we can have methods with compatibility with ::Logger
@@ -560,6 +643,20 @@ module Lumberjack
560
643
  add_entry(severity, message, progname, tags)
561
644
  end
562
645
 
646
+ # Merge a tags hash into an existing tags hash.
647
+ def merge_tags(current_tags, tags)
648
+ if current_tags.nil? || current_tags.empty?
649
+ tags = Tags.stringify_keys(tags) unless tags.nil?
650
+ else
651
+ tags = if tags.nil?
652
+ current_tags.dup
653
+ else
654
+ current_tags.merge(Tags.stringify_keys(tags))
655
+ end
656
+ end
657
+ tags
658
+ end
659
+
563
660
  # Set a local value for a thread tied to this object.
564
661
  def set_thread_local_value(name, value) # :nodoc:
565
662
  values = Thread.current[name]
@@ -3,16 +3,35 @@
3
3
  module Lumberjack
4
4
  module Rack
5
5
  # Middleware to create a global context for Lumberjack for the scope of a rack request.
6
+ #
7
+ # The optional `env_tags` parameter can be used to set up global tags from the request
8
+ # environment. This is useful for setting tags that are relevant to the entire request
9
+ # like the request id, host, etc.
6
10
  class Context
7
- def initialize(app)
11
+ # @param [Object] app The rack application.
12
+ # @param [Hash] env_tags A hash of tags to set from the request environment. If a tag value is
13
+ # a Proc, it will be called with the request `env` as an argument to allow dynamic tag values
14
+ # based on request data.
15
+ def initialize(app, env_tags = nil)
8
16
  @app = app
17
+ @env_tags = env_tags
9
18
  end
10
19
 
11
20
  def call(env)
12
21
  Lumberjack.context do
22
+ apply_tags(env) if @env_tags
13
23
  @app.call(env)
14
24
  end
15
25
  end
26
+
27
+ private
28
+
29
+ def apply_tags(env)
30
+ tags = @env_tags.transform_values do |value|
31
+ value.is_a?(Proc) ? value.call(env) : value
32
+ end
33
+ Lumberjack.tag(tags)
34
+ end
16
35
  end
17
36
  end
18
37
  end
@@ -5,12 +5,16 @@ module Lumberjack
5
5
  # Support for using the Rails ActionDispatch request id in the log.
6
6
  # The format is expected to be a random UUID and only the first chunk is used for terseness
7
7
  # if the abbreviated argument is true.
8
+ #
9
+ # @deprecated Use tags instead of request id for unit of work. Will be removed in version 2.0.
8
10
  class RequestId
9
11
  REQUEST_ID = "action_dispatch.request_id"
10
12
 
11
13
  def initialize(app, abbreviated = false)
12
- @app = app
13
- @abbreviated = abbreviated
14
+ Lumberjack::Utils.deprecated("Lumberjack::Rack::RequestId", "Lumberjack::Rack::RequestId will be removed in version 2.0") do
15
+ @app = app
16
+ @abbreviated = abbreviated
17
+ end
14
18
  end
15
19
 
16
20
  def call(env)
@@ -2,9 +2,13 @@
2
2
 
3
3
  module Lumberjack
4
4
  module Rack
5
+ # @deprecated Use the Lumberjack::Rack::Context middleware instead to set a global tag
6
+ # with an identifier to tie log entries together in a unit of work. Will be removed in version 2.0.
5
7
  class UnitOfWork
6
8
  def initialize(app)
7
- @app = app
9
+ Lumberjack::Utils.deprecated("Lumberjack::Rack::UnitOfWork", "Lumberjack::Rack::UnitOfWork will be removed in version 2.0") do
10
+ @app = app
11
+ end
8
12
  end
9
13
 
10
14
  def call(env)
@@ -11,13 +11,14 @@ module Lumberjack
11
11
  class TagFormatter
12
12
  def initialize
13
13
  @formatters = {}
14
+ @class_formatters = {}
14
15
  @default_formatter = nil
15
16
  end
16
17
 
17
18
  # Add a default formatter applied to all tag values. This can either be a Lumberjack::Formatter
18
19
  # or an object that responds to `call` or a block.
19
20
  #
20
- # @param [Lumberjack::Formatter, #call, nil] formatter The formatter to use.
21
+ # @param formatter [Lumberjack::Formatter, #call, nil] The formatter to use.
21
22
  # If this is nil, then the block will be used as the formatter.
22
23
  # @return [Lumberjack::TagFormatter] self
23
24
  def default(formatter = nil, &block)
@@ -35,22 +36,37 @@ module Lumberjack
35
36
  self
36
37
  end
37
38
 
38
- # Add a formatter for specific tag names. This can either be a Lumberjack::Formatter
39
- # or an object that responds to `call` or a block. The default formatter will not be
40
- # applied.
39
+ # Add a formatter for specific tag names or object classes. This can either be a Lumberjack::Formatter
40
+ # or an object that responds to `call` or a block. The formatter will be applied if it matches either a tag name
41
+ # or if the tag value is an instance of a registered class. Tag name formatters will take precedence
42
+ # over class formatters. The default formatter will not be applied to a value if a tag formatter
43
+ # is applied to it.
41
44
  #
42
- # @param [String, Array<String>] names The tag names to apply the formatter to.
43
- # @param [Lumberjack::Formatter, #call, nil] formatter The formatter to use.
45
+ # Name formatters can be applied to nested hashes using dot syntax. For example, if you add a formatter
46
+ # for "foo.bar", it will be applied to the value of the "bar" key in the "foo" tag if that value is a hash.
47
+ #
48
+ # Class formatters will be applied recursively to nested hashes and arrays.
49
+ #
50
+ # @param names_or_classes [String, Module, Array<String, Module>] The tag names or object classes
51
+ # to apply the formatter to.
52
+ # @param formatter [Lumberjack::Formatter, #call, nil] The formatter to use.
44
53
  # If this is nil, then the block will be used as the formatter.
45
54
  # @return [Lumberjack::TagFormatter] self
46
- def add(names, formatter = nil, &block)
55
+ #
56
+ # @example
57
+ # tag_formatter.add("password", &:redact)
58
+ def add(names_or_classes, formatter = nil, &block)
47
59
  formatter ||= block
48
60
  formatter = dereference_formatter(formatter)
49
61
  if formatter.nil?
50
62
  remove(key)
51
63
  else
52
- Array(names).each do |name|
53
- @formatters[name.to_s] = formatter
64
+ Array(names_or_classes).each do |key|
65
+ if key.is_a?(Module)
66
+ @class_formatters[key] = formatter
67
+ else
68
+ @formatters[key.to_s] = formatter
69
+ end
54
70
  end
55
71
  end
56
72
  self
@@ -58,11 +74,15 @@ module Lumberjack
58
74
 
59
75
  # Remove formatters for specific tag names. The default formatter will still be applied.
60
76
  #
61
- # @param [String, Array<String>] names The tag names to remove the formatter from.
77
+ # @param names_or_classes [String, Module, Array<String, Module>] The tag names or classes to remove the formatter from.
62
78
  # @return [Lumberjack::TagFormatter] self
63
- def remove(names)
64
- Array(names).each do |name|
65
- @formatters.delete(name.to_s)
79
+ def remove(names_or_classes)
80
+ Array(names_or_classes).each do |key|
81
+ if key.is_a?(Module)
82
+ @class_formatters.delete(key)
83
+ else
84
+ @formatters.delete(key.to_s)
85
+ end
66
86
  end
67
87
  self
68
88
  end
@@ -78,28 +98,66 @@ module Lumberjack
78
98
 
79
99
  # Format a hash of tags using the formatters
80
100
  #
81
- # @param [Hash] tags The tags to format.
101
+ # @param tags [Hash] The tags to format.
82
102
  # @return [Hash] The formatted tags.
83
103
  def format(tags)
84
104
  return nil if tags.nil?
85
- if @default_formatter.nil? && (@formatters.empty? || (@formatters.keys & tags.keys).empty?)
86
- tags
105
+ if @default_formatter.nil? && @formatters.empty? && @class_formatters.empty?
106
+ return tags
107
+ end
108
+
109
+ formated_tags(tags)
110
+ end
111
+
112
+ private
113
+
114
+ def formated_tags(tags, skip_classes: nil, prefix: nil)
115
+ formatted = {}
116
+
117
+ tags.each do |name, value|
118
+ name = name.to_s
119
+ formatted[name] = formatted_tag_value(name, value, skip_classes: skip_classes, prefix: prefix)
120
+ end
121
+
122
+ formatted
123
+ end
124
+
125
+ def formatted_tag_value(name, value, skip_classes: nil, prefix: nil)
126
+ prefixed_name = prefix ? "#{prefix}#{name}" : name
127
+ using_class_formatter = false
128
+
129
+ formatter = @formatters[prefixed_name]
130
+ if formatter.nil? && (skip_classes.nil? || !skip_classes.include?(value.class))
131
+ formatter = class_formatter(value.class)
132
+ using_class_formatter = true if formatter
133
+ end
134
+
135
+ formatter ||= @default_formatter
136
+
137
+ formatted_value = if formatter.is_a?(Lumberjack::Formatter)
138
+ formatter.format(value)
139
+ elsif formatter.respond_to?(:call)
140
+ formatter.call(value)
87
141
  else
88
- formatted = {}
89
- tags.each do |name, value|
90
- formatter = (@formatters[name.to_s] || @default_formatter)
91
- if formatter.is_a?(Lumberjack::Formatter)
92
- value = formatter.format(value)
93
- elsif formatter.respond_to?(:call)
94
- value = formatter.call(value)
142
+ value
143
+ end
144
+
145
+ if formatted_value.is_a?(Enumerable)
146
+ skip_classes ||= []
147
+ skip_classes << value.class if using_class_formatter
148
+ sub_prefix = "#{prefixed_name}."
149
+
150
+ formatted_value = if formatted_value.is_a?(Hash)
151
+ formated_tags(formatted_value, skip_classes: skip_classes, prefix: sub_prefix)
152
+ else
153
+ formatted_value.collect do |item|
154
+ formatted_tag_value(nil, item, skip_classes: skip_classes, prefix: sub_prefix)
95
155
  end
96
- formatted[name.to_s] = value
97
156
  end
98
- formatted
99
157
  end
100
- end
101
158
 
102
- private
159
+ formatted_value
160
+ end
103
161
 
104
162
  def dereference_formatter(formatter)
105
163
  if formatter.is_a?(TaggedLoggerSupport::Formatter)
@@ -111,5 +169,22 @@ module Lumberjack
111
169
  formatter
112
170
  end
113
171
  end
172
+
173
+ def class_formatter(klass)
174
+ formatter = @class_formatters[klass]
175
+ return formatter if formatter
176
+
177
+ formatters = @class_formatters.select { |k, _| klass <= k }
178
+ return formatters.values.first if formatters.length <= 1
179
+
180
+ superclass = klass.superclass
181
+ while superclass
182
+ formatter = formatters[superclass]
183
+ return formatter if formatter
184
+ superclass = superclass.superclass
185
+ end
186
+
187
+ formatters.values.first
188
+ end
114
189
  end
115
190
  end
@@ -38,15 +38,19 @@ module Lumberjack
38
38
  end
39
39
 
40
40
  # Compatibility with ActiveSupport::TaggedLogging which only supports adding tags as strings.
41
- # If a tag looks like "key:value" or "key=value", it will be added as a key value pair.
42
- # Otherwise it will be appended to a list named "tagged".
41
+ # Tags will be added to the "tagged" key in the logger's tags hash as an array.
43
42
  def tagged(*tags, &block)
44
- tag_hash = {}
45
- tags.flatten.each do |tag|
46
- tagged_values = Array(tag_hash["tagged"] || self.tags["tagged"])
47
- tag_hash["tagged"] = tagged_values + [tag]
43
+ tagged_values = Array(tag_value("tagged"))
44
+ flattened_tags = tags.flatten.collect(&:to_s).reject do |tag|
45
+ tag.respond_to?(:blank?) ? tag.blank? : tag.empty?
46
+ end
47
+ tagged_values += flattened_tags unless flattened_tags.empty?
48
+
49
+ if block || in_tag_context?
50
+ tag("tagged" => tagged_values, &block)
51
+ else
52
+ tag_globally("tagged" => tagged_values)
48
53
  end
49
- tag(tag_hash, &block)
50
54
  end
51
55
 
52
56
  def push_tags(*tags)
@@ -54,13 +58,24 @@ module Lumberjack
54
58
  end
55
59
 
56
60
  def pop_tags(size = 1)
57
- tagged_values = Array(@tags["tagged"])
61
+ tagged_values = tag_value("tagged")
62
+ return unless tagged_values.is_a?(Array)
63
+
58
64
  tagged_values = ((tagged_values.size > size) ? tagged_values[0, tagged_values.size - size] : nil)
59
- tag("tagged" => tagged_values)
65
+
66
+ if in_tag_context?
67
+ tag("tagged" => tagged_values)
68
+ else
69
+ tag_globally("tagged" => tagged_values)
70
+ end
60
71
  end
61
72
 
62
73
  def clear_tags!
63
- tag("tagged" => nil)
74
+ if in_tag_context?
75
+ tag("tagged" => nil)
76
+ else
77
+ tag_globally("tagged" => nil)
78
+ end
64
79
  end
65
80
  end
66
81
  end
@@ -12,14 +12,8 @@ module Lumberjack
12
12
  return nil if hash.nil?
13
13
  if hash.keys.all? { |key| key.is_a?(String) }
14
14
  hash
15
- elsif hash.respond_to?(:transform_keys)
16
- hash.transform_keys(&:to_s)
17
15
  else
18
- copy = {}
19
- hash.each do |key, value|
20
- copy[key.to_s] = value
21
- end
22
- copy
16
+ hash.transform_keys(&:to_s)
23
17
  end
24
18
  end
25
19
 
@@ -99,7 +99,7 @@ module Lumberjack
99
99
  return [nil] * (tag_vars.size + 1) if tags.nil? || tags.size == 0
100
100
 
101
101
  tags_string = +""
102
- tags.each do |name, value|
102
+ Lumberjack::Utils.flatten_tags(tags).each do |name, value|
103
103
  unless value.nil? || tag_vars.include?(name)
104
104
  value = value.to_s
105
105
  value = value.gsub(Lumberjack::LINE_SEPARATOR, " ") if value.include?(Lumberjack::LINE_SEPARATOR)