logging 1.8.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/History.txt +20 -0
  4. data/README.md +159 -0
  5. data/Rakefile +9 -5
  6. data/examples/appenders.rb +0 -4
  7. data/examples/layouts.rb +1 -8
  8. data/examples/names.rb +4 -4
  9. data/lib/logging.rb +24 -76
  10. data/lib/logging/appender.rb +71 -16
  11. data/lib/logging/appenders.rb +0 -2
  12. data/lib/logging/appenders/buffering.rb +32 -16
  13. data/lib/logging/appenders/file.rb +2 -2
  14. data/lib/logging/appenders/io.rb +1 -1
  15. data/lib/logging/appenders/rolling_file.rb +228 -165
  16. data/lib/logging/appenders/string_io.rb +1 -1
  17. data/lib/logging/appenders/syslog.rb +4 -4
  18. data/lib/logging/color_scheme.rb +20 -3
  19. data/lib/logging/diagnostic_context.rb +142 -17
  20. data/lib/logging/filter.rb +18 -0
  21. data/lib/logging/filters.rb +4 -0
  22. data/lib/logging/filters/level.rb +29 -0
  23. data/lib/logging/layout.rb +2 -2
  24. data/lib/logging/layouts/parseable.rb +5 -2
  25. data/lib/logging/layouts/pattern.rb +309 -168
  26. data/lib/logging/log_event.rb +5 -5
  27. data/lib/logging/logger.rb +55 -68
  28. data/lib/logging/repository.rb +24 -39
  29. data/lib/logging/root_logger.rb +1 -1
  30. data/lib/logging/utils.rb +4 -65
  31. data/lib/logging/version.rb +8 -0
  32. data/lib/rspec/logging_helper.rb +3 -3
  33. data/logging.gemspec +46 -0
  34. data/test/appenders/test_buffered_io.rb +29 -0
  35. data/test/appenders/test_file.rb +2 -2
  36. data/test/appenders/test_rolling_file.rb +62 -1
  37. data/test/layouts/test_color_pattern.rb +1 -1
  38. data/test/layouts/test_json.rb +3 -0
  39. data/test/layouts/test_pattern.rb +6 -2
  40. data/test/layouts/test_yaml.rb +4 -1
  41. data/test/test_appender.rb +56 -0
  42. data/test/test_filter.rb +33 -0
  43. data/test/test_layout.rb +4 -8
  44. data/test/test_log_event.rb +3 -3
  45. data/test/test_logger.rb +81 -57
  46. data/test/test_logging.rb +0 -59
  47. data/test/test_mapped_diagnostic_context.rb +49 -1
  48. data/test/test_nested_diagnostic_context.rb +16 -1
  49. data/test/test_repository.rb +24 -32
  50. data/test/test_utils.rb +14 -50
  51. metadata +35 -53
  52. data/README.rdoc +0 -143
  53. data/data/bad_logging_1.rb +0 -13
  54. data/data/bad_logging_2.rb +0 -21
  55. data/data/logging.rb +0 -42
  56. data/data/logging.yaml +0 -63
  57. data/data/simple_logging.rb +0 -13
  58. data/examples/consolidation.rb +0 -83
  59. data/lib/logging/appenders/email.rb +0 -178
  60. data/lib/logging/appenders/growl.rb +0 -200
  61. data/lib/logging/config/configurator.rb +0 -187
  62. data/lib/logging/config/yaml_configurator.rb +0 -190
  63. data/lib/logging/stats.rb +0 -277
  64. data/test/appenders/test_email.rb +0 -170
  65. data/test/appenders/test_growl.rb +0 -138
  66. data/test/config/test_configurator.rb +0 -69
  67. data/test/config/test_yaml_configurator.rb +0 -39
  68. data/test/test_consolidate.rb +0 -45
  69. data/test/test_stats.rb +0 -273
  70. data/version.txt +0 -1
@@ -16,7 +16,7 @@ module Logging
16
16
  #
17
17
  class Appender
18
18
 
19
- attr_reader :name, :layout, :level
19
+ attr_reader :name, :layout, :level, :filters
20
20
 
21
21
  # call-seq:
22
22
  # Appender.new( name )
@@ -32,27 +32,29 @@ class Appender
32
32
  # :layout => the layout to use when formatting log events
33
33
  # :level => the level at which to log
34
34
  # :encoding => encoding to use when writing messages (defaults to UTF-8)
35
+ # :filters => filters to apply to events before processing
35
36
  #
36
37
  def initialize( name, opts = {} )
37
38
  ::Logging.init unless ::Logging.initialized?
38
39
 
39
- @name = name.to_s
40
- @closed = false
40
+ @name = name.to_s
41
+ @closed = false
42
+ @filters = []
43
+ @mutex = ReentrantMutex.new
41
44
 
