filterfish-logging 0.9.8

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. data/History.txt +176 -0
  2. data/Manifest.txt +54 -0
  3. data/README.txt +93 -0
  4. data/Rakefile +28 -0
  5. data/data/logging.yaml +63 -0
  6. data/lib/logging.rb +288 -0
  7. data/lib/logging/appender.rb +257 -0
  8. data/lib/logging/appenders/console.rb +43 -0
  9. data/lib/logging/appenders/email.rb +131 -0
  10. data/lib/logging/appenders/file.rb +55 -0
  11. data/lib/logging/appenders/growl.rb +182 -0
  12. data/lib/logging/appenders/io.rb +81 -0
  13. data/lib/logging/appenders/rolling_file.rb +293 -0
  14. data/lib/logging/appenders/syslog.rb +202 -0
  15. data/lib/logging/config/yaml_configurator.rb +197 -0
  16. data/lib/logging/layout.rb +103 -0
  17. data/lib/logging/layouts/basic.rb +35 -0
  18. data/lib/logging/layouts/pattern.rb +292 -0
  19. data/lib/logging/log_event.rb +50 -0
  20. data/lib/logging/logger.rb +388 -0
  21. data/lib/logging/repository.rb +151 -0
  22. data/lib/logging/root_logger.rb +60 -0
  23. data/lib/logging/utils.rb +44 -0
  24. data/tasks/ann.rake +78 -0
  25. data/tasks/bones.rake +21 -0
  26. data/tasks/gem.rake +106 -0
  27. data/tasks/manifest.rake +49 -0
  28. data/tasks/notes.rake +22 -0
  29. data/tasks/post_load.rake +37 -0
  30. data/tasks/rdoc.rake +49 -0
  31. data/tasks/rubyforge.rake +57 -0
  32. data/tasks/setup.rb +253 -0
  33. data/tasks/svn.rake +45 -0
  34. data/tasks/test.rake +38 -0
  35. data/test/appenders/test_console.rb +40 -0
  36. data/test/appenders/test_email.rb +167 -0
  37. data/test/appenders/test_file.rb +94 -0
  38. data/test/appenders/test_growl.rb +115 -0
  39. data/test/appenders/test_io.rb +113 -0
  40. data/test/appenders/test_rolling_file.rb +187 -0
  41. data/test/appenders/test_syslog.rb +192 -0
  42. data/test/benchmark.rb +88 -0
  43. data/test/config/test_yaml_configurator.rb +41 -0
  44. data/test/layouts/test_basic.rb +44 -0
  45. data/test/layouts/test_pattern.rb +173 -0
  46. data/test/setup.rb +66 -0
  47. data/test/test_appender.rb +162 -0
  48. data/test/test_layout.rb +85 -0
  49. data/test/test_log_event.rb +81 -0
  50. data/test/test_logger.rb +589 -0
  51. data/test/test_logging.rb +250 -0
  52. data/test/test_repository.rb +123 -0
  53. data/test/test_root_logger.rb +82 -0
  54. data/test/test_utils.rb +48 -0
  55. metadata +126 -0
