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
@@ -1,81 +1,92 @@
1
-
2
1
  module Logging::Appenders
3
2
 
4
- # Accessor / Factory for the Stdout appender.
5
- #
6
- def self.stdout( *args )
7
- if args.empty?
8
- return self['stdout'] || ::Logging::Appenders::Stdout.new
9
- end
10
- ::Logging::Appenders::Stdout.new(*args)
11
- end
12
-
13
- # This class provides an Appender that can write to STDOUT.
14
- #
15
- class Stdout < ::Logging::Appenders::IO
3
+ # This class is provides an Appender base class for writing to the standard IO
4
+ # stream - STDOUT and STDERR. This class should not be instantiated directly.
5
+ # The `Stdout` and `Stderr` subclasses should be used.
6
+ class Console < ::Logging::Appenders::IO
16
7
 
17
8
  # call-seq:
18
9
  # Stdout.new( name = 'stdout' )
19
- # Stdout.new( :layout => layout )
10
+ # Stderr.new( :layout => layout )
20
11
  # Stdout.new( name = 'stdout', :level => 'info' )
21
12
  #
22
- # Creates a new Stdout Appender. The name 'stdout' will be used unless
23
- # another is given. Optionally, a layout can be given for the appender
24
- # to use (otherwise a basic appender will be created) and a log level
25
- # can be specified.
13
+ # Creates a new Stdout/Stderr Appender. The name 'stdout'/'stderr' will be
14
+ # used unless another is given. Optionally, a layout can be given for the
15
+ # appender to use (otherwise a basic appender will be created) and a log
16
+ # level can be specified.
26
17
  #
27
18
  # Options:
28
19
  #
29
- # :layout => the layout to use when formatting log events
30
- # :level => the level at which to log
20
+ # :layout => the layout to use when formatting log events
21
+ # :level => the level at which to log
31
22
  #
32
23
  def initialize( *args )
33
- opts = Hash === args.last ? args.pop : {}
34
- name = args.empty? ? 'stdout' : args.shift
24
+ name = self.class.name.split("::").last.downcase
25
+
26
+ opts = args.last.is_a?(Hash) ? args.pop : {}
27
+ name = args.shift unless args.empty?
35
28
 
36
- opts[:encoding] = STDOUT.external_encoding if STDOUT.respond_to? :external_encoding
29
+ io = open_fd
30
+ opts[:encoding] = io.external_encoding
37
31
 
38
- super(name, STDOUT, opts)
32
+ super(name, io, opts)
39
33
  end
40
- end # Stdout
41
34
 
35
+ # Reopen the connection to the underlying logging destination. If the
36
+ # connection is currently closed then it will be opened. If the connection
37
+ # is currently open then it will be closed and immediately reopened.
38
+ def reopen
39
+ @mutex.synchronize {
40
+ if defined? @io && @io
41
+ flush
42
+ @io.close rescue nil
43
+ end
44
+ @io = open_fd
45
+ }
46
+ super
47
+ self
48
+ end
42
49
 
43
- # Accessor / Factory for the Stderr appender.
44
- #
45
- def self.stderr( *args )
46
- if args.empty?
47
- return self['stderr'] || ::Logging::Appenders::Stderr.new
50
+ private
51
+
52
+ def open_fd
53
+ case self.class.name
54
+ when "Logging::Appenders::Stdout"
55
+ fd = STDOUT.fileno
56
+ encoding = STDOUT.external_encoding
57
+ when "Logging::Appenders::Stderr"
58
+ fd = STDERR.fileno
59
+ encoding = STDERR.external_encoding
60
+ else
61
+ raise RuntimeError, "Please do not use the `Logging::Appenders::Console` class directly - " +
62
+ "use `Logging::Appenders::Stdout` and `Logging::Appenders::Stderr` instead" +
63
+ " [class #{self.class.name}]"
64
+ end
65
+
66
+ mode = ::File::WRONLY | ::File::APPEND
67
+ ::IO.for_fd(fd, mode: mode, encoding: encoding)
48
68
  end
49
- ::Logging::Appenders::Stderr.new(*args)
50
69
  end
51
70
 