42
- self.layout = opts.getopt(:layout, ::Logging::Layouts::Basic.new)
43
- self.level = opts.getopt(:level)
45
+ self.layout = opts.fetch(:layout, ::Logging::Layouts::Basic.new)
46
+ self.level = opts.fetch(:level, nil)
44
47
  self.encoding = opts.fetch(:encoding, self.encoding)
48
+ self.filters = opts.fetch(:filters, nil)
45
49
 
46
- @mutex = ReentrantMutex.new
47
-
48
- if opts.getopt(:header, true)
50
+ if opts.fetch(:header, true)
49
51
  header = @layout.header
50
52
 
51
53
  unless header.nil? || header.empty?
52
54
  begin
53
55
  write(header)
54
56
  rescue StandardError => err
55
- ::Logging.log_internal(-2) {err}
57
+ ::Logging.log_internal_error(err)
56
58
  end
57
59
  end
58
60
  end
@@ -73,12 +75,12 @@ class Appender
73
75
  end
74
76
 
75
77
  # only append if the event level is less than or equal to the configured
76
- # appender level
77
- unless @level > event.level
78
+ # appender level and the filter does not disallow it
79
+ if event = allow(event)
78
80
  begin
79
81
  write(event)
80
82
  rescue StandardError => err
81
- ::Logging.log_internal(-2) {err}
83
+ ::Logging.log_internal_error(err)
82
84
  end
83
85
  end
84
86
 
@@ -97,11 +99,11 @@ class Appender
97
99
  "appender '<#{self.class.name}: #{@name}>' is closed"
98
100
  end
99
101
 
100
- unless @level >= ::Logging::LEVELS.length
102
+ unless off?
101
103
  begin
102
104
  write(str)
103
105
  rescue StandardError => err
104
- ::Logging.log_internal(-2) {err}
106
+ ::Logging.log_internal_error(err)
105
107
  end
106
108
  end
107
109
  self
@@ -162,6 +164,35 @@ class Appender
162
164
  @layout = layout
163
165
  end
164
166
 
167
+ # Sets the filter(s) to be used by this appender. This method will clear the
168
+ # current filter set and add those passed to this setter method.
169
+ #
170
+ # Examples
171
+ # appender.filters = Logging::Filters::Level.new(:warn, :error)
172
+ #
173
+ def filters=( args )
174
+ @filters.clear
175
+ add_filters(*args)
176
+ end
177
+
178
+ # Sets the filter(s) to be used by this appender. The filters will be
179
+ # applied in the order that they are added to the appender.
180
+ #
181
+ # Examples
182
+ # add_filters(Logging::Filters::Level.new(:warn, :error))
183
+ #
184
+ # Returns this appender instance.
185
+ def add_filters( *args )
186
+ args.flatten.each do |filter|
187
+ next if filter.nil?
188
+ unless filter.kind_of?(::Logging::Filter)
189
+ raise TypeError, "#{filter.inspect} is not a kind of 'Logging::Filter'"
190
+ end
191
+ @filters << filter
192
+ end
193
+ self
194
+ end
195
+
165
196
  # call-seq:
166
197
  # close( footer = true )
167
198
  #
@@ -183,7 +214,7 @@ class Appender
183
214
  begin
184
215
  write(footer)
185
216
  rescue StandardError => err
186
- ::Logging.log_internal(-2) {err}
217
+ ::Logging.log_internal_error(err)
187
218
  end
188
219
  end
189
220
  end
@@ -250,7 +281,6 @@ class Appender
250
281
  # value - The encoding as a String, Symbol, or Encoding instance.
251
282
  #
252
283
  # Raises ArgumentError if the value is not a valid encoding.
253
- #
254
284
  def encoding=( value )
255
285
  if value.nil?
256
286
  @encoding = nil
@@ -259,6 +289,31 @@ class Appender
259
289
  end
260
290
  end
261
291
 
292
+ # Check to see if the event should be processed by the appender. An event will
293
+ # be rejected if the event level is lower than the configured level for the
294
+ # appender. Or it will be rejected if one of the filters rejects the event.
295
+ #
296
+ # event - The LogEvent to check
297
+ #
298
+ # Returns the event if it is allowed; returns `nil` if it is not allowed.
299
+ def allow( event )
300
+ return nil if @level > event.level
301
+ @filters.each do |filter|
302
+ break unless event = filter.allow(event)
303
+ end
304
+ event
305
+ end
306
+
307
+ # Returns `true` if the appender has been turned off. This is useful for
308
+ # appenders that write data to a remote location (such as syslog or email),
309
+ # and that write encounters too many errors. The appender can turn itself off
310
+ # to and log an error via the `Logging` logger.
311
+ #
312
+ # Set the appender's level to a valid value to turn it back on.
313
+ def off?
314
+ @level >= ::Logging::LEVELS.length
315
+ end
316
+
262
317
 
