lumberjack 1.0.13 → 1.1.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 (49) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +74 -0
  3. data/README.md +92 -20
  4. data/VERSION +1 -1
  5. data/lib/lumberjack.rb +51 -16
  6. data/lib/lumberjack/context.rb +35 -0
  7. data/lib/lumberjack/device.rb +20 -6
  8. data/lib/lumberjack/device/date_rolling_log_file.rb +2 -2
  9. data/lib/lumberjack/device/log_file.rb +12 -1
  10. data/lib/lumberjack/device/multi.rb +46 -0
  11. data/lib/lumberjack/device/null.rb +0 -2
  12. data/lib/lumberjack/device/rolling_log_file.rb +2 -2
  13. data/lib/lumberjack/device/size_rolling_log_file.rb +1 -1
  14. data/lib/lumberjack/device/writer.rb +86 -55
  15. data/lib/lumberjack/formatter.rb +54 -18
  16. data/lib/lumberjack/formatter/date_time_formatter.rb +26 -0
  17. data/lib/lumberjack/formatter/id_formatter.rb +23 -0
  18. data/lib/lumberjack/formatter/object_formatter.rb +12 -0
  19. data/lib/lumberjack/formatter/structured_formatter.rb +31 -0
  20. data/lib/lumberjack/log_entry.rb +41 -16
  21. data/lib/lumberjack/logger.rb +168 -63
  22. data/lib/lumberjack/rack.rb +3 -2
  23. data/lib/lumberjack/rack/context.rb +18 -0
  24. data/lib/lumberjack/rack/request_id.rb +4 -4
  25. data/lib/lumberjack/severity.rb +11 -9
  26. data/lib/lumberjack/tags.rb +24 -0
  27. data/lib/lumberjack/template.rb +74 -32
  28. data/lumberjack.gemspec +35 -0
  29. metadata +48 -37
  30. data/Rakefile +0 -40
  31. data/spec/device/date_rolling_log_file_spec.rb +0 -73
  32. data/spec/device/log_file_spec.rb +0 -48
  33. data/spec/device/null_spec.rb +0 -12
  34. data/spec/device/rolling_log_file_spec.rb +0 -151
  35. data/spec/device/size_rolling_log_file_spec.rb +0 -58
  36. data/spec/device/writer_spec.rb +0 -118
  37. data/spec/formatter/exception_formatter_spec.rb +0 -20
  38. data/spec/formatter/inspect_formatter_spec.rb +0 -13
  39. data/spec/formatter/pretty_print_formatter_spec.rb +0 -14
  40. data/spec/formatter/string_formatter_spec.rb +0 -12
  41. data/spec/formatter_spec.rb +0 -45
  42. data/spec/log_entry_spec.rb +0 -69
  43. data/spec/logger_spec.rb +0 -411
  44. data/spec/lumberjack_spec.rb +0 -29
  45. data/spec/rack/request_id_spec.rb +0 -48
  46. data/spec/rack/unit_of_work_spec.rb +0 -26
  47. data/spec/severity_spec.rb +0 -23
  48. data/spec/spec_helper.rb +0 -32
  49. data/spec/template_spec.rb +0 -34