52
- # This class provides an Appender that can write to STDERR.
53
- #
54
- class Stderr < ::Logging::Appenders::IO
55
-
56
- # call-seq:
57
- # Stderr.new( name = 'stderr' )
58
- # Stderr.new( :layout => layout )
59
- # Stderr.new( name = 'stderr', :level => 'warn' )
60
- #
61
- # Creates a new Stderr Appender. The name 'stderr' will be used unless
62
- # another is given. Optionally, a layout can be given for the appender
63
- # to use (otherwise a basic appender will be created) and a log level
64
- # can be specified.
65
- #
66
- # Options:
67
- #
68
- # :layout => the layout to use when formatting log events
69
- # :level => the level at which to log
70
- #
71
- def initialize( *args )
72
- opts = Hash === args.last ? args.pop : {}
73
- name = args.empty? ? 'stderr' : args.shift
71
+ # This class provides an Appender that can write to STDOUT.
72
+ Stdout = Class.new(Console)
74
73
 
75
- opts[:encoding] = STDERR.external_encoding if STDERR.respond_to? :external_encoding
74
+ # This class provides an Appender that can write to STDERR.
75
+ Stderr = Class.new(Console)
76
76
 
77
- super(name, STDERR, opts)
77
+ # Accessor / Factory for the Stdout appender.
78
+ def self.stdout( *args )
79
+ if args.empty?
80
+ return self['stdout'] || ::Logging::Appenders::Stdout.new
78
81
  end
79
- end # Stderr
80
- end # Logging::Appenders
82
+ ::Logging::Appenders::Stdout.new(*args)
83
+ end
81
84
 
85
+ # Accessor / Factory for the Stderr appender.
86
+ def self.stderr( *args )
87
+ if args.empty?
88
+ return self['stderr'] || ::Logging::Appenders::Stderr.new
89
+ end
90
+ ::Logging::Appenders::Stderr.new(*args)
91
+ end
92
+ end
@@ -2,14 +2,12 @@
2
2
  module Logging::Appenders
3
3
 
4
4
  # Accessor / Factory for the File appender.
5
- #
6
5
  def self.file( *args )
7
- return ::Logging::Appenders::File if args.empty?
6
+ fail ArgumentError, '::Logging::Appenders::File needs a name as first argument.' if args.empty?
8
7
  ::Logging::Appenders::File.new(*args)
9
8
  end
10
9
 
11
10
  # This class provides an Appender that can write to a File.
12
- #
13
11
  class File < ::Logging::Appenders::IO
14
12
 
15
13
  # call-seq:
@@ -21,15 +19,14 @@ module Logging::Appenders
21
19
  # writable.
22
20
  #
23
21
  # An +ArgumentError+ is raised if any of these assertions fail.
24
- #
25
22
  def self.assert_valid_logfile( fn )
26
23
  if ::File.exist?(fn)
27
- if not ::File.file?(fn)
24
+ if !::File.file?(fn)
28
25
  raise ArgumentError, "#{fn} is not a regular file"
29
- elsif not ::File.writable?(fn)
26
+ elsif !::File.writable?(fn)
30
27
  raise ArgumentError, "#{fn} is not writeable"
31
28
  end
32
- elsif not ::File.writable?(::File.dirname(fn))
29
+ elsif !::File.writable?(::File.dirname(fn))
33
30
  raise ArgumentError, "#{::File.dirname(fn)} is not writable"
34
31
  end
35
32
  true
@@ -45,41 +42,65 @@ module Logging::Appenders
45
42
  # created. If the :truncate option is set to +true+ then the file will
46
43
  # be truncated before writing begins; otherwise, log messages will be
47
44
  # appended to the file.
48
- #
49
45
  def initialize( name, opts = {} )
50
- @fn = opts.fetch(:filename, name)
51
- raise ArgumentError, 'no filename was given' if @fn.nil?
46
+ @filename = opts.fetch(:filename, name)
47
+ raise ArgumentError, 'no filename was given' if @filename.nil?
52
48
 
53
- @fn = ::File.expand_path(@fn)
54
- self.class.assert_valid_logfile(@fn)
55
- @mode = opts.fetch(:truncate, false) ? 'w' : 'a'
49
+ @filename = ::File.expand_path(@filename).freeze
50
+ self.class.assert_valid_logfile(@filename)
56
51
 
57
52
  self.encoding = opts.fetch(:encoding, self.encoding)
58
- @mode = "#{@mode}:#{self.encoding}" if self.encoding
59
53
 
60
- super(name, ::File.new(@fn, @mode), opts)
54
+ io = open_file
55
+ super(name, io, opts)
56
+
57
+ truncate if opts.fetch(:truncate, false)
61
58
  end
62
59
 
63
60
  # Returns the path to the logfile.
64
- #
65
- def filename() @fn.dup end
61
+ attr_reader :filename
66
62
 