263
318
  private
264
319
 
@@ -54,9 +54,7 @@ module Logging
54
54
  require libpath('logging/appenders/buffering')
55
55
  require libpath('logging/appenders/io')
56
56
  require libpath('logging/appenders/console')
57
- require libpath('logging/appenders/email')
58
57
  require libpath('logging/appenders/file')
59
- require libpath('logging/appenders/growl')
60
58
  require libpath('logging/appenders/rolling_file')
61
59
  require libpath('logging/appenders/string_io')
62
60
  require libpath('logging/appenders/syslog')
@@ -14,23 +14,24 @@ module Logging::Appenders
14
14
  module Buffering
15
15
 
16
16
  # Default buffer size
17
- #
18
17
  DEFAULT_BUFFER_SIZE = 500;
19
18
 
20
19
  # The buffer holding the log messages
21
- #
22
20
  attr_reader :buffer
23
21
 
24
22
  # The auto-flushing setting. When the buffer reaches this size, all
25
23
  # messages will be be flushed automatically.
26
- #
27
24
  attr_reader :auto_flushing
28
25
 
29
26
  # When set, the buffer will be flushed at regular intervals defined by the
30
27
  # flush_period.
31
- #
32
28
  attr_reader :flush_period
33
29
 
30
+ # Messages will be written in chunks. This controls the number of messages
31
+ # to pull from the buffer for each write operation. The default is to pull
32
+ # all messages from the buffer at once.
33
+ attr_accessor :write_size
34
+
34
35
  # Setup the message buffer and other variables for automatically and
35
36
  # periodically flushing the buffer.
36
37
  #
@@ -67,22 +68,36 @@ module Logging::Appenders
67
68
  super
68
69
  end
69
70
 
70
- # Call +flush+ to force an appender to write out any buffered log events.
71
- # Similar to IO#flush, so use in a similar fashion.
72
- #
71
+ # Call `flush` to force an appender to write out any buffered log events.
72
+ # Similar to `IO#flush`, so use in a similar fashion.
73
73
  def flush
74
74
  return self if @buffer.empty?
75
75
 
76
- str = nil
76
+ ary = nil
77
77
  sync {
78
- str = @buffer.join
78
+ ary = @buffer.dup
79
79
  @buffer.clear
80
80
  }
81
81
 
82
- canonical_write str unless str.empty?
82
+ if ary.length <= write_size
83
+ str = ary.join
84
+ canonical_write str unless str.empty?
85
+ else
86
+ ary.each_slice(write_size) do |a|
87
+ str = a.join
88
+ canonical_write str unless str.empty?
89
+ end
90
+ end
91
+
83
92
  self
84
93
  end
85
94
 
95
+ # Clear the underlying buffer of all log events. These events will not be
96
+ # appended to the logging destination; they will be lost.
97
+ def clear!
98
+ sync { @buffer.clear }
99
+ end
100
+
86
101
  # Configure the levels that will trigger an immediate flush of the
87
102
  # logging buffer. When a log event of the given level is seen, the
88
103
  # buffer will be flushed immediately. Only the levels explicitly given
@@ -154,7 +169,7 @@ module Logging::Appenders
154
169
  "auto_flushing period must be greater than zero: #{period.inspect}"
155
170
  end
156
171
 
157
- @auto_flushing = DEFAULT_BUFFER_SIZE if @flush_period and @auto_flushing <= 1
172
+ @auto_flushing = DEFAULT_BUFFER_SIZE if @flush_period && @auto_flushing <= 1
158
173
  end
159
174
 
160
175
  # Configure periodic flushing of the message buffer. Periodic flushing is
@@ -205,9 +220,10 @@ module Logging::Appenders
205
220
  def configure_buffering( opts )
206
221
  ::Logging.init unless ::Logging.initialized?
207
222
 
208
- self.immediate_at = opts.getopt(:immediate_at, '')
209
- self.auto_flushing = opts.getopt(:auto_flushing, true)
210
- self.flush_period = opts.getopt(:flush_period, nil)
223
+ self.immediate_at = opts.fetch(:immediate_at, '')
224
+ self.auto_flushing = opts.fetch(:auto_flushing, true)
225
+ self.flush_period = opts.fetch(:flush_period, nil)
226
+ self.write_size = opts.fetch(:write_size, DEFAULT_BUFFER_SIZE)
211
227
  end
212
228
 
213
229
  # Returns true if the _event_ level matches one of the configured
