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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +244 -0
  3. data/CHANGELOG.md +251 -56
  4. data/README.md +197 -62
  5. data/VERSION +1 -1
  6. data/lib/lumberjack/context.rb +25 -5
  7. data/lib/lumberjack/device/date_rolling_log_file.rb +17 -8
  8. data/lib/lumberjack/device/log_file.rb +14 -7
  9. data/lib/lumberjack/device/multi.rb +8 -7
  10. data/lib/lumberjack/device/null.rb +2 -2
  11. data/lib/lumberjack/device/rolling_log_file.rb +46 -22
  12. data/lib/lumberjack/device/size_rolling_log_file.rb +10 -10
  13. data/lib/lumberjack/device/writer.rb +45 -21
  14. data/lib/lumberjack/device.rb +28 -13
  15. data/lib/lumberjack/formatter/date_time_formatter.rb +5 -5
  16. data/lib/lumberjack/formatter/exception_formatter.rb +4 -4
  17. data/lib/lumberjack/formatter/id_formatter.rb +4 -3
  18. data/lib/lumberjack/formatter/inspect_formatter.rb +1 -1
  19. data/lib/lumberjack/formatter/multiply_formatter.rb +25 -0
  20. data/lib/lumberjack/formatter/object_formatter.rb +1 -1
  21. data/lib/lumberjack/formatter/pretty_print_formatter.rb +7 -5
  22. data/lib/lumberjack/formatter/redact_formatter.rb +23 -0
  23. data/lib/lumberjack/formatter/round_formatter.rb +21 -0
  24. data/lib/lumberjack/formatter/string_formatter.rb +1 -1
  25. data/lib/lumberjack/formatter/strip_formatter.rb +1 -1
  26. data/lib/lumberjack/formatter/structured_formatter.rb +3 -1
  27. data/lib/lumberjack/formatter/tagged_message.rb +39 -0
  28. data/lib/lumberjack/formatter/truncate_formatter.rb +27 -0
  29. data/lib/lumberjack/formatter.rb +96 -28
  30. data/lib/lumberjack/log_entry.rb +90 -19
  31. data/lib/lumberjack/logger.rb +318 -86
  32. data/lib/lumberjack/rack/context.rb +21 -2
  33. data/lib/lumberjack/rack/request_id.rb +8 -4
  34. data/lib/lumberjack/rack/unit_of_work.rb +7 -3
  35. data/lib/lumberjack/rack.rb +4 -4
  36. data/lib/lumberjack/severity.rb +22 -3
  37. data/lib/lumberjack/tag_context.rb +78 -0
  38. data/lib/lumberjack/tag_formatter.rb +124 -25
  39. data/lib/lumberjack/tagged_logger_support.rb +26 -12
  40. data/lib/lumberjack/tagged_logging.rb +1 -1
  41. data/lib/lumberjack/tags.rb +8 -8
  42. data/lib/lumberjack/template.rb +17 -5
  43. data/lib/lumberjack/utils.rb +182 -0
  44. data/lib/lumberjack.rb +64 -35
  45. data/lumberjack.gemspec +18 -15
  46. 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
@@ -1,4 +1,4 @@
1
- # frozen_string_literals: true
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.rb"
16
- require_relative "formatter/exception_formatter.rb"
17
- require_relative "formatter/id_formatter.rb"
18
- require_relative "formatter/inspect_formatter.rb"
19
- require_relative "formatter/object_formatter.rb"
20
- require_relative "formatter/pretty_print_formatter.rb"
21
- require_relative "formatter/string_formatter.rb"
22
- require_relative "formatter/strip_formatter.rb"
23
- require_relative "formatter/structured_formatter.rb"
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: :inspect, :string, :exception, and :pretty_print.
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
- # === Examples
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
- formatter = Formatter.const_get(formatter_class_name).new
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.class == Module
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.class == Module
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
- # Format a message object as a string.
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 && formatter.respond_to?(:call)
121
- formatter.call(message)
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
- "#{format(msg)}#{Lumberjack::LINE_SEPARATOR}"
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
- def formatter_for(klass) #:nodoc:
200
+ #
201
+ # @api private
202
+ def formatter_for(klass)
203
+ return nil if empty?
204
+
137
205
  check_modules = true
138
- while klass != nil do
206
+ until klass.nil?
139
207
  formatter = @class_formatters[klass.name]
140
208
  return formatter if formatter
141
209
 
@@ -1,4 +1,4 @@
1
- # frozen_string_literals: true
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
- if tags.nil? || tags.is_a?(Hash)
21
- @tags = tags
22
- else
23
- @tags = { UNIT_OF_WORK_ID => tags }
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, '0')} #{severity_label} #{progname}(#{pid})#{tags_to_s}] #{message}"
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
- # Deprecated - backward compatibility with 1.0 API
48
+ # @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
40
49
  def unit_of_work_id
41
- tags[UNIT_OF_WORK_ID] if tags
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
- # Deprecated - backward compatibility with 1.0 API
55
+ # @deprecated - backward compatibility with 1.0 API. Will be removed in version 2.0.
45
56
  def unit_of_work_id=(value)
46
- if tags
47
- tags[UNIT_OF_WORK_ID] = value
48
- else
49
- @tags = { UNIT_OF_WORK_ID => value }
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
- tags[name.to_s] if tags
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 = String.new
62
- if tags
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