@@ -0,0 +1,26 @@
1
+ # frozen_string_literals: true
2
+
3
+ module Lumberjack
4
+ class Formatter
5
+ # Format a Date, Time, or DateTime object. If you don't specify a format in the constructor,
6
+ # it will use the ISO-8601 format.
7
+ class DateTimeFormatter
8
+
9
+ attr_reader :format
10
+
11
+ def initialize(format = nil)
12
+ @format = format.dup.to_s.freeze unless format.nil?
13
+ end
14
+
15
+ def call(obj)
16
+ if @format && obj.respond_to?(:strftime)
17
+ obj.strftime(@format)
18
+ elsif obj.respond_to?(:iso8601)
19
+ obj.iso8601
20
+ else
21
+ obj.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literals: true
2
+
3
+ module Lumberjack
4
+ class Formatter
5
+ # Format an object that has an id as a hash with keys for class and id. This formatter is useful
6
+ # as a default formatter for objects pulled from a data store. By default it will use :id as the
7
+ # id attribute.
8
+ class IdFormatter
9
+ def initialize(id_attribute = :id)
10
+ @id_attribute = id_attribute
11
+ end
12
+
13
+ def call(obj)
14
+ if obj.respond_to?(@id_attribute)
15
+ id = obj.send(@id_attribute)
16
+ { "class" => obj.class.name, "id" => id }
17
+ else
18
+ obj.to_s
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literals: true
2
+
3
+ module Lumberjack
4
+ class Formatter
5
+ # No-op formatter that just returns the object itself.
6
+ class ObjectFormatter
7
+ def call(obj)
8
+ obj
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literals: true
2
+
3
+ require "set"
4
+
5
+ module Lumberjack
6
+ class Formatter
7
+ # Dereference arrays and hashes and recursively call formatters on each element.
8
+ class StructuredFormatter
9
+ def initialize(formatter = nil)
10
+ @formatter = formatter
11
+ end
12
+
13
+ def call(obj)
14
+ if obj.is_a?(Hash)
15
+ hash = {}
16
+ references ||= Set.new
17
+ obj.each do |name, value|
18
+ hash[name.to_s] = call(value)
19
+ end
20
+ hash
21
+ elsif obj.is_a?(Enumerable)
22
+ obj.collect { |element| call(element) }
23
+ elsif @formatter
24
+ @formatter.format(obj)
25
+ else
26
+ obj
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,35 +4,60 @@ module Lumberjack
4
4
  # An entry in a log is a data structure that captures the log message as well as
5
5
  # information about the system that logged the message.
6
6
  class LogEntry
7
- attr_accessor :time, :message, :severity, :progname, :pid, :unit_of_work_id
8
-
9
- TIME_FORMAT = "%Y-%m-%dT%H:%M:%S".freeze
10
-
11
- def initialize(time, severity, message, progname, pid, unit_of_work_id)
7
+ attr_accessor :time, :message, :severity, :progname, :pid, :tags
8
+
9
+ TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
10
+
11
+ UNIT_OF_WORK_ID = "unit_of_work_id"
12
+
13
+ def initialize(time, severity, message, progname, pid, tags)
12
14
  @time = time
13
15
  @severity = (severity.is_a?(Integer) ? severity : Severity.label_to_level(severity))
14
16
  @message = message
15
17
  @progname = progname
16
18
  @pid = pid
17
- @unit_of_work_id = unit_of_work_id
19
+ # 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 }
24
+ end
18
25
  end
19
-
26
+
20
27
  def severity_label
21
28
  Severity.level_to_label(severity)
22
29
  end
23
-
30
+
24
31
  def to_s
25
- buf = "[#{time.strftime(TIME_FORMAT)}.#{(time.usec / 1000.0).round.to_s.rjust(3, '0')} #{severity_label} #{progname}(#{pid})"
26
- if unit_of_work_id
27
- buf << " #"
28
- buf << unit_of_work_id
29
- end
30
- buf << "] "
31
- buf << message
32
+ "[#{time.strftime(TIME_FORMAT)}.#{(time.usec / 1000.0).round.to_s.rjust(3, '0')} #{severity_label} #{progname}(#{pid})#{tags_to_s}] #{message}"
32
33
  end
33
-
34
+
34
35
  def inspect
35
36
  to_s
36
37
  end
38
+
39
+ # Deprecated - backward compatibility with 1.0 API
40
+ def unit_of_work_id
41
+ tags[UNIT_OF_WORK_ID] if tags
42
+ end
43
+
44
+ # Deprecated - backward compatibility with 1.0 API
45
+ 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 }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def tags_to_s
56
+ tags_string = String.new
57
+ if tags
58
+ tags.each { |name, value| tags_string << " #{name}:#{value.inspect}" }
59
+ end
60
+ tags_string
61
+ end
37
62
  end