@@ -241,7 +257,7 @@ module Logging::Appenders
241
257
  canonical_write(str)
242
258
  else
243
259
  sync {
244
- str = str.force_encoding(encoding) if encoding and str.encoding != encoding
260
+ str = str.force_encoding(encoding) if encoding && str.encoding != encoding
245
261
  @buffer << str
246
262
  }
247
263
  @periodic_flusher.signal if @periodic_flusher
@@ -333,7 +349,7 @@ module Logging::Appenders
333
349
  @appender.flush
334
350
  rescue => err
335
351
  ::Logging.log_internal {"PeriodicFlusher for appender #{@appender.inspect} encountered an error"}
336
- ::Logging.log_internal(-2) {err}
352
+ ::Logging.log_internal_error(err)
337
353
  end
338
354
  }; @thread = nil }
339
355
 
@@ -47,12 +47,12 @@ module Logging::Appenders
47
47
  # appended to the file.
48
48
  #
49
49
  def initialize( name, opts = {} )
50
- @fn = opts.getopt(:filename, name)
50
+ @fn = opts.fetch(:filename, name)
51
51
  raise ArgumentError, 'no filename was given' if @fn.nil?
52
52
 
53
53
  @fn = ::File.expand_path(@fn)
54
54
  self.class.assert_valid_logfile(@fn)
55
- @mode = opts.getopt(:truncate) ? 'w' : 'a'
55
+ @mode = opts.fetch(:truncate, false) ? 'w' : 'a'
56
56
 
57
57
  self.encoding = opts.fetch(:encoding, self.encoding)
58
58
  @mode = "#{@mode}:#{self.encoding}" if self.encoding
@@ -76,7 +76,7 @@ module Logging::Appenders
76
76
  rescue StandardError => err
77
77
  self.level = :off
78
78
  ::Logging.log_internal {"appender #{name.inspect} has been disabled"}
79
- ::Logging.log_internal(-2) {err}
79
+ ::Logging.log_internal_error(err)
80
80
  end
81
81
 
82
82
  end # IO
@@ -1,8 +1,6 @@
1
-
2
1
  module Logging::Appenders
3
2
 
4
3
  # Accessor / Factory for the RollingFile appender.
5
- #
6
4
  def self.rolling_file( *args )
7
5
  return ::Logging::Appenders::RollingFile if args.empty?
8
6
  ::Logging::Appenders::RollingFile.new(*args)
@@ -23,10 +21,9 @@ module Logging::Appenders
23
21
  # /var/log/ruby.log => /var/log/ruby.1.log
24
22
  #
25
23
  # New log messages will continue to be appended to the same log file
26
- # (<tt>/var/log/ruby.log</tt> in our example above). The age number for all
27
- # older log files is incremented when the log file is rolled. The number of
28
- # older log files to keep can be given, otherwise all the log files are
29
- # kept.
24
+ # (`/var/log/ruby.log` in our example above). The age number for all older
25
+ # log files is incremented when the log file is rolled. The number of older
26
+ # log files to keep can be given, otherwise all the log files are kept.
30
27
  #
31
28
  # The actual process of rolling all the log file names can be expensive if
32
29
  # there are many, many older log files to process.
@@ -38,14 +35,13 @@ module Logging::Appenders
38
35
  #
39
36
  # /var/log/ruby.log => /var/log/ruby.20091225.log
40
37
  #
41
- # Where the date is expressed as <tt>%Y%m%d</tt> in the Time#strftime format.
38
+ # Where the date is expressed as `%Y%m%d` in the Time#strftime format.
42
39
  #
43
40
  # NOTE: this class is not safe to use when log messages are written to files
44
41
  # on NFS mounts or other remote file system. It should only be used for log
45
42
  # files on the local file system. The exception to this is when a single
46
43
  # process is writing to the log file; remote file systems are safe to
47
44
  # use in this case but still not recommended.
48
- #
49
45
  class RollingFile < ::Logging::Appenders::IO
50
46
 
51
47
  # call-seq:
@@ -58,6 +54,20 @@ module Logging::Appenders
58
54
  # [:filename] The base filename to use when constructing new log
59
55
  # filenames.
60
56
  #
57
+ # The "rolling" portion of the filename can be configured via some simple
58
+ # pattern templates. For numbered rolling, you can use {{.%d}}
59
+ #
60
+ # "logname{{.%d}}.log" => ["logname.log", "logname.1.log", "logname.2.log" ...]
61
+ # "logname.log{{-%d}}" => ["logname.log", "logname.log-1", "logname.log-2" ...]
62
+ #
63
+ # And for date rolling you can use `strftime` patterns:
64
+ #
65
+ # "logname{{.%Y%m%d}}.log" => ["logname.log, "logname.20130626.log" ...]
66
+ # "logname{{.%Y-%m-%dT%H:%M:%S}}.log" => ["logname.log, "logname.2013-06-26T22:03:31.log" ...]
67
+ #
68
+ # If the defaults suit you fine, just pass in the :roll_by option and use
69
+ # your normal log filename without any pattern template.
70
+ #
61
71
  # The following options are optional:
