logging 2.0.0 → 2.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 (55) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -5
  4. data/History.txt +59 -0
  5. data/LICENSE +22 -0
  6. data/README.md +20 -41
  7. data/Rakefile +2 -2
  8. data/examples/appenders.rb +1 -1
  9. data/examples/layouts.rb +1 -1
  10. data/examples/lazy.rb +1 -1
  11. data/examples/mdc.rb +2 -2
  12. data/examples/rails4.rb +21 -0
  13. data/examples/reusing_layouts.rb +51 -0
  14. data/lib/logging.rb +99 -9
  15. data/lib/logging/appender.rb +13 -34
  16. data/lib/logging/appenders/buffering.rb +130 -59
  17. data/lib/logging/appenders/console.rb +68 -57
  18. data/lib/logging/appenders/file.rb +43 -22
  19. data/lib/logging/appenders/io.rb +22 -16
  20. data/lib/logging/appenders/rolling_file.rb +60 -26
  21. data/lib/logging/appenders/string_io.rb +1 -1
  22. data/lib/logging/appenders/syslog.rb +3 -4
  23. data/lib/logging/color_scheme.rb +1 -1
  24. data/lib/logging/diagnostic_context.rb +100 -73
  25. data/lib/logging/layout.rb +144 -16
  26. data/lib/logging/layouts/parseable.rb +50 -12
  27. data/lib/logging/layouts/pattern.rb +8 -9
  28. data/lib/logging/log_event.rb +19 -12
  29. data/lib/logging/logger.rb +117 -95
  30. data/lib/logging/proxy.rb +1 -1
  31. data/lib/logging/rails_compat.rb +4 -13
  32. data/lib/logging/version.rb +1 -1
  33. data/logging.gemspec +31 -32
  34. data/script/console +8 -0
  35. data/test/appenders/{test_periodic_flushing.rb → test_async_flushing.rb} +67 -14
  36. data/test/appenders/test_buffered_io.rb +19 -18
  37. data/test/appenders/test_console.rb +55 -12
  38. data/test/appenders/test_file.rb +48 -28
  39. data/test/appenders/test_rolling_file.rb +18 -12
  40. data/test/appenders/test_syslog.rb +6 -0
  41. data/test/benchmark.rb +42 -18
  42. data/test/layouts/test_json.rb +14 -1
  43. data/test/layouts/test_nested_exceptions.rb +124 -0
  44. data/test/layouts/test_pattern.rb +16 -3
  45. data/test/layouts/test_yaml.rb +15 -1
  46. data/test/performance.rb +66 -0
  47. data/test/setup.rb +26 -30
  48. data/test/test_appender.rb +2 -4
  49. data/test/test_layout.rb +49 -0
  50. data/test/test_log_event.rb +10 -2
  51. data/test/test_logger.rb +20 -3
  52. data/test/test_logging.rb +75 -4
  53. data/test/test_mapped_diagnostic_context.rb +15 -6
  54. data/test/test_nested_diagnostic_context.rb +6 -1
  55. metadata +23 -17
@@ -62,7 +62,7 @@ module Logging::Appenders
62
62
  %w[read readline readlines].each do|m|
63
63
  class_eval <<-CODE, __FILE__, __LINE__+1
64
64
  def #{m}( *args )
