logging 1.8.2 → 2.0.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 (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