filterfish-logging 0.9.8

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