65
- sync {
65
+ @mutex.synchronize {
66
66
  begin
67
67
  @sio.seek @pos
68
68
  rv = @sio.#{m}(*args)
@@ -9,7 +9,7 @@ module Logging::Appenders
9
9
  # Accessor / Factory for the Syslog appender.
10
10
  #
11
11
  def self.syslog( *args )
12
- return ::Logging::Appenders::Syslog if args.empty?
12
+ fail ArgumentError, '::Logging::Appenders::Syslog needs a name as first argument.' if args.empty?
13
13
  ::Logging::Appenders::Syslog.new(*args)
14
14
  end
15
15
 
@@ -188,7 +188,7 @@ module Logging::Appenders
188
188
  end
189
189
  return if message.empty?
190
190
 
191
- @syslog.log(pri, '%s', message)
191
+ @mutex.synchronize { @syslog.log(pri, '%s', message) }
192
192
  self
193
193
  end
194
194
 
@@ -205,11 +205,10 @@ module Logging::Appenders
205
205
  level = level.to_s.upcase
206
206
  self.class.const_get level
207
207
  else
208
- raise ArgumentError, "unkonwn level '#{level}'"
208
+ raise ArgumentError, "unknown level '#{level}'"
209
209
  end
210
210
  end
211
211
 
212
212
  end # Syslog
213
213
  end # Logging::Appenders
214
214
  end # HAVE_SYSLOG
215
-
@@ -40,7 +40,7 @@ module Logging
40
40
  # Store a color scheme by name.
41
41
  #
42
42
  def []=( name, value )
43
- raise ArgumentError, "Silly! That's not a ColorSchmeme!" unless ColorScheme === value
43
+ raise ArgumentError, "Silly! That's not a ColorSchmeme!" unless value.is_a?(ColorScheme)
44
44
  @color_schemes[name.to_s] = value
45
45
  end
46
46
 
@@ -110,7 +110,7 @@ module Logging
110
110
  # Returns nil or the Hash removed from the stack.
111
111
  #
112
112
  def pop
113
- return unless Thread.current[STACK_NAME]
113
+ return unless Thread.current.thread_variable_get(STACK_NAME)
114
114
  return unless stack.length > 1
115
115
  clear_context
116
116
  stack.pop
@@ -125,7 +125,7 @@ module Logging
125
125
  #
126
126
  def clear
127
127
  clear_context
128
- Thread.current[STACK_NAME] = nil
128
+ Thread.current.thread_variable_set(STACK_NAME, nil)
129
129
  self
130
130
  end
131
131
 
@@ -140,15 +140,14 @@ module Logging
140
140
  def inherit( obj )
141
141
  case obj
142
142
  when Hash
143
- Thread.current[STACK_NAME] = [obj.dup]
143
+ Thread.current.thread_variable_set(STACK_NAME, [obj.dup])
144
144
  when Thread
145
145
  return if Thread.current == obj
146
- Thread.exclusive {
147
- if obj[STACK_NAME]
148
- hash = flatten(obj[STACK_NAME])
149
- Thread.current[STACK_NAME] = [hash]
146
+ DIAGNOSTIC_MUTEX.synchronize do
147
+ if hash = obj.thread_variable_get(STACK_NAME)
148
+ Thread.current.thread_variable_set(STACK_NAME, [flatten(hash)])
150
149
  end
151
- }
150
+ end
152
151
  end
153
152
 
154
153
  self
@@ -159,12 +158,18 @@ module Logging
159
158
  # application.
160
159
  #
161
160
  def context
162
- c = Thread.current[NAME]
163
- return c unless c.nil?
161
+ c = Thread.current.thread_variable_get(NAME)
164
162
 
165
- return Thread.current[NAME] = {} unless Thread.current[STACK_NAME]
163
+ if c.nil?
164
+ c = if Thread.current.thread_variable_get(STACK_NAME)
165
+ flatten(stack)
166
+ else
167
+ Hash.new
168
+ end
169
+ Thread.current.thread_variable_set(NAME, c)
170
+ end
166
171
 
167
- Thread.current[NAME] = flatten(stack)
172
+ return c
168
173
  end
169
174
 
170
175
  # Returns the stack of Hash objects that are storing the diagnostic
@@ -172,7 +177,12 @@ module Logging
172
177
  # one Hash.
173
178
  #
174
179
  def stack
175
- Thread.current[STACK_NAME] ||= [{}]
180
+ s = Thread.current.thread_variable_get(STACK_NAME)
181
+ if s.nil?
182
+ s = [{}]
183
+ Thread.current.thread_variable_set(STACK_NAME, s)
184
+ end
185
+ return s
176
186
  end
177
187
 
178
188
  # Returns the most current Hash from the stack of contexts.
@@ -184,7 +194,7 @@ module Logging
184
194
  # Remove the flattened context.
185
195
  #
186
196
  def clear_context
187
- Thread.current[NAME] = nil
197
+ Thread.current.thread_variable_set(NAME, nil)
188
198
  self
189
199
  end
190
200
 
@@ -199,7 +209,7 @@ module Logging
199
209
  # Raises an ArgumentError if hash is not a Hash.
200
210
  #
201
211
  def sanitize( hash, target = {} )
202
- unless Hash === hash
212
+ unless hash.is_a?(Hash)
203
213
  raise ArgumentError, "Expecting a Hash but received a #{hash.class.name}"
204
214
  end
205
215
 
@@ -320,7 +330,7 @@ module Logging
320
330
  # Returns the NestedDiagnosticContext.
321
331
  #
322
332
  def clear
323
- Thread.current[NAME] = nil
333
+ Thread.current.thread_variable_set(NAME, nil)
324
334
  self
325
335
  end
326
336
 
@@ -335,12 +345,12 @@ module Logging
335
345
  def inherit( obj )
336
346
  case obj
337
347
  when Array
338
- Thread.current[NAME] = obj.dup
348
+ Thread.current.thread_variable_set(NAME, obj.dup)
339
349
  when Thread
340
350
  return if Thread.current == obj
341
- Thread.exclusive {
342
- Thread.current[NAME] = obj[NAME].dup if obj[NAME]
343
- }
351
+ DIAGNOSTIC_MUTEX.synchronize do
352
+ Thread.current.thread_variable_set(NAME, obj.thread_variable_get(NAME).dup) if obj.thread_variable_get(NAME)
353
+ end
344
354
  end
345
355
 
346
356
  self
@@ -351,7 +361,12 @@ module Logging
351
361
  # running in the application.
352
362
  #
353
363
  def context
354
- Thread.current[NAME] ||= Array.new
364
+ c = Thread.current.thread_variable_get(NAME)
365
+ if c.nil?
366
+ c = Array.new
367
+ Thread.current.thread_variable_set(NAME, c)
368
+ end
369
+ return c
355
370
  end
356
371
  end # NestedDiagnosticContext
357
372
 
@@ -381,13 +396,13 @@ module Logging
381
396
  #
382
397
  def self.clear_diagnostic_contexts( all = false )
383
398
  if all
384
- Thread.exclusive {
385
- Thread.list.each { |thread|
386
- thread[MappedDiagnosticContext::NAME] = nil if thread[MappedDiagnosticContext::NAME]
387
- thread[NestedDiagnosticContext::NAME] = nil if thread[NestedDiagnosticContext::NAME]
388
- thread[MappedDiagnosticContext::STACK_NAME] = nil if thread[MappedDiagnosticContext::STACK_NAME]
389
- }
390
- }
399
+ DIAGNOSTIC_MUTEX.synchronize do
400
+ Thread.list.each do |t|
401
+ t.thread_variable_set(MappedDiagnosticContext::NAME, nil) if t.thread_variable?(MappedDiagnosticContext::NAME)
402
+ t.thread_variable_set(NestedDiagnosticContext::NAME, nil) if t.thread_variable?(NestedDiagnosticContext::NAME)
403
+ t.thread_variable_set(MappedDiagnosticContext::STACK_NAME, nil) if t.thread_variable?(MappedDiagnosticContext::STACK_NAME)
404
+ end
405
+ end
391
406
  else
392
407
  MappedDiagnosticContext.clear
393
408
  NestedDiagnosticContext.clear
@@ -396,60 +411,72 @@ module Logging
396
411
  self
397
412
  end
398
413
 
399
- end # module Logging
400
-
414
+ DIAGNOSTIC_MUTEX = Mutex.new
415
+ end
401
416
 
402
417
  # :stopdoc:
403
- class Thread
404
- class << self
405
-
406
- %w[new start fork].each do |m|
407
- class_eval <<-__, __FILE__, __LINE__
408
- alias_method :_orig_#{m}, :#{m}
409
- private :_orig_#{m}
410
- def #{m}( *a, &b )
411
- create_with_logging_context(:_orig_#{m}, *a ,&b)
412
- end
413
- __
414
- end
415
-
416
- private
418
+ Logging::INHERIT_CONTEXT =
419
+ if ENV.key?("LOGGING_INHERIT_CONTEXT")
420
+ case ENV["LOGGING_INHERIT_CONTEXT"].downcase
421
+ when 'false', 'no', '0'; false
422
+ when false, nil; false
423
+ else true end
424
+ else
425
+ true
426
+ end
417
427
 
418
- # In order for the diagnostic contexts to behave properly we need to
419
- # inherit state from the parent thread. The only way I have found to do
420
- # this in Ruby is to override `new` and capture the contexts from the
421
- # parent Thread at the time the child Thread is created. The code below does
422
- # just this. If there is a more idiomatic way of accomplishing this in Ruby,
423
- # please let me know!
424
- #
425
- # Also, great care is taken in this code to ensure that a reference to the
426
- # parent thread does not exist in the binding associated with the block
427
- # being executed in the child thread. The same is true for the parent
428
- # thread's mdc and ndc. If any of those references end up in the binding,
429
- # then they cannot be garbage collected until the child thread exits.
430
- #
431
- def create_with_logging_context( m, *a, &b )
432
- mdc, ndc = nil
428
+ if Logging::INHERIT_CONTEXT
429
+ class Thread
430
+ class << self
433
431
 
434
- if Thread.current[Logging::MappedDiagnosticContext::STACK_NAME]
435
- mdc = Logging::MappedDiagnosticContext.context.dup
432
+ %w[new start fork].each do |m|
433
+ class_eval <<-__, __FILE__, __LINE__
434
+ alias_method :_orig_#{m}, :#{m}
435
+ private :_orig_#{m}
436
+ def #{m}( *a, &b )
437
+ create_with_logging_context(:_orig_#{m}, *a ,&b)
438
+ end
439
+ __
436
440
  end
437
441
 
438
- if Thread.current[Logging::NestedDiagnosticContext::NAME]
439
- ndc = Logging::NestedDiagnosticContext.context.dup
442
+ private
443
+
444
+ # In order for the diagnostic contexts to behave properly we need to
445
+ # inherit state from the parent thread. The only way I have found to do
446
+ # this in Ruby is to override `new` and capture the contexts from the
447
+ # parent Thread at the time the child Thread is created. The code below does
448
+ # just this. If there is a more idiomatic way of accomplishing this in Ruby,
449
+ # please let me know!
450
+ #
451
+ # Also, great care is taken in this code to ensure that a reference to the
452
+ # parent thread does not exist in the binding associated with the block
453
+ # being executed in the child thread. The same is true for the parent
454
+ # thread's mdc and ndc. If any of those references end up in the binding,
455
+ # then they cannot be garbage collected until the child thread exits.
456
+ #
457
+ def create_with_logging_context( m, *a, &b )
458
+ mdc, ndc = nil
459
+
460
+ if Thread.current.thread_variable_get(Logging::MappedDiagnosticContext::STACK_NAME)
461
+ mdc = Logging::MappedDiagnosticContext.context.dup
462
+ end
463
+
464
+ if Thread.current.thread_variable_get(Logging::NestedDiagnosticContext::NAME)
465
+ ndc = Logging::NestedDiagnosticContext.context.dup
466
+ end
467
+
468
+ # This calls the actual `Thread#new` method to create the Thread instance.
469
+ # If your memory profiling tool says this method is leaking memory, then
470
+ # you are leaking Thread instances somewhere.
471
+ self.send(m, *a) { |*args|
472
+ Logging::MappedDiagnosticContext.inherit(mdc)
473
+ Logging::NestedDiagnosticContext.inherit(ndc)
474
+ b.call(*args)
475
+ }
440
476
  end
441
477
 
442
- # This calls the actual `Thread#new` method to create the Thread instance.
443
- # If your memory profiling tool says this method is leaking memory, then
444
- # you are leaking Thread instances somewhere.
445
- self.send(m, *a) { |*args|
446
- Logging::MappedDiagnosticContext.inherit(mdc)
447
- Logging::NestedDiagnosticContext.inherit(ndc)
448
- b.call(*args)
449
- }
450
478
  end
451
-
452
479
  end
453
- end # Thread
480
+ end
454
481
  # :startdoc:
455
482
 
@@ -41,13 +41,86 @@ class Layout
41
41
  when :inspect, :yaml, :json; f
42
42
  else :string end
43
43
 
44
- b = opts.fetch(:backtrace, ::Logging.backtrace)
45
- @backtrace = case b
46
- when :on, 'on', true; true
47
- when :off, 'off', false; false
48
- else
49
- raise ArgumentError, "backtrace must be true or false"
50
- end
44
+ self.backtrace = opts.fetch(:backtrace, ::Logging.backtrace)
45
+ self.utc_offset = opts.fetch(:utc_offset, ::Logging.utc_offset)
46
+ self.cause_depth = opts.fetch(:cause_depth, ::Logging.cause_depth)
47
+ end
48
+
49
+ # call-seq:
50
+ # layout.backtrace = true
51
+ #
52
+ # Set the backtrace flag to the given value. This can be set to `true` or
53
+ # `false`.
54
+ #
55
+ def backtrace=( value )
56
+ @backtrace = case value
57
+ when :on, 'on', true; true
58
+ when :off, 'off', false; false
59
+ else
60
+ raise ArgumentError, "backtrace must be `true` or `false`"
61
+ end
62
+ end
63
+
64
+ # Returns the backtrace setting.
65
+ attr_reader :backtrace
66
+ alias :backtrace? :backtrace
67
+
68
+ # Set the UTC offset used when formatting time values. If left unset, the
69
+ # default local time zone will be used for time values. This method accepts
70
+ # the `utc_offset` format supported by the `Time#localtime` method in Ruby.
71
+ #
72
+ # Passing "UTC" or `0` as the UTC offset will cause all times to be reported
73
+ # in the UTC timezone.
74
+ #
75
+ # layout.utc_offset = "-07:00" # Mountain Standard Time in North America
76
+ # layout.utc_offset = "+01:00" # Central European Time
77
+ # layout.utc_offset = "UTC" # UTC
78
+ # layout.utc_offset = 0 # UTC
79
+ #
80
+ def utc_offset=( value )
81
+ @utc_offset = case value
82
+ when nil; nil
83
+ when "UTC", "GMT", 0; 0
84
+ else
85
+ Time.now.localtime(value)
86
+ value
87
+ end
88
+ end
89
+
90
+ # Returns the UTC offset.
91
+ attr_reader :utc_offset
92
+
93
+ #
94
+ #
95
+ def cause_depth=( value )
96
+ if value.nil?
97
+ @cause_depth = ::Logging::DEFAULT_CAUSE_DEPTH
98
+ else
99
+ value = Integer(value)
100
+ @cause_depth = value < 0 ? ::Logging::DEFAULT_CAUSE_DEPTH : value
101
+ end
102
+ end
103
+
104
+ # Returns the exception cause depth formatting limit.
105
+ attr_reader :cause_depth
106
+
107
+ # Internal: Helper method that applies the UTC offset to the given `time`
108
+ # instance. A new Time is returned that is equivalent to the original `time`
109
+ # but pinned to the timezone given by the UTC offset.
110
+ #
111
+ # If a UTC offset has not been set, then the original `time` instance is
112
+ # returned unchanged.
113
+ #
114
+ def apply_utc_offset( time )
115
+ return time if utc_offset.nil?
116
+
117
+ time = time.dup
118
+ if utc_offset == 0
119
+ time.utc
120
+ else
121
+ time.localtime(utc_offset)
122
+ end
123
+ time
51
124
  end
52
125
 
53
126
  # call-seq:
@@ -84,11 +157,10 @@ class Layout
84
157
  case obj
85
158
  when String; obj
86
159
  when Exception
87
- str = "<#{obj.class.name}> #{obj.message}"
88
- if @backtrace && !obj.backtrace.nil?
89
- str << "\n\t" << obj.backtrace.join("\n\t")
90
- end
91
- str
160
+ lines = ["<#{obj.class.name}> #{obj.message}"]
161
+ lines.concat(obj.backtrace) if backtrace? && obj.backtrace
162
+ format_cause(obj, lines)
163
+ lines.join("\n\t")
92
164
  when nil; "<#{obj.class.name}> nil"
93
165
  else
94
166
  str = "<#{obj.class.name}> "
@@ -101,6 +173,64 @@ class Layout
101
173
  end
102
174
  end
103
175
 
176
+ # Internal: Format any nested exceptions found in the given exception `e`
177
+ # while respecting the maximum `cause_depth`. The lines array is used to
178
+ # capture all the output lines form the nested exceptions; the array is later
179
+ # joined by the `format_obj` method.
180
+ #
181
+ # e - Exception to format
182
+ # lines - Array of output lines
183
+ #
184
+ # Returns the input `lines` Array
185
+ def format_cause(e, lines)
186
+ return lines if cause_depth == 0
187
+
188
+ cause_depth.times do
189
+ break unless e.respond_to?(:cause) && e.cause
190
+
191
+ cause = e.cause
192
+ lines << "--- Caused by ---"
193
+ lines << "<#{cause.class.name}> #{cause.message}"
194
+ lines.concat(format_cause_backtrace(e, cause)) if backtrace? && cause.backtrace
195
+
196
+ e = cause
197
+ end
198
+
199
+ if e.respond_to?(:cause) && e.cause
200
+ lines << "--- Further #cause backtraces were omitted ---"
201
+ end
202
+
203
+ lines
204
+ end
205
+
206
+ # Internal: Format the backtrace of the nested `cause` but remove the common
207
+ # exception lines from the parent exception. This helps keep the backtraces a
208
+ # wee bit shorter and more comprehensible.
209
+ #
210
+ # e - parent exception
211
+ # cause - the nested exception generating the returned backtrace
212
+ #
213
+ # Returns an Array of backtracke lines.
214
+ def format_cause_backtrace(e, cause)
215
+ # Find where the cause's backtrace differs from the parent exception's.
216
+ backtrace = Array(e.backtrace)
217
+ cause_backtrace = Array(cause.backtrace)
218
+ index = -1
219
+ min_index = [backtrace.size, cause_backtrace.size].min * -1
220
+ just_in_case = -5000
221
+
222
+ while index > min_index && backtrace[index] == cause_backtrace[index] && index >= just_in_case
223
+ index -= 1
224
+ end
225
+
226
+ # Add on a few common frames to make it clear where the backtraces line up.
227
+ index += 3
228
+ index = -1 if index >= 0
229
+
230
+ cause_backtrace[0..index]
231
+ end
232
+
233
+
104
234
  # Attempt to format the _obj_ using yaml, but fall back to inspect style
105
235
  # formatting if yaml fails.
106
236
  #
@@ -126,7 +256,5 @@ class Layout
126
256
  rescue StandardError
127
257
  obj.inspect
128
258
  end
129
-
130
- end # class Layout
131
- end # module Logging
132
-
259
+ end
260
+ end