62
72
  #
63
73
  # [:layout] The Layout that will be used by this appender. The Basic
@@ -74,90 +84,27 @@ module Logging::Appenders
74
84
  # 'date'.
75
85
  #
76
86
  def initialize( name, opts = {} )
77
- # raise an error if a filename was not given
78
- @fn = opts.getopt(:filename, name)
79
- raise ArgumentError, 'no filename was given' if @fn.nil?
80
-
81
- @fn = ::File.expand_path(@fn)
82
- @fn_copy = @fn + '._copy_'
83
- ::Logging::Appenders::File.assert_valid_logfile(@fn)
87
+ @roller = Roller.new name, opts
84
88
 
85
89
  # grab our options
86
- @size = opts.getopt(:size, :as => Integer)
90
+ @size = opts.fetch(:size, nil)
91
+ @size = Integer(@size) unless @size.nil?
87
92
 
88
- code = 'def sufficiently_aged?() false end'
89
- @age_fn = @fn + '.age'
93
+ @age_fn = filename + '.age'
90
94
  @age_fn_mtime = nil
95
+ @age = opts.fetch(:age, nil)
91
96
 
92
- case @age = opts.getopt(:age)
93
- when 'daily'
94
- code = <<-CODE
95
- def sufficiently_aged?
96
- @age_fn_mtime ||= ::File.mtime(@age_fn)
97
- now = Time.now
98
- if (now.day != @age_fn_mtime.day) or (now - @age_fn_mtime) > 86400
99
- return true
100
- end
101
- false
102
- end
103
- CODE
104
- when 'weekly'
105
- code = <<-CODE
106
- def sufficiently_aged?
107
- @age_fn_mtime ||= ::File.mtime(@age_fn)
108
- if (Time.now - @age_fn_mtime) > 604800
109
- return true
110
- end
111
- false
112
- end
113
- CODE
114
- when 'monthly'
115
- code = <<-CODE
116
- def sufficiently_aged?
117
- @age_fn_mtime ||= ::File.mtime(@age_fn)
118
- now = Time.now
119
- if (now.month != @age_fn_mtime.month) or (now - @age_fn_mtime) > 2678400
120
- return true
121
- end
122
- false
123
- end
124
- CODE
125
- when Integer, String
126
- @age = Integer(@age)
127
- code = <<-CODE
128
- def sufficiently_aged?
129
- @age_fn_mtime ||= ::File.mtime(@age_fn)
130
- if (Time.now - @age_fn_mtime) > @age
131
- return true
132
- end
133
- false
134
- end
135
- CODE
136
- end
137
-
138
- FileUtils.touch(@age_fn) if @age and !test(?f, @age_fn)
139
-
140
- meta = class << self; self end
141
- meta.class_eval code, __FILE__, __LINE__
97
+ # create our `sufficiently_aged?` method
98
+ build_singleton_methods
99
+ FileUtils.touch(@age_fn) if @age && !test(?f, @age_fn)
142
100
 
143
101
  # we are opening the file in read/write mode so that a shared lock can
144
102
  # be used on the file descriptor => http://pubs.opengroup.org/onlinepubs/009695399/functions/fcntl.html
145
103
  @mode = encoding ? "a+:#{encoding}" : 'a+'
146
- super(name, ::File.new(@fn, @mode), opts)
147
-
148
- # setup the file roller
149
- @roller =
150
- case opts.getopt(:roll_by)
151
- when 'number'; NumberedRoller.new(@fn, opts)
152
- when 'date'; DateRoller.new(@fn, opts)
153
- else
154
- (@age and !@size) ?
155
- DateRoller.new(@fn, opts) :
156
- NumberedRoller.new(@fn, opts)
157
- end
104
+ super(name, ::File.new(filename, @mode), opts)
158
105
 
159
106
  # if the truncate flag was set to true, then roll
160
- roll_now = opts.getopt(:truncate, false)
107
+ roll_now = opts.fetch(:truncate, false)
161
108
  if roll_now
162
109
  copy_truncate
163
110
  @roller.roll_files
@@ -165,20 +112,20 @@ module Logging::Appenders
165
112
  end
166
113
 
167
114
  # Returns the path to the logfile.