38
63
  end
@@ -30,52 +30,62 @@ module Lumberjack
30
30
  # The time that the device was last flushed.
31
31
  attr_reader :last_flushed_at
32
32
 
33
- # The name of the program associated with log messages.
33
+ # Set +silencer+ to false to disable silencing the log.
34
+ attr_accessor :silencer
35
+
36
+ # Set the name of the program to attach to log entries.
34
37
  attr_writer :progname
35
38
 
36
- # The device being written to.
39
+ # The device being written to
37
40
  attr_reader :device
38
41
 
39
- # Set +silencer+ to false to disable silencing the log.
40
- attr_accessor :silencer
41
-
42
42
  # Create a new logger to log to a Device.
43
43
  #
44
44
  # The +device+ argument can be in any one of several formats.
45
45
  #
46
46
  # If it is a Device object, that object will be used.
47
47
  # If it has a +write+ method, it will be wrapped in a Device::Writer class.
48
- # If it is <tt>:null</tt>, it will be a Null device that won't record any output.
48
+ # If it is :null, it will be a Null device that won't record any output.
49
49
  # Otherwise, it will be assumed to be file path and wrapped in a Device::LogFile class.
50
50
  #
51
51
  # This method can take the following options:
52
52
  #
53
- # * <tt>:level</tt> - The logging level below which messages will be ignored.
54
- # * <tt>:progname</tt> - The name of the program that will be recorded with each log entry.
55
- # * <tt>:flush_seconds</tt> - The maximum number of seconds between flush calls.
56
- # * <tt>:roll</tt> - If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
57
- # * <tt>:max_size</tt> - If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
53
+ # * :level - The logging level below which messages will be ignored.
54
+ # * :formatter - The formatter to use for outputting messages to the log.
55
+ # * :datetime_format - The format to use for log timestamps.
56
+ # * :progname - The name of the program that will be recorded with each log entry.
57
+ # * :flush_seconds - The maximum number of seconds between flush calls.
58
+ # * :roll - If the log device is a file path, it will be a Device::DateRollingLogFile if this is set.
59
+ # * :max_size - If the log device is a file path, it will be a Device::SizeRollingLogFile if this is set.
58
60
  #
59
61
  # All other options are passed to the device constuctor.
60
62
  def initialize(device = STDOUT, options = {})
61
- @thread_settings = {}
62
-
63
63
  options = options.dup
64
64
  self.level = options.delete(:level) || INFO
65
65
  self.progname = options.delete(:progname)
66
66
  max_flush_seconds = options.delete(:flush_seconds).to_f
67
67
 
68
- @device = open_device(device, options)
69
- @_formatter = Formatter.new
68
+ @device = open_device(device, options) if device
69
+ @_formatter = (options[:formatter] || Formatter.new)
70
+ time_format = (options[:datetime_format] || options[:time_format])
71
+ self.datetime_format = time_format if time_format
70
72
  @last_flushed_at = Time.now
71
73
  @silencer = true
74
+ @tags = {}
72
75
 
73
76
  create_flusher_thread(max_flush_seconds) if max_flush_seconds > 0
74
77
  end
75
78
 
76
- # Get the Formatter object used to convert messages into strings.
77
- def formatter
78
- @_formatter
79
+ # Get the timestamp format on the device if it has one.
80
+ def datetime_format
81
+ @device.datetime_format if @device.respond_to?(:datetime_format)
82
+ end
83
+
84
+ # Set the timestamp format on the device if it is supported.
85
+ def datetime_format=(format)
86
+ if @device.respond_to?(:datetime_format=)
87
+ @device.datetime_format = format
88
+ end
79
89
  end
80
90
 
81
91
  # Get the level of severity of entries that are logged. Entries with a lower
@@ -84,6 +94,30 @@ module Lumberjack
84
94
  thread_local_value(:lumberjack_logger_level) || @level
85
95
  end
86
96
 
