lumberjack 1.2.7 → 1.4.2
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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +244 -0
- data/CHANGELOG.md +251 -56
- data/README.md +197 -62
- data/VERSION +1 -1
- data/lib/lumberjack/context.rb +25 -5
- data/lib/lumberjack/device/date_rolling_log_file.rb +17 -8
- data/lib/lumberjack/device/log_file.rb +14 -7
- data/lib/lumberjack/device/multi.rb +8 -7
- data/lib/lumberjack/device/null.rb +2 -2
- data/lib/lumberjack/device/rolling_log_file.rb +46 -22
- data/lib/lumberjack/device/size_rolling_log_file.rb +10 -10
- data/lib/lumberjack/device/writer.rb +45 -21
- data/lib/lumberjack/device.rb +28 -13
- data/lib/lumberjack/formatter/date_time_formatter.rb +5 -5
- data/lib/lumberjack/formatter/exception_formatter.rb +4 -4
- data/lib/lumberjack/formatter/id_formatter.rb +4 -3
- data/lib/lumberjack/formatter/inspect_formatter.rb +1 -1
- data/lib/lumberjack/formatter/multiply_formatter.rb +25 -0
- data/lib/lumberjack/formatter/object_formatter.rb +1 -1
- data/lib/lumberjack/formatter/pretty_print_formatter.rb +7 -5
- data/lib/lumberjack/formatter/redact_formatter.rb +23 -0
- data/lib/lumberjack/formatter/round_formatter.rb +21 -0
- data/lib/lumberjack/formatter/string_formatter.rb +1 -1
- data/lib/lumberjack/formatter/strip_formatter.rb +1 -1
- data/lib/lumberjack/formatter/structured_formatter.rb +3 -1
- data/lib/lumberjack/formatter/tagged_message.rb +39 -0
- data/lib/lumberjack/formatter/truncate_formatter.rb +27 -0
- data/lib/lumberjack/formatter.rb +96 -28
- data/lib/lumberjack/log_entry.rb +90 -19
- data/lib/lumberjack/logger.rb +318 -86
- data/lib/lumberjack/rack/context.rb +21 -2
- data/lib/lumberjack/rack/request_id.rb +8 -4
- data/lib/lumberjack/rack/unit_of_work.rb +7 -3
- data/lib/lumberjack/rack.rb +4 -4
- data/lib/lumberjack/severity.rb +22 -3
- data/lib/lumberjack/tag_context.rb +78 -0
- data/lib/lumberjack/tag_formatter.rb +124 -25
- data/lib/lumberjack/tagged_logger_support.rb +26 -12
- data/lib/lumberjack/tagged_logging.rb +1 -1
- data/lib/lumberjack/tags.rb +8 -8
- data/lib/lumberjack/template.rb +17 -5
- data/lib/lumberjack/utils.rb +182 -0
- data/lib/lumberjack.rb +64 -35
- data/lumberjack.gemspec +18 -15
- metadata +23 -54
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lumberjack
|
|
4
|
+
class Formatter
|
|
5
|
+
# This class can be used as the return value from a formatter `call` method to
|
|
6
|
+
# extract additional tags from an object being logged. This can be useful when there
|
|
7
|
+
# using structured logging to include important metadata in the log message.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # Automatically add tags with error details when logging an exception.
|
|
11
|
+
# logger.add_formatter(Exception, ->(e) {
|
|
12
|
+
# Lumberjack::Formatter::TaggedMessage.new(e.message, {
|
|
13
|
+
# error: {
|
|
14
|
+
# message: e.message,
|
|
15
|
+
# class: e.class.name,
|
|
16
|
+
# trace: e.backtrace
|
|
17
|
+
# }
|
|
18
|
+
# })
|
|
19
|
+
# })
|
|
20
|
+
class TaggedMessage
|
|
21
|
+
attr_reader :message, :tags
|
|
22
|
+
|
|
23
|
+
# @param [Formatter] formatter The formatter to apply the transformation to.
|
|
24
|
+
# @param [Proc] transform The transformation function to apply to the formatted string.
|
|
25
|
+
def initialize(message, tags)
|
|
26
|
+
@message = message
|
|
27
|
+
@tags = tags || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
inspect
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect
|
|
35
|
+
{message: @message, tags: @tags}.inspect
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lumberjack
|
|
4
|
+
class Formatter
|
|
5
|
+
# Truncate a string object to a specific length. This is useful
|
|
6
|
+
# for formatting messages when there is a limit on the number of
|
|
7
|
+
# characters that can be logged per message. This formatter should
|
|
8
|
+
# only be used when necessary since it is a lossy formatter.
|
|
9
|
+
#
|
|
10
|
+
# When a string is truncated, it will have a unicode ellipsis
|
|
11
|
+
# character (U+2026) appended to the end of the string.
|
|
12
|
+
class TruncateFormatter
|
|
13
|
+
# @param [Integer] length The maximum length of the string (defaults to 32K)
|
|
14
|
+
def initialize(length = 32768)
|
|
15
|
+
@length = length
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(obj)
|
|
19
|
+
if obj.is_a?(String) && obj.length > @length
|
|
20
|
+
"#{obj[0, @length - 1]}…"
|
|
21
|
+
else
|
|
22
|
+
obj
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/lumberjack/formatter.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lumberjack
|
|
4
4
|
# This class controls the conversion of log entry messages into a loggable format. This allows you
|
|
@@ -12,25 +12,32 @@ module Lumberjack
|
|
|
12
12
|
#
|
|
13
13
|
# Enumerable objects (including Hash and Array) will call the formatter recursively for each element.
|
|
14
14
|
class Formatter
|
|
15
|
-
require_relative "formatter/date_time_formatter
|
|
16
|
-
require_relative "formatter/exception_formatter
|
|
17
|
-
require_relative "formatter/id_formatter
|
|
18
|
-
require_relative "formatter/inspect_formatter
|
|
19
|
-
require_relative "formatter/
|
|
20
|
-
require_relative "formatter/
|
|
21
|
-
require_relative "formatter/
|
|
22
|
-
require_relative "formatter/
|
|
23
|
-
require_relative "formatter/
|
|
15
|
+
require_relative "formatter/date_time_formatter"
|
|
16
|
+
require_relative "formatter/exception_formatter"
|
|
17
|
+
require_relative "formatter/id_formatter"
|
|
18
|
+
require_relative "formatter/inspect_formatter"
|
|
19
|
+
require_relative "formatter/multiply_formatter"
|
|
20
|
+
require_relative "formatter/object_formatter"
|
|
21
|
+
require_relative "formatter/pretty_print_formatter"
|
|
22
|
+
require_relative "formatter/redact_formatter"
|
|
23
|
+
require_relative "formatter/round_formatter"
|
|
24
|
+
require_relative "formatter/string_formatter"
|
|
25
|
+
require_relative "formatter/strip_formatter"
|
|
26
|
+
require_relative "formatter/structured_formatter"
|
|
27
|
+
require_relative "formatter/truncate_formatter"
|
|
28
|
+
require_relative "formatter/tagged_message"
|
|
24
29
|
|
|
25
30
|
class << self
|
|
26
31
|
# Returns a new empty formatter with no mapping. For historical reasons, a formatter
|
|
27
32
|
# is initialized with mappings to help output objects as strings. This will return one
|
|
28
33
|
# without the default mappings.
|
|
34
|
+
#
|
|
35
|
+
# @return [Lumberjack::Formatter] a new empty formatter
|
|
29
36
|
def empty
|
|
30
37
|
new.clear
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
|
-
|
|
40
|
+
|
|
34
41
|
def initialize
|
|
35
42
|
@class_formatters = {}
|
|
36
43
|
@module_formatters = {}
|
|
@@ -45,7 +52,17 @@ module Lumberjack
|
|
|
45
52
|
# that responds to the +call+ method or as a symbol representing one of the predefined
|
|
46
53
|
# formatters, or as a block to the method call.
|
|
47
54
|
#
|
|
48
|
-
# The predefined formatters are:
|
|
55
|
+
# The predefined formatters are:
|
|
56
|
+
# - :date_time
|
|
57
|
+
# - :exception
|
|
58
|
+
# - :id
|
|
59
|
+
# - :inspect
|
|
60
|
+
# - :object
|
|
61
|
+
# - :pretty_print
|
|
62
|
+
# - :string
|
|
63
|
+
# - :strip
|
|
64
|
+
# - :structured
|
|
65
|
+
# - :truncate
|
|
49
66
|
#
|
|
50
67
|
# You can add multiple classes at once by passing an array of classes.
|
|
51
68
|
#
|
|
@@ -53,7 +70,18 @@ module Lumberjack
|
|
|
53
70
|
# help avoid loading dependency issues. This applies only to classes; modules cannot be
|
|
54
71
|
# passed in as strings.
|
|
55
72
|
#
|
|
56
|
-
#
|
|
73
|
+
# @param klass [Class, Module, String, Array<Class, Module, String>] The class or module to add a formatter for.
|
|
74
|
+
# @param formatter [Symbol, Class, String, #call] The formatter to use for the class.
|
|
75
|
+
# If a symbol is passed in, it will be used to load one of the predefined formatters.
|
|
76
|
+
# If a class is passed in, it will be initialized with the args passed in.
|
|
77
|
+
# Otherwise, the object will be used as the formatter and must respond to call method.
|
|
78
|
+
# @param args [Array] Arguments to pass to the formatter when it is initialized.
|
|
79
|
+
# @yield [obj] A block that will be used as the formatter for the class.
|
|
80
|
+
# @yieldparam [Object] obj The object to format.
|
|
81
|
+
# @yieldreturn [String] The formatted string.
|
|
82
|
+
# @return [self] Returns itself so that add statements can be chained together.
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
57
85
|
#
|
|
58
86
|
# # Use a predefined formatter
|
|
59
87
|
# formatter.add(MyClass, :pretty_print)
|
|
@@ -66,18 +94,27 @@ module Lumberjack
|
|
|
66
94
|
#
|
|
67
95
|
# # Add statements can be chained together
|
|
68
96
|
# formatter.add(MyClass, :pretty_print).add(YourClass){|obj| obj.humanize}
|
|
69
|
-
def add(klass, formatter = nil, &block)
|
|
97
|
+
def add(klass, formatter = nil, *args, &block)
|
|
70
98
|
formatter ||= block
|
|
71
99
|
if formatter.nil?
|
|
72
100
|
remove(klass)
|
|
73
101
|
else
|
|
102
|
+
formatter_class_name = nil
|
|
74
103
|
if formatter.is_a?(Symbol)
|
|
75
|
-
formatter_class_name = "#{formatter.to_s.gsub(/(^|_)([a-z])/){|m| $~[2].upcase}}Formatter"
|
|
76
|
-
|
|
104
|
+
formatter_class_name = "#{formatter.to_s.gsub(/(^|_)([a-z])/) { |m| $~[2].upcase }}Formatter"
|
|
105
|
+
elsif formatter.is_a?(String)
|
|
106
|
+
formatter_class_name = formatter
|
|
107
|
+
end
|
|
108
|
+
if formatter_class_name
|
|
109
|
+
formatter = Formatter.const_get(formatter_class_name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if formatter.is_a?(Class)
|
|
113
|
+
formatter = formatter.new(*args)
|
|
77
114
|
end
|
|
78
|
-
|
|
115
|
+
|
|
79
116
|
Array(klass).each do |k|
|
|
80
|
-
if k.
|
|
117
|
+
if k.instance_of?(Module)
|
|
81
118
|
@module_formatters[k] = formatter
|
|
82
119
|
else
|
|
83
120
|
k = k.name if k.is_a?(Class)
|
|
@@ -95,9 +132,12 @@ module Lumberjack
|
|
|
95
132
|
# You can also pass class names as strings instead of the classes themselves. This can
|
|
96
133
|
# help avoid loading dependency issues. This applies only to classes; modules cannot be
|
|
97
134
|
# passed in as strings.
|
|
135
|
+
#
|
|
136
|
+
# @param klass [Class, Module, String, Array<Class, Module, String>] The class or module to remove the formatters for.
|
|
137
|
+
# @return [self] Returns itself so that remove statements can be chained together.
|
|
98
138
|
def remove(klass)
|
|
99
139
|
Array(klass).each do |k|
|
|
100
|
-
if k.
|
|
140
|
+
if k.instance_of?(Module)
|
|
101
141
|
@module_formatters.delete(k)
|
|
102
142
|
else
|
|
103
143
|
k = k.name if k.is_a?(Class)
|
|
@@ -106,19 +146,38 @@ module Lumberjack
|
|
|
106
146
|
end
|
|
107
147
|
self
|
|
108
148
|
end
|
|
109
|
-
|
|
149
|
+
|
|
110
150
|
# Remove all formatters including the default formatter. Can be chained to add method calls.
|
|
151
|
+
#
|
|
152
|
+
# @return [self] Returns itself so that clear statements can be chained together.
|
|
111
153
|
def clear
|
|
112
154
|
@class_formatters.clear
|
|
113
155
|
@module_formatters.clear
|
|
114
156
|
self
|
|
115
157
|
end
|
|
116
158
|
|
|
117
|
-
#
|
|
159
|
+
# Return true if their are no registered formatters.
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean] true if there are no registered formatters, false otherwise.
|
|
162
|
+
def empty?
|
|
163
|
+
@class_formatters.empty? && @module_formatters.empty?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Format a message object by applying all formatters attached to it.
|
|
167
|
+
#
|
|
168
|
+
# @param message [Object] The message object to format.
|
|
169
|
+
# @return [Object] The formatted object.
|
|
118
170
|
def format(message)
|
|
119
171
|
formatter = formatter_for(message.class)
|
|
120
|
-
if formatter
|
|
121
|
-
|
|
172
|
+
if formatter&.respond_to?(:call)
|
|
173
|
+
begin
|
|
174
|
+
formatter.call(message)
|
|
175
|
+
rescue SystemStackError, StandardError => e
|
|
176
|
+
error_message = e.class.name
|
|
177
|
+
error_message = "#{error_message} #{e.message}" if e.message && e.message != ""
|
|
178
|
+
warn("<Error formatting #{message.class.name}: #{error_message}>")
|
|
179
|
+
"<Error formatting #{message.class.name}: #{error_message}>"
|
|
180
|
+
end
|
|
122
181
|
else
|
|
123
182
|
message
|
|
124
183
|
end
|
|
@@ -126,16 +185,25 @@ module Lumberjack
|
|
|
126
185
|
|
|
127
186
|
# Compatibility with the Logger::Formatter signature. This method will just convert the message
|
|
128
187
|
# object to a string and ignores the other parameters.
|
|
188
|
+
#
|
|
189
|
+
# @param severity [Integer, String, Symbol] The severity of the message.
|
|
190
|
+
# @param timestamp [Time] The time the message was logged.
|
|
191
|
+
# @param progname [String] The name of the program logging the message.
|
|
192
|
+
# @param msg [Object] The message object to format.
|
|
129
193
|
def call(severity, timestamp, progname, msg)
|
|
130
|
-
|
|
194
|
+
formatted_message = format(msg)
|
|
195
|
+
formatted_message = formatted_message.message if formatted_message.is_a?(TaggedMessage)
|
|
196
|
+
"#{formatted_message}#{Lumberjack::LINE_SEPARATOR}"
|
|
131
197
|
end
|
|
132
198
|
|
|
133
|
-
private
|
|
134
|
-
|
|
135
199
|
# Find the formatter for a class by looking it up using the class hierarchy.
|
|
136
|
-
|
|
200
|
+
#
|
|
201
|
+
# @api private
|
|
202
|
+
def formatter_for(klass)
|
|
203
|
+
return nil if empty?
|
|
204
|
+
|
|
137
205
|
check_modules = true
|
|
138
|
-
|
|
206
|
+
until klass.nil?
|
|
139
207
|
formatter = @class_formatters[klass.name]
|
|
140
208
|
return formatter if formatter
|
|
141
209
|
|
data/lib/lumberjack/log_entry.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lumberjack
|
|
4
4
|
# An entry in a log is a data structure that captures the log message as well as
|
|
@@ -8,8 +8,17 @@ module Lumberjack
|
|
|
8
8
|
|
|
9
9
|
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
10
10
|
|
|
11
|
+
# @deprecated Will be removed in version 2.0.
|
|
11
12
|
UNIT_OF_WORK_ID = "unit_of_work_id"
|
|
12
13
|
|
|
14
|
+
# Create a new log entry.
|
|
15
|
+
#
|
|
16
|
+
# @param time [Time] The time the log entry was created.
|
|
17
|
+
# @param severity [Integer, String] The severity of the log entry.
|
|
18
|
+
# @param message [String] The message to log.
|
|
19
|
+
# @param progname [String] The name of the program that created the log entry.
|
|
20
|
+
# @param pid [Integer] The process id of the program that created the log entry.
|
|
21
|
+
# @param tags [Hash<String, Object>] A hash of tags to associate with the log entry.
|
|
13
22
|
def initialize(time, severity, message, progname, pid, tags)
|
|
14
23
|
@time = time
|
|
15
24
|
@severity = (severity.is_a?(Integer) ? severity : Severity.label_to_level(severity))
|
|
@@ -17,10 +26,10 @@ module Lumberjack
|
|
|
17
26
|
@progname = progname
|
|
18
27
|
@pid = pid
|
|
19
28
|
# backward compatibility with 1.0 API where the last argument was the unit of work id
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
@tags = if tags.is_a?(Hash)
|
|
30
|
+
compact_tags(tags)
|
|
31
|
+
elsif !tags.nil?
|
|
32
|
+
{UNIT_OF_WORK_ID => tags}
|
|
24
33
|
end
|
|
25
34
|
end
|
|
26
35
|
|
|
@@ -29,40 +38,102 @@ module Lumberjack
|
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def to_s
|
|
32
|
-
"[#{time.strftime(TIME_FORMAT)}.#{(time.usec / 1000.0).round.to_s.rjust(3,
|
|
41
|
+
"[#{time.strftime(TIME_FORMAT)}.#{(time.usec / 1000.0).round.to_s.rjust(3, "0")} #{severity_label} #{progname}(#{pid})#{tags_to_s}] #{message}"
|
|
33
42
|
end
|
|
34
43
|
|
|
35
44
|
def inspect
|
|
36
45
|
to_s
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
#
|
|
48
|
+
# @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
|
|
40
49
|
def unit_of_work_id
|
|
41
|
-
|
|
50
|
+
Lumberjack::Utils.deprecated("Lumberjack::LogEntry#unit_of_work_id", "Lumberjack::LogEntry#unit_of_work_id will be removed in version 2.0") do
|
|
51
|
+
tags[UNIT_OF_WORK_ID] if tags
|
|
52
|
+
end
|
|
42
53
|
end
|
|
43
54
|
|
|
44
|
-
#
|
|
55
|
+
# @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
|
|
45
56
|
def unit_of_work_id=(value)
|
|
46
|
-
|
|
47
|
-
tags
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
Lumberjack::Utils.deprecated("Lumberjack::LogEntry#unit_of_work_id=", "Lumberjack::LogEntry#unit_of_work_id= will be removed in version 2.0") do
|
|
58
|
+
if tags
|
|
59
|
+
tags[UNIT_OF_WORK_ID] = value
|
|
60
|
+
else
|
|
61
|
+
@tags = {UNIT_OF_WORK_ID => value}
|
|
62
|
+
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
|
-
|
|
65
|
+
|
|
53
66
|
# Return the tag with the specified name.
|
|
67
|
+
#
|
|
68
|
+
# @param name [String, Symbol] The tag name.
|
|
69
|
+
# @return [Object, nil] The tag value or nil if the tag does not exist.
|
|
54
70
|
def tag(name)
|
|
55
|
-
|
|
71
|
+
return nil if tags.nil?
|
|
72
|
+
|
|
73
|
+
TagContext.new(tags)[name]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Helper method to expand the tags into a nested structure. Tags with dots in the name
|
|
77
|
+
# will be expanded into nested hashes.
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash] The tags expanded into a nested structure.
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# entry = Lumberjack::LogEntry.new(Time.now, Logger::INFO, "test", "app", 1500, "a.b.c" => 1, "a.b.d" => 2)
|
|
83
|
+
# entry.nested_tags # => {"a" => {"b" => {"c" => 1, "d" => 2}}}
|
|
84
|
+
def nested_tags
|
|
85
|
+
Utils.expand_tags(tags)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Return true if the log entry has no message and no tags.
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] True if the log entry is empty, false otherwise.
|
|
91
|
+
def empty?
|
|
92
|
+
(message.nil? || message == "") && (tags.nil? || tags.empty?)
|
|
56
93
|
end
|
|
57
94
|
|
|
58
95
|
private
|
|
59
96
|
|
|
60
97
|
def tags_to_s
|
|
61
|
-
tags_string =
|
|
62
|
-
|
|
63
|
-
tags.each { |name, value| tags_string << " #{name}:#{value.inspect}" }
|
|
64
|
-
end
|
|
98
|
+
tags_string = +""
|
|
99
|
+
tags&.each { |name, value| tags_string << " #{name}:#{value.inspect}" }
|
|
65
100
|
tags_string
|
|
66
101
|
end
|
|
102
|
+
|
|
103
|
+
def compact_tags(tags, seen = nil)
|
|
104
|
+
return {} if seen&.include?(tags.object_id)
|
|
105
|
+
|
|
106
|
+
delete_keys = nil
|
|
107
|
+
compacted_keys = nil
|
|
108
|
+
|
|
109
|
+
tags.each do |key, value|
|
|
110
|
+
if value.nil? || value == ""
|
|
111
|
+
delete_keys ||= []
|
|
112
|
+
delete_keys << key
|
|
113
|
+
elsif value.is_a?(Hash)
|
|
114
|
+
seen ||= Set.new
|
|
115
|
+
seen << tags.object_id
|
|
116
|
+
compacted_value = compact_tags(value, seen)
|
|
117
|
+
if compacted_value.empty?
|
|
118
|
+
delete_keys ||= []
|
|
119
|
+
delete_keys << key
|
|
120
|
+
elsif !value.equal?(compacted_value)
|
|
121
|
+
compacted_keys ||= []
|
|
122
|
+
compacted_keys << [key, compacted_value]
|
|
123
|
+
end
|
|
124
|
+
elsif value.is_a?(Array) && value.empty?
|
|
125
|
+
delete_keys ||= []
|
|
126
|
+
delete_keys << key
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return tags if delete_keys.nil? && compacted_keys.nil?
|
|
131
|
+
|
|
132
|
+
tags = tags.dup
|
|
133
|
+
delete_keys&.each { |key| tags.delete(key) }
|
|
134
|
+
compacted_keys&.each { |key, value| tags[key] = value }
|
|
135
|
+
|
|
136
|
+
tags
|
|
137
|
+
end
|
|
67
138
|
end
|
|
68
139
|
end
|