168
- #
169
- def filename() @fn.dup end
115
+ def filename
116
+ @roller.filename
117
+ end
170
118
 
171
119
  # Reopen the connection to the underlying logging destination. If the
172
120
  # connection is currently closed then it will be opened. If the connection
173
121
  # is currently open then it will be closed and immediately opened.
174
- #
175
122
  def reopen
176
123
  @mutex.synchronize {
177
- if defined? @io and @io
124
+ if defined?(@io) && @io
178
125
  flush
179
126
  @io.close rescue nil
180
127
  end
181
- @io = ::File.new(@fn, @mode)
128
+ @io = ::File.new(filename, @mode)
182
129
  }
183
130
  super
184
131
  self
@@ -187,14 +134,20 @@ module Logging::Appenders
187
134
 
188
135
  private
189
136
 
137
+ # Returns the file name to use as the temporary copy location. We are
138
+ # using copy-and-truncate semantics for rolling files so that the IO
139
+ # file descriptor remains valid during rolling.
140
+ def copy_file
141
+ @roller.copy_file
142
+ end
143
+
190
144
  # Write the given _event_ to the log file. The log file will be rolled
191
145
  # if the maximum file size is exceeded or if the file is older than the
192
146
  # maximum age.
193
- #
194
147
  def canonical_write( str )
195
148
  return self if @io.nil?
196
149
 
197
- str = str.force_encoding(encoding) if encoding and str.encoding != encoding
150
+ str = str.force_encoding(encoding) if encoding && str.encoding != encoding
198
151
  @io.flock_sh { @io.syswrite str }
199
152
 
200
153
  if roll_required?
@@ -208,16 +161,15 @@ module Logging::Appenders
208
161
  rescue StandardError => err
209
162
  self.level = :off
210
163
  ::Logging.log_internal {"appender #{name.inspect} has been disabled"}
211
- ::Logging.log_internal(-2) {err}
164
+ ::Logging.log_internal_error(err)
212
165
  end
213
166
 
214
167
  # Returns +true+ if the log file needs to be rolled.
215
- #
216
168
  def roll_required?
217
- return false if ::File.exist?(@fn_copy) and (Time.now - ::File.mtime(@fn_copy)) < 180
169
+ return false if ::File.exist?(copy_file) && (Time.now - ::File.mtime(copy_file)) < 180
218
170
 
219
171
  # check if max size has been exceeded
220
- s = @size ? ::File.size(@fn) > @size : false
172
+ s = @size ? ::File.size(filename) > @size : false
221
173
 
222
174
  # check if max age has been exceeded
223
175
  a = sufficiently_aged?
@@ -228,10 +180,9 @@ module Logging::Appenders
228
180
  # Copy the contents of the logfile to another file. Truncate the logfile
229
181
  # to zero length. This method will set the roll flag so that all the
230
182
  # current logfiles will be rolled along with the copied file.
231
- #
232
183
  def copy_truncate
233
- return unless ::File.exist?(@fn)
234
- FileUtils.concat @fn, @fn_copy
184
+ return unless ::File.exist?(filename)
185
+ FileUtils.concat filename, copy_file
235
186
  @io.truncate 0
236
187
 
237
188
  # touch the age file if needed
@@ -243,96 +194,208 @@ module Logging::Appenders
243
194
  @roller.roll = true
244
195
  end
245
196
 
197
+ # Returns the modification time of the age file.
198
+ def age_fn_mtime
199
+ @age_fn_mtime ||= ::File.mtime(@age_fn)
200
+ end
246
201
 
247
- # :stopdoc:
248
- class NumberedRoller
249
- attr_accessor :roll
250
-
251
- def initialize( fn, opts )
252
- # grab the information we need to properly roll files
253
- ext = ::File.extname(fn)
254
- bn = ::File.join(::File.dirname(fn), ::File.basename(fn, ext))
255
- @rgxp = %r/\.(\d+)#{Regexp.escape(ext)}\z/
256
- @glob = "#{bn}.*#{ext}"
257
- @logname_fmt = "#{bn}.%d#{ext}"
258
- @fn_copy = fn + '._copy_'
259
- @keep = opts.getopt(:keep, :as => Integer)
260
- @roll = false
261
- end
202
+ # We use meta-programming here to define the `sufficiently_aged?` method for
203
+ # the rolling appender. The `sufficiently_aged?` method is responsible for
204
+ # determining if the current log file is older than the rolling criteria -
205
+ # daily, weekly, etc.
206
+ #
207
+ # Returns this rolling file appender instance
208
+ def build_singleton_methods
209
+ method =
210
+ case @age
211
+ when 'daily'
212
+ -> {
213
+ now = Time.now
214
+ (now.day != age_fn_mtime.day) || (now - age_fn_mtime) > 86400
215
+ }
216
+
217
+ when 'weekly'
218
+ -> { (Time.now - age_fn_mtime) > 604800 }
219
+
220
+ when 'monthly'
221
+ -> {
222
+ now = Time.now
223
+ (now.month != age_fn_mtime.month) || (now - age_fn_mtime) > 2678400
224
+ }
225
+
226
+ when Integer, String
227
+ @age = Integer(@age)
228
+ -> { (Time.now - age_fn_mtime) > @age }
262
229
 