@@ -0,0 +1,182 @@
1
+ # $Id$
2
+
3
+ module Logging::Appenders
4
+
5
+ # This class provides an Appender that can send notifications to the Growl
6
+ # notification system on Mac OS X.
7
+ #
8
+ # +growlnotify+ must be installed somewhere in the path in order for the
9
+ # appender to function properly.
10
+ #
11
+ class Growl < ::Logging::Appender
12
+
13
+ # call-seq:
14
+ # Growl.new( name, opts = {} )
15
+ #
16
+ # Create an appender that will log messages to the Growl framework on a
17
+ # Mac OS X machine.
18
+ #
19
+ def initialize( name, opts = {} )
20
+ super
21
+
22
+ @growl = "growlnotify -w -n \"#{@name}\" -t \"%s\" -m \"%s\" -p %d &"
23
+
24
+ @coalesce = opts.getopt(:coalesce, false)
25
+ @title_sep = opts.getopt(:separator)
26
+
27
+ # provides a mapping from the default Logging levels
28
+ # to the Growl notification levels
29
+ @map = [-2, -1, 0, 1, 2]
30
+
31
+ map = opts.getopt(:map)
32
+ self.map = map unless map.nil?
33
+ setup_coalescing if @coalesce
34
+
35
+ # make sure the growlnotify command can be called
36
+ unless system('growlnotify -v 2>&1 > /dev/null')
37
+ self.level = :off
38
+ # TODO - log that the growl notification is turned off
39
+ end
40
+ end
41
+
42
+ # call-seq:
43
+ # map = { logging_levels => growl_levels }
44
+ #
45
+ # Configure the mapping from the Logging levels to the Growl
46
+ # notification levels. This is needed in order to log events at the
47
+ # proper Growl level.
48
+ #
49
+ # Without any configuration, the following maping will be used:
50
+ #
51
+ # :debug => -2
52
+ # :info => -1
53
+ # :warn => 0
54
+ # :error => 1
55
+ # :fatal => 2
56
+ #
57
+ def map=( levels )
58
+ map = []
59
+ levels.keys.each do |lvl|
60
+ num = ::Logging.level_num(lvl)
61
+ map[num] = growl_level_num(levels[lvl])
62
+ end
63
+ @map = map
64
+ end
65
+
66
+
67
+ private
68
+
69
+ # call-seq:
70
+ # write( event )
71
+ #
72
+ # Write the given _event_ to the growl notification facility. The log
73
+ # event will be processed through the Layout assciated with this
74
+ # appender. The message will be logged at the level specified by the
75
+ # event.
76
+ #
77
+ def write( event )
78
+ title = ''
79
+ priority = 0
80
+ message = if event.instance_of?(::Logging::LogEvent)
81
+ priority = @map[event.level]
82
+ @layout.format(event)
83
+ else
84
+ event.to_s
85
+ end
86
+ return if message.empty?
87
+
88
+ if @title_sep
89
+ title, message = message.split(@title_sep)
90
+ title, message = '', title if message.nil?
91
+ title.strip!
92
+ end
93
+
94
+ growl(title, message, priority)
95
+ self
96
+ end
97
+
98
+ # call-seq:
99
+ # growl_level_num( level ) => integer
100
+ #
101
+ # Takes the given _level_ as a string or integer and returns the
102
+ # corresponding Growl notification level number.
103
+ #
104
+ def growl_level_num( level )
105
+ level = Integer(level)
106
+ if level < -2 or level > 2
107
+ raise ArgumentError, "level '#{level}' is not in range -2..2"
108
+ end
109
+ level
110
+ end
111
+
112
+ # call-seq:
113
+ # growl( title, message, priority )
114
+ #
115
+ # Send the _message_ to the growl notifier using the given _title_ and
116
+ # _priority_.
117
+ #
118
+ def growl( title, message, priority )
119
+ message.tr!("`", "'")
120
+ if @coalesce then coalesce(title, message, priority)
121
+ else system @growl % [title, message, priority] end
122
+ end
123
+
124
+ # call-seq:
125
+ # coalesce( title, message, priority )
126
+ #
127
+ # Attempt to coalesce the given _message_ with any that might be pending
128
+ # in the queue to send to the growl notifier. Messages are coalesced
129
+ # with any in the queue that have the same _title_ and _priority_.
130
+ #
131
+ # There can be only one message in the queue, so if the _title_ and/or
132
+ # _priority_ don't match, the message in the queue is sent immediately
133
+ # to the growl notifier, and the current _message_ is queued.
134
+ #
135
+ def coalesce( *msg )
136
+ @c_mutex.synchronize do
137
+ if @c_queue.empty?
138
+ @c_queue << msg
139
+ @c_thread.run
140
+
141
+ else
142
+ qmsg = @c_queue.last
143
+ if qmsg.first != msg.first or qmsg.last != msg.last
144
+ @c_queue << msg
145
+ else
146
+ qmsg[1] << "\n" << msg[1]
147
+ end
148
+ end
149
+ end
150
+
151
+ Thread.pass
152
+ end
153
+
154
+ # call-seq:
155
+ # setup_coalescing
156
+ #
157
+ # Setup the appender to handle coalescing of messages before sending
158
+ # them to the growl notifier. This requires the creation of a thread and
159
+ # mutex for passing messages from the appender thread to the growl
160
+ # notifier thread.
161
+ #
162
+ def setup_coalescing
163
+ @c_mutex = Mutex.new
164
+ @c_queue = []
165
+
166
+ @c_thread = Thread.new do
167
+ Thread.stop
168
+ loop do
169
+ sleep 0.5
170
+ @c_mutex.synchronize {
171
+ system(@growl % @c_queue.shift) until @c_queue.empty?
172
+ }
173
+ Thread.stop if @c_queue.empty?
174
+ end # loop
175
+ end # Thread.new
176
+ end
177
+
178
+ end # class Growl
179
+
180
+ end # module Logging::Appenders
181
+
182
+ # EOF
@@ -0,0 +1,81 @@
1
+ # $Id$
2
+
3
+ module Logging::Appenders
4
+
5
+ # This class provides an Appender that can write to any IO stream
6
+ # configured for writing.
7
+ #
8
+ class IO < ::Logging::Appender
9
+
10
+ # call-seq:
11
+ # IO.new( name, io )
12
+ # IO.new( name, io, :layout => layout )
13
+ #
14
+ # Creates a new IO Appender using the given name that will use the _io_
15
+ # stream as the logging destination.
16
+ #
17
+ def initialize( name, io, opts = {} )
18
+ unless io.respond_to? :print
19
+ raise TypeError, "expecting an IO object but got '#{io.class.name}'"
20
+ end
21
+
22
+ @io = io
23
+ @io.sync = true
24
+ super(name, opts)
25
+ end
26
+
27
+ # call-seq:
28
+ # close( footer = true )
29
+ #
30
+ # Close the appender and writes the layout footer to the logging
31
+ # destination if the _footer_ flag is set to +true+. Log events will
32
+ # no longer be written to the logging destination after the appender
33
+ # is closed.
34
+ #
35
+ def close( *args )
36
+ return self if @io.nil?
37
+ super(*args)
38
+ io, @io = @io, nil
39
+ io.close unless [STDIN, STDERR, STDOUT].include?(io)
40
+ rescue IOError => err
41
+ ensure
42
+ return self
43
+ end
44
+
45
+ # call-seq:
46
+ # flush
47
+ #
48
+ # Call +flush+ to force an appender to write out any buffered log events.
49
+ # Similar to IO#flush, so use in a similar fashion.
50
+ #
51
+ def flush
52
+ return self if @io.nil?
53
+ @io.flush
54
+ self
55
+ end
56
+
57
+
58
+ private
59
+
60
+ # call-seq:
61
+ # write( event )
62
+ #
63
+ # Writes the given _event_ to the IO stream. If an +IOError+ is detected,
64
+ # than this appender will be turned off and the error reported.
65
+ #
66
+ def write( event )
67
+ begin
68
+ str = event.instance_of?(::Logging::LogEvent) ?
69
+ @layout.format(event) : event.to_s
70
+ return if str.empty?
71
+ @io.print str
72
+ rescue IOError
73
+ self.level = :off
74
+ raise
75
+ end
76
+ end
77
+
78
+ end # class IO
79
+ end # module Logging::Appenders
80
+
81
+ # EOF
@@ -0,0 +1,293 @@
1
+ # $Id$
2
+
3
+ require 'lockfile'
4
+
5
+ module Logging::Appenders
6
+
7
+ # An appender that writes to a file and ensures that the file size or age
8
+ # never exceeds some user specified level.
9
+ #
10
+ # The goal of this class is to write log messages to a file. When the file
11
+ # age or size exceeds a given limit then the log file is closed, the name
12
+ # is changed to indicate it is an older log file, and a new log file is
13
+ # created.
14
+ #
15
+ # The name of the log file is changed by inserting the age of the log file
16
+ # (as a single number) between the log file name and the extension. If the
17
+ # file has no extension then the number is appended to the filename. Here
18
+ # is a simple example:
19
+ #
20
+ # /var/log/ruby.log => /var/log/ruby.1.log
21
+ #
22
+ # New log messages will be appended to a newly opened log file of the same
23
+ # name (<tt>/var/log/ruby.log</tt> in our example above). The age number
24
+ # for all older log files is incremented when the log file is rolled. The
25
+ # number of older log files to keep can be given, otherwise all the log
26
+ # files are kept.
27
+ #
28
+ # The actual process of rolling all the log file names can be expensive if
29
+ # there are many, many older log files to process.
30
+ #
31
+ class RollingFile < ::Logging::Appenders::IO
32
+
33
+ # call-seq:
34
+ # RollingFile.new( name, opts )
35
+ #
36
+ # Creates a new Rolling File Appender. The _name_ is the unique Appender
37
+ # name used to retrieve this appender from the Appender hash. The only
38
+ # required option is the filename to use for creating log files.
39
+ #
40
+ # [:filename] The base filename to use when constructing new log
41
+ # filenames.
42
+ #
43
+ # The following options are optional:
44
+ #
45
+ # [:layout] The Layout that will be used by this appender. The Basic
46
+ # layout will be used if none is given.
47
+ # [:truncate] When set to true any existing log files will be rolled
48
+ # immediately and a new, empty log file will be created.
49
+ # [:size] The maximum allowed size (in bytes) of a log file before
50
+ # it is rolled.
51
+ # [:age] The maximum age (in seconds) of a log file before it is
52
+ # rolled. The age can also be given as 'daily', 'weekly',
53
+ # or 'monthly'.
54
+ # [:keep] The number of rolled log files to keep.
55
+ # [:safe] When set to true, extra checks are made to ensure that
56
+ # only once process can roll the log files; this option
57
+ # should only be used when multiple processes will be
58
+ # logging to the same log file (does not work on Windows)
59
+ #
60
+ def initialize( name, opts = {} )
61
+ # raise an error if a filename was not given
62
+ @fn = opts.getopt(:filename, name)
63
+ raise ArgumentError, 'no filename was given' if @fn.nil?
64
+ ::Logging::Appenders::File.assert_valid_logfile(@fn)
65
+
66
+ # grab the information we need to properly roll files
67
+ ext = ::File.extname(@fn)
68
+ bn = ::File.join(::File.dirname(@fn), ::File.basename(@fn, ext))
69
+ @rgxp = %r/\.(\d+)#{Regexp.escape(ext)}\z/
70
+ @glob = "#{bn}.*#{ext}"
71
+ @logname_fmt = "#{bn}.%d#{ext}"
72
+
73
+ # grab our options
74
+ @keep = opts.getopt(:keep, :as => Integer)
75
+ @size = opts.getopt(:size, :as => Integer)
76
+
77
+ @lockfile = if opts.getopt(:safe, false) and !::Logging::WIN32
78
+ ::Lockfile.new(
79
+ @fn + '.lck',
80
+ :retries => 1,
81
+ :timeout => 2
82
+ )
83
+ end
84
+
85
+ code = 'def sufficiently_aged?() false end'
86
+ @age_fn = @fn + '.age'
87
+
88
+ case @age = opts.getopt(:age)
89
+ when 'daily'
90
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
91
+ code = <<-CODE
92
+ def sufficiently_aged?
93
+ now = Time.now
94
+ start = ::File.mtime(@age_fn)
95
+ if (now.day != start.day) or (now - start) > 86400
96
+ return true
97
+ end
98
+ false
99
+ end
100
+ CODE
101
+ when 'weekly'
102
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
103
+ code = <<-CODE
104
+ def sufficiently_aged?
105
+ if (Time.now - ::File.mtime(@age_fn)) > 604800
106
+ return true
107
+ end
108
+ false
109
+ end
110
+ CODE
111
+ when 'monthly'
112
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
113
+ code = <<-CODE
114
+ def sufficiently_aged?
115
+ now = Time.now
116
+ start = ::File.mtime(@age_fn)
117
+ if (now.month != start.month) or (now - start) > 2678400
118
+ return true
119
+ end
120
+ false
121
+ end
122
+ CODE
123
+ when Integer, String
124
+ @age = Integer(@age)
125
+ FileUtils.touch(@age_fn) unless test(?f, @age_fn)
126
+ code = <<-CODE
127
+ def sufficiently_aged?
128
+ if (Time.now - ::File.mtime(@age_fn)) > @age
129
+ return true
130
+ end
131
+ false
132
+ end
133
+ CODE
134
+ end
135
+ meta = class << self; self end
136
+ meta.class_eval code
137
+
138
+ # if the truncate flag was set to true, then roll
139
+ roll_now = opts.getopt(:truncate, false)
140
+ roll_files if roll_now
141
+
142
+ super(name, open_logfile, opts)
143
+ end
144
+
145
+
146
+ private
147
+
148
+ # call-seq:
149
+ # write( event )
150
+ #
151
+ # Write the given _event_ to the log file. The log file will be rolled
152
+ # if the maximum file size is exceeded or if the file is older than the
153
+ # maximum age.
154
+ #
155
+ def write( event )
156
+ str = event.instance_of?(::Logging::LogEvent) ?
157
+ @layout.format(event) : event.to_s
158
+ return if str.empty?
159
+
160
+ check_logfile
161
+ super(str)
162
+
163
+ if roll_required?(str)
164
+ return roll unless @lockfile
165
+
166
+ begin
167
+ @lockfile.lock
168
+ check_logfile
169
+ roll if roll_required?
170
+ ensure
171
+ @lockfile.unlock
172
+ end
173
+ end
174
+ end
175
+
176
+ # call-seq:
177
+ # roll
178
+ #
179
+ # Close the currently open log file, roll all the log files, and open a
180
+ # new log file.
181
+ #
182
+ def roll
183
+ @io.close rescue nil
184
+ roll_files
185
+ open_logfile
186
+ end
187
+
188
+ # call-seq:
189
+ # roll_required?( str ) => true or false
190
+ #
191
+ # Returns +true+ if the log file needs to be rolled.
192
+ #
193
+ def roll_required?( str = nil )
194
+ # check if max size has been exceeded
195
+ s = if @size
196
+ @file_size = @stat.size if @stat.size > @file_size
197
+ @file_size += str.size if str
198
+ @file_size > @size
199
+ end
200
+
201
+ # check if max age has been exceeded
202
+ a = sufficiently_aged?
203
+
204
+ return (s || a)
205
+ end
206
+
207
+ # call-seq:
208
+ # roll_files
209
+ #
210
+ # Roll the log files. This is accomplished by renaming the log files
211
+ # starting with the oldest and working towards the youngest.
212
+ #
213
+ # test.10.log => deleted (we are only keeping 10)
214
+ # test.9.log => test.10.log
215
+ # test.8.log => test.9.log
216
+ # ...
217
+ # test.1.log => test.2.log
218
+ #
219
+ # Lastly the current log file is rolled to a numbered log file.
220
+ #
221
+ # test.log => test.1.log
222
+ #
223
+ # This method leaves no <tt>test.log</tt> file when it is done. This
224
+ # file will be created elsewhere.
225
+ #
226
+ def roll_files
227
+ return unless ::File.exist?(@fn)
228
+
229
+ files = Dir.glob(@glob).find_all {|fn| @rgxp =~ fn}
230
+ unless files.empty?
231
+ # sort the files in revese order based on their count number
232
+ files = files.sort do |a,b|
233
+ a = Integer(@rgxp.match(a)[1])
234
+ b = Integer(@rgxp.match(b)[1])
235
+ b <=> a
236
+ end
237
+
238
+ # for each file, roll its count number one higher
239
+ files.each do |fn|
240
+ cnt = Integer(@rgxp.match(fn)[1])
241
+ if @keep and cnt >= @keep
242
+ ::File.delete fn
243
+ next
244
+ end
245
+ ::File.rename fn, sprintf(@logname_fmt, cnt+1)
246
+ end
247
+ end
248
+
249
+ # finally reanme the base log file
250
+ ::File.rename(@fn, sprintf(@logname_fmt, 1))
251
+
252
+ # touch the age file if needed
253
+ FileUtils.touch(@age_fn) if @age
254
+ end
255
+
256
+ # call-seq:
257
+ # open_logfile => io
258
+ #
259
+ # Opens the logfile and stores the current file szie and inode.
260
+ #
261
+ def open_logfile
262
+ @io = ::File.new(@fn, 'a')
263
+ @io.sync = true
264
+
265
+ @stat = ::File.stat(@fn)
266
+ @file_size = @stat.size
267
+ @inode = @stat.ino
268
+
269
+ return @io
270
+ end
271
+
272
+ #
273
+ #
274
+ def check_logfile
275
+ retry_cnt ||= 0
276
+
277
+ @stat = ::File.stat(@fn)
278
+ return unless @lockfile
279
+ return if @inode == @stat.ino
280
+
281
+ @io.close rescue nil
282
+ open_logfile
283
+ rescue SystemCallError
284
+ raise if retry_cnt > 3
285
+ retry_cnt += 1
286
+ sleep 0.08
287
+ retry
288
+ end
289
+
290
+ end # class RollingFile
291
+ end # module Logging::Appenders
292
+
293
+ # EOF