# $Id: rolling_file.rb 29 2007-02-06 19:06:16Z tim_pease $ require 'logging/appenders/file' module Logging::Appenders # # An appender that writes to a file and ensures that the file size or age # never exceeds some user specified level. # # The goal of this class is to write log messages to a file. When the file # age or size exceeds a given limit then the log file is closed, the name # is changed to indicate it is an older log file, and a new log file is # created. # # The name of the log file is changed by inserting the age of the log file # (as a single number) between the log file name and the extension. If the # file has no extension then the number is appended to the filename. Here # is a simple example: # # /var/log/ruby.log => /var/log/ruby.1.log # # New log messages will be appended to a newly opened log file of the same # name (/var/log/ruby.log in our example above). The age number # for all older log files is incremented when the log file is rolled. The # number of older log files to keep can be given, otherwise all the log # files are kept. # # The actual process of rolling all the log file names can be expensive if # there are many, many older log files to process. # class RollingFile < ::Logging::Appenders::File # # call-seq: # RollingFile.new( name, opts ) # # Creates a new Rolling File Appender. The _name_ is the unique Appender # name used to retrieve this appender from the Appender hash. The only # required option is the filename to use for creating log files. # # [:filename] The base filename to use when constructing new log # filenames. # # The following options are optional: # # [:layout] The Layout that will be used by this appender. The Basic # layout will be used if none is given. # [:truncate] When set to true any existing log files will be rolled # immediately and a new, empty log file will be created. # [:max_size] The maximum allowed size (in bytes) of a log file before # it is rolled. # [:max_age] The maximum age (in seconds) of a log file before it is # rolled. # [:keep] The number of rolled log files to keep. # def initialize( name, opts = {} ) # raise an error if a filename was not given @fn = opts[:filename] || opts['filename'] raise ArgumentError, 'no filename was given' if @fn.nil? # grab the information we need to properly roll files ext = ::File.extname(@fn) bn = ::File.join(::File.dirname(@fn), ::File.basename(@fn, ext)) @rgxp = %r/\.(\d+)#{Regexp.escape(ext)}\z/ @glob = "#{bn}.*#{ext}" @logname_fmt = "#{bn}.%d#{ext}" @keep = opts.delete(:keep) || opts.delete('keep') @keep = Integer(@keep) unless @keep.nil? # if the truncate flag was set to true, then roll roll_now = opts.delete(:truncate) || opts.delete('truncate') roll_files if roll_now # grab out our options @max_size = opts.delete(:max_size) || opts.delete('max_size') @max_age = opts.delete(:max_age) || opts.delete('max_age') @max_size = Integer(@max_size) unless @max_size.nil? unless @max_age.nil? @max_age = Integer(@max_age) @start_time = Time.now end @file_size = (::File.exist?(@fn) ? ::File.size(@fn) : 0) super(name, opts) end private # # call-seq: # write( str ) # # Write the given string to the log file. The log file will be rolled # if the maximum file size is exceeded or if the file is older than the # maximum age. # def write( str ) super @file_size += str.size # keep track of the size internally since roll if roll_required? # the file IO stream is probably not being end # flushed to disk immediately # # call-seq: # roll # # Close the currently open log file, roll all the log files, and open a # new log file. # def roll begin; @io.close; rescue; end roll_files @io = ::File.new(@fn, 'w') end # # call-seq: # roll_required? # # Returns +true+ if the log file needs to be rolled. # def roll_required? # check if max size has been exceeded if @max_size and @file_size > @max_size @file_size = 0 return true end # check if max age has been exceeded if @max_age and (Time.now - @start_time) > @max_age @start_time = Time.now return true end false end # # call-seq: # roll_files # # Roll the log files. This is accomplished by renaming the log files # starting with the oldest and working towards the youngest. # # test.10.log => deleted (we are only keeping 10) # test.9.log => test.10.log # test.8.log => test.9.log # ... # test.1.log => test.2.log # # Lastly the current log file is rolled to a numbered log file. # # test.log => test.1.log # # This method leaves no test.log file when it is done. This # file will be created elsewhere. # def roll_files return unless ::File.exist?(@fn) files = Dir.glob(@glob).find_all {|fn| @rgxp =~ fn} unless files.empty? # sort the files in revese order based on their count number files = files.sort do |a,b| a = Integer(@rgxp.match(a)[1]) b = Integer(@rgxp.match(b)[1]) b <=> a end # for each file, roll its count number one higher files.each do |fn| cnt = Integer(@rgxp.match(fn)[1]) if @keep and cnt >= @keep ::File.delete fn next end ::File.rename fn, sprintf(@logname_fmt, cnt+1) end end # finally reanme the base log file ::File.rename(@fn, sprintf(@logname_fmt, 1)) end end # class RollingFile end # module Logging::Appenders # EOF