67
63
  # Reopen the connection to the underlying logging destination. If the
68
64
  # connection is currently closed then it will be opened. If the connection
69
65
  # is currently open then it will be closed and immediately opened.
70
- #
71
66
  def reopen
72
67
  @mutex.synchronize {
73
- if defined? @io and @io
68
+ if defined? @io && @io
74
69
  flush
75
70
  @io.close rescue nil
76
71
  end
77
- @io = ::File.new(@fn, @mode)
72
+ @io = open_file
78
73
  }
79
74
  super
80
75
  self
81
76
  end
82
77
 
83
- end # FileAppender
84
- end # Logging::Appenders
85
78
 
79
+ protected
80
+
81
+ def truncate
82
+ @mutex.synchronize {
83
+ begin
84
+ @io.flock(::File::LOCK_EX)
85
+ @io.truncate(0)
86
+ ensure
87
+ @io.flock(::File::LOCK_UN)
88
+ end
89
+ }
90
+ end
91
+
92
+ def open_file
93
+ mode = ::File::WRONLY | ::File::APPEND
94
+ ::File.open(filename, mode: mode, external_encoding: encoding)
95
+ rescue Errno::ENOENT
96
+ create_file
97
+ end
98
+
99
+ def create_file
100
+ mode = ::File::WRONLY | ::File::APPEND | ::File::CREAT | ::File::EXCL
101
+ ::File.open(filename, mode: mode, external_encoding: encoding)
102
+ rescue Errno::EEXIST
103
+ open_file
104
+ end
105
+ end
106
+ end
@@ -2,7 +2,6 @@
2
2
  module Logging::Appenders
3
3
 
4
4
  # Accessor / Factory for the IO appender.
5
- #
6
5
  def self.io( *args )
7
6
  return ::Logging::Appenders::IO if args.empty?
8
7
  ::Logging::Appenders::IO.new(*args)
@@ -10,14 +9,12 @@ module Logging::Appenders
10
9
 
11
10
  # This class provides an Appender that can write to any IO stream
12
11
  # configured for writing.
13
- #
14
12
  class IO < ::Logging::Appender
15
13
  include Buffering
16
14
 
17
15
  # The method that will be used to close the IO stream. Defaults to :close
18
16
  # but can be :close_read, :close_write or nil. When nil, the IO stream
19
17
  # will not be closed when the appender's close method is called.
20
- #
21
18
  attr_accessor :close_method
22
19
 
23
20
  # call-seq:
@@ -26,15 +23,13 @@ module Logging::Appenders
26
23
  #
27
24
  # Creates a new IO Appender using the given name that will use the _io_
28
25
  # stream as the logging destination.
29
- #
30
26
  def initialize( name, io, opts = {} )
31
- unless io.respond_to? :syswrite
27
+ unless io.respond_to? :write
32
28
  raise TypeError, "expecting an IO object but got '#{io.class.name}'"
33
29
  end
34
30
 
35
31
  @io = io
36
- @io.sync = true if io.respond_to? :sync= # syswrite complains if the IO stream is buffered
37
- @io.flush rescue nil # syswrite also complains if in unbuffered mode and buffer isn't empty
32
+ @io.sync = true if io.respond_to? :sync=
38
33
  @close_method = :close
39
34
 
40
35
  super(name, opts)
@@ -48,37 +43,48 @@ module Logging::Appenders
48
43
  # destination if the _footer_ flag is set to +true+. Log events will
49
44
  # no longer be written to the logging destination after the appender
50
45
  # is closed.
51
- #
52
46
  def close( *args )
53
47
  return self if @io.nil?
54
48
  super
55
49
 
56
50
  io, @io = @io, nil
57
51
  unless [STDIN, STDERR, STDOUT].include?(io)
58
- io.send(@close_method) if @close_method and io.respond_to? @close_method
52
+ io.send(@close_method) if @close_method && io.respond_to?(@close_method)
59
53
  end
60
54
  rescue IOError
61
55
  ensure
62
56
  return self
63
57
  end
64
58
 
59
+ # Reopen the connection to the underlying logging destination. If the
60
+ # connection is currently closed then it will be opened. If the connection
61
+ # is currently open then it will be closed and immediately opened. If
62
+ # supported, the IO will have its sync mode set to `true` so that all writes
63
+ # are immediately flushed to the underlying operating system.
64
+ def reopen
65
+ super
66
+ @io.sync = true if @io.respond_to? :sync=
67
+ self
68
+ end
65
69
 
