logging 2.0.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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