263
- def roll_files
264
- return unless @roll and ::File.exist?(@fn_copy)
230
+ else
231
+ -> { false }
232
+ end
265
233
 
266
- files = Dir.glob(@glob).find_all {|fn| @rgxp =~ fn}
267
- unless files.empty?
268
- # sort the files in reverse order based on their count number
269
- files = files.sort do |a,b|
270
- a = Integer(@rgxp.match(a)[1])
271
- b = Integer(@rgxp.match(b)[1])
272
- b <=> a
273
- end
234
+ self.define_singleton_method(:sufficiently_aged?, method)
235
+ end
274
236
 
275
- # for each file, roll its count number one higher
276
- files.each do |fn|
277
- cnt = Integer(@rgxp.match(fn)[1])
278
- if @keep and cnt >= @keep
279
- ::File.delete fn
280
- next
281
- end
282
- ::File.rename fn, sprintf(@logname_fmt, cnt+1)
283
- end
237
+ # Not intended for general consumption, but the Roller class is used
238
+ # internally by the RollingFile appender to roll dem log files according
239
+ # to the user's desires.
240
+ class Roller
241
+
242
+ # The magic regex for finding user-defined roller patterns.
243
+ RGXP = %r/{{(([^%]+)?.*?)}}/
244
+
245
+ # Create a new roller. See the RollingFile#initialize documentation for
246
+ # the list of options.
247
+ #
248
+ # name - The appender name as a String
249
+ # opts - The options Hash
250
+ #
251
+ def initialize( name, opts )
252
+ # raise an error if a filename was not given
253
+ @fn = opts.fetch(:filename, name)
254
+ raise ArgumentError, 'no filename was given' if @fn.nil?
255
+
256
+ if (m = RGXP.match @fn)
257
+ @roll_by = ("#{m[2]}%d" == m[1]) ? :number : :date
258
+ else
259
+ age = opts.fetch(:age, nil)
260
+ size = opts.fetch(:size, nil)
261
+
262
+ @roll_by =
263
+ case opts.fetch(:roll_by, nil)
264
+ when 'number'; :number
265
+ when 'date'; :date
266
+ else
267
+ (age && !size) ? :date : :number
268
+ end
269
+
270
+ ext = ::File.extname(@fn)
271
+ bn = ::File.join(::File.dirname(@fn), ::File.basename(@fn, ext))
272
+
273
+ @fn = if :date == @roll_by && %w[daily weekly monthly].include?(age)
274
+ "#{bn}{{.%Y%m%d}}#{ext}"
275
+ elsif :date == @roll_by
276
+ "#{bn}{{.%Y%m%d-%H%M%S}}#{ext}"
277
+ else
278
+ "#{bn}{{.%d}}#{ext}"
279
+ end
284
280
  end
285
281
 
286
- # finally rename the copied log file
287
- ::File.rename(@fn_copy, sprintf(@logname_fmt, 1))
288
- ensure
282
+ @fn = ::File.expand_path(@fn)
283
+ ::Logging::Appenders::File.assert_valid_logfile(filename)
284
+
289
285
  @roll = false
286
+ @keep = opts.fetch(:keep, nil)
287
+ @keep = Integer(keep) unless keep.nil?
290
288
  end
291
- end
292
289
 
293
- class DateRoller
290
+ attr_reader :keep, :roll_by
294
291
  attr_accessor :roll
295
292
 
296
- def initialize( fn, opts )
297
- @fn_copy = fn + '._copy_'
298
- @roll = false
299
- @keep = opts.getopt(:keep, :as => Integer)
293
+ # Returns the regular log file name without any roller text.
294
+ def filename
295
+ return @filename if defined? @filename
296
+ @filename = (@fn =~ RGXP ? @fn.sub(RGXP, '') : @fn.dup)
297
+ @filename.freeze
298
+ end
300
299
 
301
- ext = ::File.extname(fn)
302
- bn = ::File.join(::File.dirname(fn), ::File.basename(fn, ext))
300
+ # Returns the file name to use as the temporary copy location. We are
301
+ # using copy-and-truncate semantics for rolling files so that the IO
302
+ # file descriptor remains valid during rolling.
303
+ def copy_file
304
+ return @copy_file if defined? @copy_file
305
+ @copy_file = filename + '._copy_'
306
+ @copy_file.freeze
307
+ end
303
308
 
