# :nodoc: # Version:: $Id: rollingfileoutputter.rb,v 1.2 2009/09/29 18:13:13 colbygk Exp $ require "log4r/outputter/fileoutputter" require "log4r/staticlogger" require 'fileutils' module Log4r # RollingFileOutputter - subclass of FileOutputter that rolls files on size # or time. So, given a filename of "error.log", the first log file will be "error000001.log". # When its check condition is exceeded, it'll create and log to "error000002.log", etc. # # Additional hash arguments are: # # [:maxsize] Maximum size of the file in bytes. # [:maxtime] Maximum age of the file in seconds. # [:max_backups] Maxium number of prior log files to maintain. If max_backups is a positive number, # then each time a roll happens, RollingFileOutputter will delete the oldest backup log files in excess # of this number (if any). So, if max_backups is 10, then a maximum of 11 files will be maintained (the current # log, plus 10 backups). If max_backups is 0, no backups will be kept. If it is negative (the default), # there will be no limit on the number of files created. Note that the sequence numbers will continue to escalate; # old sequence numbers are not reused. # [:trunc] If true, deletes ALL existing log files (based on :filename) upon initialization, # and the sequence numbering will start over at 000001. Otherwise continues logging where it left off # last time (i.e. either to the file with the highest sequence number, or a new file, as appropriate). class RollingFileOutputter < FileOutputter attr_reader :current_sequence_number, :maxsize, :maxtime, :start_time, :max_backups def initialize(_name, hash={}) super( _name, hash.merge({:create => false}) ) if hash.has_key?(:maxsize) || hash.has_key?('maxsize') _maxsize = (hash[:maxsize] or hash['maxsize']).to_i if _maxsize.class != Fixnum raise TypeError, "Argument 'maxsize' must be an Fixnum", caller end if _maxsize == 0 raise TypeError, "Argument 'maxsize' must be > 0", caller end @maxsize = _maxsize end if hash.has_key?(:maxtime) || hash.has_key?('maxtime') _maxtime = (hash[:maxtime] or hash['maxtime']).to_i if _maxtime.class != Fixnum raise TypeError, "Argument 'maxtime' must be an Fixnum", caller end if _maxtime == 0 raise TypeError, "Argument 'maxtime' must be > 0", caller end @maxtime = _maxtime end if hash.has_key?(:max_backups) || hash.has_key?('max_backups') _max_backups = (hash[:max_backups] or hash['max_backups']).to_i if _max_backups.class != Fixnum raise TypeError, "Argument 'max_backups' must be an Fixnum", caller end @max_backups = _max_backups else @max_backups = -1 end # @filename starts out as the file (including path) provided by the user, e.g. "\usr\logs\error.log". # It will get assigned the current log file (including sequence number) # @log_dir is the directory in which we'll log, e.g. "\usr\logs" # @file_extension is the file's extension (if any) including any period, e.g. ".log" # @core_file_name is the part of the log file's name, sans sequence digits or extension, e.g. "error" @log_dir = File.dirname(@filename) @file_extension = File.extname(@filename) # Note: the File API doc comment states that this doesn't include the period, but its examples and behavior do include it. We'll depend on the latter. @core_file_name = File.basename(@filename, @file_extension) if (@trunc) purge_log_files(0) end @current_sequence_number = get_current_sequence_number() makeNewFilename # Now @filename points to a properly sequenced filename, which may or may not yet exist. open_log_file('a') # Note: it's possible we're already in excess of our time or size constraint for the current file; # no worries -- if a new file needs to be started, it'll happen during the write() call. end ####### private ####### # Delete all but the latest number_to_keep log files. def purge_log_files(number_to_keep) Dir.chdir(@log_dir) do # Make a list of the log files to delete. Start with all of the matching log files... glob = "#{@core_file_name}[0-9][0-9][0-9][0-9][0-9][0-9]#{@file_extension}" files = Dir.glob(glob) # ... if there are fewer than our threshold, just return... if (files.size() <= number_to_keep ) # Logger.log_internal {"No log files need purging."} return end # ...then remove those that we want to keep (i.e. the most recent #{number_to_keep} files). files.sort!().slice!(-number_to_keep, number_to_keep) # Delete the files. We use force (rm_f), so in case any files can't be deleted (e.g. someone's got one # open in an editor), we'll swallow the error and keep going. FileUtils.rm_f(files) Logger.log_internal { "Purged #{files.length} log files: #{files}" } end end # Get the highest existing log file sequence number, or 1 if there are no existing log files. def get_current_sequence_number() max_seq_no = 0 Dir.foreach(@log_dir) do |child| if child =~ /^#{@core_file_name}(\d+)#{@file_extension}$/ seq_no = $1.to_i if (seq_no > max_seq_no) max_seq_no = seq_no end end end return [max_seq_no, 1].max end # perform the write def write(data) # we have to keep track of the file size ourselves - File.size doesn't # seem to report the correct size when the size changes rapidly @datasize += data.size + 1 # the 1 is for newline roll if requiresRoll super end # Constructs a new filename from the @current_sequence_number, @core_file_name, and @file_extension, # and assigns it to @filename def makeNewFilename # note use of hard coded 6 digit sequence width - is this enough files? padded_seq_no = "0" * (6 - @current_sequence_number.to_s.length) + @current_sequence_number.to_s newbase = "#{@core_file_name}#{padded_seq_no}#{@file_extension}" @filename = File.join(@log_dir, newbase) end # Open @filename with the given mode: # 'a' - appends to the end of the file if it exists; otherwise creates it. # 'w' - truncates the file to zero length if it exists, otherwise creates it. # Re-initializes @datasize and @startime appropriately. def open_log_file(mode) # It appears that if a file has been recently deleted then recreated, calls like # File.ctime can return the erstwhile creation time. File.size? can similarly return # old information. So instead of simply doing ctime and size checks after File.new, we # do slightly more complicated checks beforehand: if (mode == 'w' || !File.exists?(@filename)) @start_time = Time.now() @datasize = 0 else @start_time = File.ctime(@filename) @datasize = File.size?(@filename) || 0 # File.size? returns nil even if the file exists but is empty; we convert it to 0. end @out = File.new(@filename, mode) Logger.log_internal {"File #{@filename} opened with mode #{mode}"} end # does the file require a roll? def requiresRoll if !@maxsize.nil? && @datasize > @maxsize Logger.log_internal { "Rolling because #{@filename} (#{@datasize} bytes) has exceded the maxsize limit (#{@maxsize} bytes)." } return true end if !@maxtime.nil? && (Time.now - @start_time) > @maxtime Logger.log_internal { "Rolling because #{@filename} (created: #{@start_time}) has exceded the maxtime age (#{@maxtime} seconds)." } return true end false end # roll the file def roll begin # If @baseFilename == @filename, then this method is about to # try to close out a file that is not actually opened because # fileoutputter has been called with the parameter roll=true # TODO: Is this check valid any more? I suspect not. Am commenting out...: #if ( @baseFilename != @filename ) then @out.close #end rescue Logger.log_internal { "RollingFileOutputter '#{@name}' could not close #{@filename}" } end # Prepare the next file. (Note: if max_backups is zero, we can skip this; we'll # just overwrite the existing log file) if (@max_backups != 0) @current_sequence_number += 1 makeNewFilename end open_log_file('w') # purge any excess log files (unless max_backups is negative, which means don't purge). if (@max_backups >= 0) purge_log_files(@max_backups + 1) end end end end # this can be found in examples/fileroll.rb as well if __FILE__ == $0 require 'log4r' include Log4r timeLog = Logger.new 'WbExplorer' timeLog.outputters = RollingFileOutputter.new("WbExplorer", { "filename" => "TestTime.log", "maxtime" => 10, "trunc" => true }) timeLog.level = DEBUG 100.times { |t| timeLog.info "blah #{t}" sleep(1.0) } sizeLog = Logger.new 'WbExplorer' sizeLog.outputters = RollingFileOutputter.new("WbExplorer", { "filename" => "TestSize.log", "maxsize" => 16000, "trunc" => true }) sizeLog.level = DEBUG 10000.times { |t| sizeLog.info "blah #{t}" } end