97
+ alias_method :sev_threshold, :level
98
+
99
+ # Set the log level using either an integer level like Logger::INFO or a label like
100
+ # :info or "info"
101
+ def level=(value)
102
+ if value.is_a?(Integer)
103
+ @level = value
104
+ else
105
+ @level = Severity::label_to_level(value)
106
+ end
107
+ end
108
+
109
+ alias_method :sev_threshold=, :level=
110
+
111
+ # Set the Lumberjack::Formatter used to format objects for logging as messages.
112
+ def formatter=(value)
113
+ @_formatter = value
114
+ end
115
+
116
+ # Get the Lumberjack::Formatter used to format objects for logging as messages.
117
+ def formatter
118
+ @_formatter
119
+ end
120
+
87
121
  # Add a message to the log with a given severity. The message can be either
88
122
  # passed in the +message+ argument or supplied with a block. This method
89
123
  # is not normally called. Instead call one of the helper functions
@@ -94,36 +128,49 @@ module Lumberjack
94
128
  #
95
129
  # === Example
96
130
  #
97
- # logger.add(Lumberjack::Severity::ERROR, exception)
98
- # logger.add(Lumberjack::Severity::INFO, "Request completed")
99
- # logger.add(:warn, "Request took a long time")
100
- # logger.add(Lumberjack::Severity::DEBUG){"Start processing with options #{options.inspect}"}
101
- def add(severity, message = nil, progname = nil)
102
- severity = Severity.label_to_level(severity) if severity.is_a?(String) || severity.is_a?(Symbol)
131
+ # logger.add_entry(Logger::ERROR, exception)
132
+ # logger.add_entry(Logger::INFO, "Request completed")
133
+ # logger.add_entry(:warn, "Request took a long time")
134
+ # logger.add_entry(Logger::DEBUG){"Start processing with options #{options.inspect}"}
135
+ def add_entry(severity, message, progname = nil, tags = nil)
136
+ severity = Severity.label_to_level(severity) unless severity.is_a?(Integer)
103
137
 
104
- return unless severity && severity >= level
138
+ return true unless @device && severity && severity >= level
105
139
 
106
140
  time = Time.now
141
+ message = message.call if message.is_a?(Proc)
142
+ message = formatter.format(message)
143
+ progname ||= self.progname
144
+
145
+ current_tags = self.tags
146
+ tags = nil unless tags.is_a?(Hash)
147
+ if current_tags.empty?
148
+ tags = Tags.stringify_keys(tags) unless tags.nil?
149
+ else
150
+ if tags.nil?
151
+ tags = current_tags.dup
152
+ else
153
+ tags = current_tags.merge(Tags.stringify_keys(tags))
154
+ end
155
+ end
156
+
157
+ entry = LogEntry.new(time, severity, message, progname, $$, tags)
158
+ write_to_device(entry)
159
+
160
+ true
161
+ end
162
+
163
+ # ::Logger compatible method to add a log entry.
164
+ def add(severity, message = nil, progname = nil, &block)
107
165
  if message.nil?
108
- if block_given?
109
- message = yield
166
+ if block
167
+ message = block
110
168
  else
111
169
  message = progname
112
170
  progname = nil
113
171
  end
114
172
  end
115
-
116
- message = @_formatter.format(message)
117
- progname ||= self.progname
118
- entry = LogEntry.new(time, severity, message, progname, $$, Lumberjack.unit_of_work_id)
119
- begin
120
- device.write(entry)
121
- rescue => e
122
- $stderr.puts("#{e.class.name}: #{e.message}#{' at ' + e.backtrace.first if e.backtrace}")
123
- $stderr.puts(entry.to_s)
124
- end
125
-
126
- nil
173
+ add_entry(severity, message, progname)
127
174
  end
128
175
 
129
176
  alias_method :log, :add
@@ -141,9 +188,13 @@ module Lumberjack
141
188
  @device.close if @device.respond_to?(:close)
142
189
  end
143
190
 