304
- if @keep
305
- @rgxp = %r/\.(\d+)(-\d+)?#{Regexp.escape(ext)}\z/
306
- @glob = "#{bn}.*#{ext}"
307
- end
309
+ # Returns the glob pattern used to find rolled log files. We use this
310
+ # list for pruning older log files and doing the numbered rolling.
311
+ def glob
312
+ return @glob if defined? @glob
313
+ m = RGXP.match @fn
314
+ @glob = @fn.sub(RGXP, (m[2] ? "#{m[2]}*" : '*'))
315
+ @glob.freeze
316
+ end
308
317
 
309
- if %w[daily weekly monthly].include?(opts.getopt(:age)) and !opts.getopt(:size)
310
- @logname_fmt = "#{bn}.%Y%m%d#{ext}"
311
- else
312
- @logname_fmt = "#{bn}.%Y%m%d-%H%M%S#{ext}"
313
- end
318
+ # Returns the format String used to generate rolled file names.
319
+ # Depending upon the `roll_by` type (:date or :number), this String will
320
+ # be processed by `sprintf` or `Time#strftime`.
321
+ def format
322
+ return @format if defined? @format
323
+ m = RGXP.match @fn
324
+ @format = @fn.sub(RGXP, m[1])
325
+ @format.freeze
314
326
  end
315
327
 
328
+ # Roll the log files. This method will collect the list of rolled files
329
+ # and then pass that list to either `roll_by_number` or `roll_by_date`
330
+ # to perform the actual rolling.
331
+ #
332
+ # Returns nil
316
333
  def roll_files
317
- return unless @roll and ::File.exist?(@fn_copy)
334
+ return unless roll && ::File.exist?(copy_file)
318
335
 
319
- # rename the copied log file
320
- ::File.rename(@fn_copy, Time.now.strftime(@logname_fmt))
321
-
322
- # prune old log files
323
- if @keep
324
- files = Dir.glob(@glob).find_all {|fn| @rgxp =~ fn}
325
- length = files.length
326
- if length > @keep
327
- files.sort {|a,b| b <=> a}.last(length-@keep).each {|fn| ::File.delete fn}
336
+ files = Dir.glob(glob)
337
+ files.delete copy_file
338
+
339
+ self.send "roll_by_#{roll_by}", files
340
+
341
+ nil
342
+ ensure
343
+ self.roll = false
344
+ end
345
+
346
+ # Roll the list of log files optionally removing older files. The "older
347
+ # files" are determined by extracting the number from the log file name
348
+ # and order by the number.
349
+ #
350
+ # files - The Array of filename Strings
351
+ #
352
+ # Returns nil
353
+ def roll_by_number( files )
354
+ @number_rgxp ||= Regexp.new(@fn.sub(RGXP, '\2(\d+)'))
355
+
356
+ # sort the files in reverse order based on their count number
357
+ files = files.sort do |a,b|
358
+ a = Integer(@number_rgxp.match(a)[1])
359
+ b = Integer(@number_rgxp.match(b)[1])
360
+ b <=> a
361
+ end
362
+
363
+ # for each file, roll its count number one higher
364
+ files.each do |fn|
365
+ cnt = Integer(@number_rgxp.match(fn)[1])
366
+ if keep && cnt >= keep
367
+ ::File.delete fn
368
+ next
328
369
  end
370
+ ::File.rename fn, sprintf(format, cnt+1)
329
371
  end
330
- ensure
331
- @roll = false
372
+
373
+ # finally rename the copied log file
374
+ ::File.rename(copy_file, sprintf(format, 1))
332
375
  end
333
- end
334
- # :startdoc:
335
376
 
336
- end # RollingFile
337
- end # Logging::Appenders
377
+ # Roll the list of log files optionally removing older files. The "older
378
+ # files" are determined by the mtime of the log files. So touching log
379
+ # files or otherwise messing with them will screw this up.
380
+ #
381
+ # files - The Array of filename Strings
382
+ #
383
+ # Returns nil
384
+ def roll_by_date( files )
385
+ length = files.length
386
+
387
+ if keep && length >= keep
388
+ files = files.sort do |a,b|
389
+ a = ::File.mtime(a)
390
+ b = ::File.mtime(b)
391
+ b <=> a
392
+ end
393
+ files.last(length-keep+1).each { |fn| ::File.delete fn }
394
+ end
338
395
 
396
+ # rename the copied log file
397
+ ::File.rename(copy_file, Time.now.strftime(format))
398
+ end
399
+ end
400
+ end
401
+ end