66
70
  private
67
71
 
68
72
  # This method is called by the buffering code when messages need to be
69
73
  # written to the logging destination.
70
- #
71
74
  def canonical_write( str )
72
75
  return self if @io.nil?
73
- str = str.force_encoding(encoding) if encoding and str.encoding != encoding
74
- @io.syswrite str
76
+ str = str.force_encoding(encoding) if encoding && str.encoding != encoding
77
+ @mutex.synchronize { @io.write str }
75
78
  self
76
79
  rescue StandardError => err
80
+ handle_internal_error(err)
81
+ end
82
+
83
+ def handle_internal_error( err )
84
+ return err if off?
77
85
  self.level = :off
78
86
  ::Logging.log_internal {"appender #{name.inspect} has been disabled"}
79
87
  ::Logging.log_internal_error(err)
80
88
  end
81
-
82
- end # IO
83
- end # Logging::Appenders
84
-
89
+ end
90
+ end
@@ -2,7 +2,7 @@ module Logging::Appenders
2
2
 
3
3
  # Accessor / Factory for the RollingFile appender.
4
4
  def self.rolling_file( *args )
5
- return ::Logging::Appenders::RollingFile if args.empty?
5
+ fail ArgumentError, '::Logging::Appenders::RollingFile needs a name as first argument.' if args.empty?
6
6
  ::Logging::Appenders::RollingFile.new(*args)
7
7
  end
8
8
 
@@ -84,24 +84,32 @@ module Logging::Appenders
84
84
  # 'date'.
85
85
  #
86
86
  def initialize( name, opts = {} )
87
- @roller = Roller.new name, opts
87
+ @roller = Roller.new(
88
+ opts.fetch(:filename, name),
89
+ age: opts.fetch(:age, nil),
90
+ size: opts.fetch(:size, nil),
91
+ roll_by: opts.fetch(:roll_by, nil),
92
+ keep: opts.fetch(:keep, nil)
93
+ )
88
94
 
89
95
  # grab our options
90
96
  @size = opts.fetch(:size, nil)
91
97
  @size = Integer(@size) unless @size.nil?
92
98
 
93
- @age_fn = filename + '.age'
99
+ @age_fn = self.filename + '.age'
94
100
  @age_fn_mtime = nil
95
101
  @age = opts.fetch(:age, nil)
96
102
 
97
103
  # create our `sufficiently_aged?` method
98
104
  build_singleton_methods
99
- FileUtils.touch(@age_fn) if @age && !test(?f, @age_fn)
105
+ FileUtils.touch(@age_fn) if @age && !::File.file?(@age_fn)
100
106
 
101
107
  # we are opening the file in read/write mode so that a shared lock can
102
108
  # be used on the file descriptor => http://pubs.opengroup.org/onlinepubs/009695399/functions/fcntl.html
103
- @mode = encoding ? "a+:#{encoding}" : 'a+'
104
- super(name, ::File.new(filename, @mode), opts)
109
+ self.encoding = opts.fetch(:encoding, self.encoding)
110
+
111
+ io = open_file
112
+ super(name, io, opts)
105
113
 
106
114
  # if the truncate flag was set to true, then roll
107
115
  roll_now = opts.fetch(:truncate, false)
@@ -121,11 +129,11 @@ module Logging::Appenders
121
129
  # is currently open then it will be closed and immediately opened.
122
130
  def reopen
123
131
  @mutex.synchronize {
124
- if defined?(@io) && @io
132
+ if defined? @io && @io
125
133
  flush
126
134
  @io.close rescue nil
127
135
  end
128
- @io = ::File.new(filename, @mode)
136
+ @io = open_file
129
137
  }
130
138
  super
131
139
  self
@@ -134,6 +142,20 @@ module Logging::Appenders
134
142
 
135
143
  private
136
144
 
145
+ def open_file
146
+ mode = ::File::RDWR | ::File::APPEND
147
+ ::File.open(filename, mode: mode, external_encoding: encoding)
148
+ rescue Errno::ENOENT
149
+ create_file
150
+ end
151
+
152
+ def create_file
153
+ mode = ::File::RDWR | ::File::APPEND | ::File::CREAT | ::File::EXCL
154
+ ::File.open(filename, mode: mode, external_encoding: encoding)
155
+ rescue Errno::EEXIST
156
+ open_file
157
+ end
158
+
137
159
  # Returns the file name to use as the temporary copy location. We are
138
160
  # using copy-and-truncate semantics for rolling files so that the IO