191
+ def reopen(logdev = nil)
192
+ device.reopen(logdev) if device.respond_to?(:reopen)
193
+ end
194
+
144
195
  # Log a +FATAL+ message. The message can be passed in either the +message+ argument or in a block.
145
- def fatal(message = nil, progname = nil, &block)
146
- add(FATAL, message, progname, &block)
196
+ def fatal(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
197
+ call_add_entry(FATAL, message_or_progname_or_tags, progname_or_tags, &block)
147
198
  end
148
199
 
149
200
  # Return +true+ if +FATAL+ messages are being logged.
@@ -152,8 +203,8 @@ module Lumberjack
152
203
  end
153
204
 
154
205
  # Log an +ERROR+ message. The message can be passed in either the +message+ argument or in a block.
155
- def error(message = nil, progname = nil, &block)
156
- add(ERROR, message, progname, &block)
206
+ def error(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
207
+ call_add_entry(ERROR, message_or_progname_or_tags, progname_or_tags, &block)
157
208
  end
158
209
 
159
210
  # Return +true+ if +ERROR+ messages are being logged.
@@ -162,8 +213,8 @@ module Lumberjack
162
213
  end
163
214
 
164
215
  # Log a +WARN+ message. The message can be passed in either the +message+ argument or in a block.
165
- def warn(message = nil, progname = nil, &block)
166
- add(WARN, message, progname, &block)
216
+ def warn(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
217
+ call_add_entry(WARN, message_or_progname_or_tags, progname_or_tags, &block)
167
218
  end
168
219
 
169
220
  # Return +true+ if +WARN+ messages are being logged.
@@ -172,8 +223,8 @@ module Lumberjack
172
223
  end
173
224
 
174
225
  # Log an +INFO+ message. The message can be passed in either the +message+ argument or in a block.
175
- def info(message = nil, progname = nil, &block)
176
- add(INFO, message, progname, &block)
226
+ def info(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
227
+ call_add_entry(INFO, message_or_progname_or_tags, progname_or_tags, &block)
177
228
  end
178
229
 
179
230
  # Return +true+ if +INFO+ messages are being logged.
@@ -182,8 +233,8 @@ module Lumberjack
182
233
  end
183
234
 
184
235
  # Log a +DEBUG+ message. The message can be passed in either the +message+ argument or in a block.
185
- def debug(message = nil, progname = nil, &block)
186
- add(DEBUG, message, progname, &block)
236
+ def debug(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
237
+ call_add_entry(DEBUG, message_or_progname_or_tags, progname_or_tags, &block)
187
238
  end
188
239
 
189
240
  # Return +true+ if +DEBUG+ messages are being logged.
@@ -193,19 +244,12 @@ module Lumberjack
193
244
 
194
245
  # Log a message when the severity is not known. Unknown messages will always appear in the log.
195
246
  # The message can be passed in either the +message+ argument or in a block.
196
- def unknown(message = nil, progname = nil, &block)
197
- add(UNKNOWN, message, progname, &block)
247
+ def unknown(message_or_progname_or_tags = nil, progname_or_tags = nil, &block)
248
+ call_add_entry(UNKNOWN, message_or_progname_or_tags, progname_or_tags, &block)
198
249
  end
199
250
 
200
- alias_method :<<, :unknown
201
-
202
- # Set the minimum level of severity of messages to log.
203
- def level=(severity)
204
- if severity.is_a?(Integer)
205
- @level = severity
206
- else
207
- @level = Severity.label_to_level(severity)
208
- end
251
+ def <<(msg)
252
+ add_entry(UNKNOWN, msg)
209
253
  end
210
254
 
211
255
  # Silence the logger by setting a new log level inside a block. By default, only +ERROR+ or +FATAL+
@@ -213,7 +257,7 @@ module Lumberjack
213
257
  #
214
258
  # === Example
215
259
  #
216
- # logger.level = Lumberjack::Severity::INFO
260
+ # logger.level = Logger::INFO
217
261
  # logger.silence do
218
262
  # do_something # Log level inside the block is +ERROR+
219
263
  # end
@@ -240,8 +284,58 @@ module Lumberjack
240
284
  thread_local_value(:lumberjack_logger_progname) || @progname
241
285
  end
242
286
 
287
+ # Set a hash of tags on logger. If a block is given, the tags will only be set
288
+ # for the duration of the block.
289
+ def tag(tags, &block)
290
+ tags = Tags.stringify_keys(tags)
291
+ if block
292
+ thread_tags = thread_local_value(:lumberjack_logger_tags)
293
+ value = (thread_tags ? thread_tags.merge(tags) : tags)
294
+ push_thread_local_value(:lumberjack_logger_tags, value, &block)
295
+ else
296
+ @tags.merge!(tags)
297
+ end
298
+ end
299
+
300
+ # Return all tags in scope on the logger including global tags set on the Lumberjack
301
+ # context, tags set on the logger, and tags set on the current block for the logger
302
+ def tags
303
+ tags = {}
304
+ context_tags = Lumberjack.context_tags
305
+ tags.merge!(context_tags) if context_tags && !context_tags.empty?
306
+ tags.merge!(@tags) if !@tags.empty?
307
+ scope_tags = thread_local_value(:lumberjack_logger_tags)
308
+ tags.merge!(scope_tags) if scope_tags && !scope_tags.empty?
309
+ tags
310
+ end
311
+
243
312
  private
244
313
 
314
+ # Dereference arguments to log calls so we can have methods with compatibility with ::Logger
315
+ def call_add_entry(severity, message_or_progname_or_tags, progname_or_tags, &block) #:nodoc:
316
+ message = nil
317
+ progname = nil
318
+ tags = nil
319
+ if block
320
+ message = block
321
+ if message_or_progname_or_tags.is_a?(Hash)
322
+ tags = message_or_progname_or_tags
323
+ progname = progname_or_tags
324
+ else
325
+ progname = message_or_progname_or_tags
326
+ tags = progname_or_tags if progname_or_tags.is_a?(Hash)
327
+ end
328
+ else
329
+ message = message_or_progname_or_tags
330
+ if progname_or_tags.is_a?(Hash)
331
+ tags = progname_or_tags
332
+ else
333
+ progname = progname_or_tags
334
+ end
335
+ end
336
+ add_entry(severity, message, progname, tags)
337
+ end
338
+
245
339
  # Set a local value for a thread tied to this object.
246
340
  def set_thread_local_value(name, value) #:nodoc:
247
341
  values = Thread.current[name]
@@ -276,7 +370,9 @@ module Lumberjack
276
370
 
277
371
  # Open a logging device.
278
372
  def open_device(device, options) #:nodoc:
279
- if device.is_a?(Device)
373
+ if device.nil?
374
+ nil
375
+ elsif device.is_a?(Device)
280
376
  device
281
377
  elsif device.respond_to?(:write) && device.respond_to?(:flush)
282
378
  Device::Writer.new(device, options)
@@ -287,13 +383,22 @@ module Lumberjack
287
383
  if options[:roll]
288
384
  Device::DateRollingLogFile.new(device, options)
289
385
  elsif options[:max_size]
290
- Device::SizeRollingLogFile.new(device, options)
386
+ Device::SizeRollingLogFile.new(device, options)
291
387
  else
292
388
  Device::LogFile.new(device, options)
293
389
  end
294
390
  end
295
391
  end
296
392
 
393
+ def write_to_device(entry) #:nodoc:
394
+ begin
395
+ @device.write(entry)
396
+ rescue => e
397
+ $stderr.puts("#{e.class.name}: #{e.message}#{' at ' + e.backtrace.first if e.backtrace}")
398
+ $stderr.puts(entry.to_s)
399
+ end
400
+ end
401
+
297
402
  # Create a thread that will periodically call flush.
298
403
  def create_flusher_thread(flush_seconds) #:nodoc:
299
404
  if flush_seconds > 0