139
161
  # file descriptor remains valid during rolling.
@@ -141,6 +163,15 @@ module Logging::Appenders
141
163
  @roller.copy_file
142
164
  end
143
165
 
166
+ # Returns the modification time of the copy file if one exists. Otherwise
167
+ # returns `nil`.
168
+ def copy_file_mtime
169
+ return nil unless ::File.exist?(copy_file)
170
+ ::File.mtime(copy_file)
171
+ rescue Errno::ENOENT
172
+ nil
173
+ end
174
+
144
175
  # Write the given _event_ to the log file. The log file will be rolled
145
176
  # if the maximum file size is exceeded or if the file is older than the
146
177
  # maximum age.
@@ -148,14 +179,18 @@ module Logging::Appenders
148
179
  return self if @io.nil?
149
180
 
150
181
  str = str.force_encoding(encoding) if encoding && str.encoding != encoding
151
- @io.flock_sh { @io.syswrite str }
182
+ @mutex.synchronize {
183
+ @io.flock_sh { @io.write str }
184
+ }
152
185
 
153
186
  if roll_required?
154
- @io.flock? {
155
- @age_fn_mtime = nil
156
- copy_truncate if roll_required?
187
+ @mutex.synchronize {
188
+ @io.flock? {
189
+ @age_fn_mtime = nil
190
+ copy_truncate if roll_required?
191
+ }
192
+ @roller.roll_files
157
193
  }
158
- @roller.roll_files
159
194
  end
160
195
  self
161
196
  rescue StandardError => err
@@ -166,7 +201,8 @@ module Logging::Appenders
166
201
 
167
202
  # Returns +true+ if the log file needs to be rolled.
168
203
  def roll_required?
169
- return false if ::File.exist?(copy_file) && (Time.now - ::File.mtime(copy_file)) < 180
204
+ mtime = copy_file_mtime
205
+ return false if mtime && (Time.now - mtime) < 180
170
206
 
171
207
  # check if max size has been exceeded
172
208
  s = @size ? ::File.size(filename) > @size : false
@@ -183,7 +219,7 @@ module Logging::Appenders
183
219
  def copy_truncate
184
220
  return unless ::File.exist?(filename)
185
221
  FileUtils.concat filename, copy_file
186
- @io.truncate 0
222
+ @io.truncate(0)
187
223
 
188
224
  # touch the age file if needed
189
225
  if @age
@@ -245,22 +281,22 @@ module Logging::Appenders
245
281
  # Create a new roller. See the RollingFile#initialize documentation for
246
282
  # the list of options.
247
283
  #
248
- # name - The appender name as a String
249
- # opts - The options Hash
284
+ # filename - the name of the file to roll
285
+ # age - the age of the file at which it should be rolled
286
+ # size - the size of the file in bytes at which it should be rolled
287
+ # roll_by - roll either by 'number' or 'date'
288
+ # keep - the number of log files to keep when rolling
250
289
  #
251
- def initialize( name, opts )
290
+ def initialize( filename, age: nil, size: nil, roll_by: nil, keep: nil )
252
291
  # raise an error if a filename was not given
253
- @fn = opts.fetch(:filename, name)
292
+ @fn = filename
254
293
  raise ArgumentError, 'no filename was given' if @fn.nil?
255
294
 
256
295
  if (m = RGXP.match @fn)
257
296
  @roll_by = ("#{m[2]}%d" == m[1]) ? :number : :date
258
297
  else
259
- age = opts.fetch(:age, nil)
260
- size = opts.fetch(:size, nil)
261
-
262
298
  @roll_by =
263
- case opts.fetch(:roll_by, nil)
299
+ case roll_by
264
300
  when 'number'; :number
265
301
  when 'date'; :date
266
302
  else
@@ -283,8 +319,7 @@ module Logging::Appenders
283
319
  ::Logging::Appenders::File.assert_valid_logfile(filename)
284
320
 
285
321
  @roll = false
286
- @keep = opts.fetch(:keep, nil)
287
- @keep = Integer(keep) unless keep.nil?
322
+ @keep = keep.nil? ? nil : Integer(keep)
288
323
  end
289
324
 
290
325
  attr_reader :keep, :roll_by
@@ -337,7 +372,6 @@ module Logging::Appenders
337
372
  files.delete copy_file
338
373
 
339
374
  self.send "roll_by_#{roll_by}", files
340
-
341
375
  nil
342
376
  ensure
343
377
  self.roll = false