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 copied and then
# truncated. The name of the copy indicates it is an older log file.
#
# 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 continue to be appended to the same log file
# (/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.
#
# If you do not wish to use numbered files when rolling, you can specify the
# :roll_by option as 'date'. This will use a date/time stamp to
# differentiate the older files from one another. If you configure your
# rolling file appender to roll daily and ignore the file size:
#
# /var/log/ruby.log => /var/log/ruby.20091225.log
#
# Where the date is expressed as %Y%m%d in the Time#strftime format.
#
# NOTE: this class is not safe to use when log messages are written to files
# on NFS mounts or other remote file system. It should only be used for log
# files on the local file system. The exception to this is when a single
# process is writing to the log file; remote file systems are safe to
# use in this case but still not recommended.
#
class RollingFile < ::Logging::Appenders::IO
# 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.
# [:size] The maximum allowed size (in bytes) of a log file before
# it is rolled.
# [:age] The maximum age (in seconds) of a log file before it is
# rolled. The age can also be given as 'daily', 'weekly',
# or 'monthly'.
# [:keep] The number of rolled log files to keep.
# [:roll_by] How to name the rolled log files. This can be 'number' or
# 'date'.
#
def initialize( name, opts = {} )
# raise an error if a filename was not given
@fn = opts.getopt(:filename, name)
@fn_copy = @fn + '._copy_'
raise ArgumentError, 'no filename was given' if @fn.nil?
::Logging::Appenders::File.assert_valid_logfile(@fn)
# grab our options
@size = opts.getopt(:size, :as => Integer)
code = 'def sufficiently_aged?() false end'
@age_fn = @fn + '.age'
@age_fn_mtime = nil
case @age = opts.getopt(:age)
when 'daily'
code = <<-CODE
def sufficiently_aged?
@age_fn_mtime ||= ::File.mtime(@age_fn)
now = Time.now
if (now.day != @age_fn_mtime.day) or (now - @age_fn_mtime) > 86400
return true
end
false
end
CODE
when 'weekly'
code = <<-CODE
def sufficiently_aged?
@age_fn_mtime ||= ::File.mtime(@age_fn)
if (Time.now - @age_fn_mtime) > 604800
return true
end
false
end
CODE
when 'monthly'
code = <<-CODE
def sufficiently_aged?
@age_fn_mtime ||= ::File.mtime(@age_fn)
now = Time.now
if (now.month != @age_fn_mtime.month) or (now - @age_fn_mtime) > 2678400
return true
end
false
end
CODE
when Integer, String
@age = Integer(@age)
code = <<-CODE
def sufficiently_aged?
@age_fn_mtime ||= ::File.mtime(@age_fn)
if (Time.now - @age_fn_mtime) > @age
return true
end
false
end
CODE
end
FileUtils.touch(@age_fn) if @age and !test(?f, @age_fn)
meta = class << self; self end
meta.class_eval code, __FILE__, __LINE__
# we are opening the file in read/write mode so that a shared lock can
# be used on the file descriptor => http://pubs.opengroup.org/onlinepubs/009695399/functions/fcntl.html
super(name, ::File.new(@fn, 'a+'), opts)
# setup the file roller
@roller =
case opts.getopt(:roll_by)
when 'number'; NumberedRoller.new(@fn, opts)
when 'date'; DateRoller.new(@fn, opts)
else
(@age and !@size) ?
DateRoller.new(@fn, opts) :
NumberedRoller.new(@fn, opts)
end
# if the truncate flag was set to true, then roll
roll_now = opts.getopt(:truncate, false)
if roll_now
copy_truncate
@roller.roll_files
end
end
# Returns the path to the logfile.
#
def filename() @fn.dup end
# Reopen the connection to the underlying logging destination. If the
# connection is currently closed then it will be opened. If the connection
# is currently open then it will be closed and immediately opened.
#
def reopen
@mutex.synchronize {
if defined? @io and @io
flush
@io.close rescue nil
end
@closed = false
@io = ::File.new(@fn, 'a+')
}
self
end
private
# Write the given _event_ 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 canonical_write( str )
return self if @io.nil?
@io.flock_sh { @io.syswrite(str) }
if roll_required?
@io.flock? {
@age_fn_mtime = nil
copy_truncate if roll_required?
}
@roller.roll_files
end
self
rescue StandardError => err
self.level = :off
::Logging.log_internal {"appender #{name.inspect} has been disabled"}
::Logging.log_internal(-2) {err}
end
# Returns +true+ if the log file needs to be rolled.
#
def roll_required?
return false if ::File.exist? @fn_copy
# check if max size has been exceeded
s = @size ? ::File.size(@fn) > @size : false
# check if max age has been exceeded
a = sufficiently_aged?
return (s || a)
end
# Copy the contents of the logfile to another file. Truncate the logfile
# to zero length. This method will set the roll flag so that all the
# current logfiles will be rolled along with the copied file.
#
def copy_truncate
return unless ::File.exist?(@fn)
FileUtils.copy @fn, @fn_copy
@io.truncate 0
# touch the age file if needed
if @age
FileUtils.touch @age_fn
@age_fn_mtime = nil
end
@roller.roll = true
end
# :stopdoc:
class NumberedRoller
attr_accessor :roll
def initialize( fn, opts )
# 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}"
@fn_copy = fn + '._copy_'
@keep = opts.getopt(:keep, :as => Integer)
@roll = false
end
def roll_files
return unless @roll and ::File.exist?(@fn_copy)
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 copied log file
::File.rename(@fn_copy, sprintf(@logname_fmt, 1))
ensure
@roll = false
end
end
class DateRoller
attr_accessor :roll
def initialize( fn, opts )
@fn_copy = fn + '._copy_'
@roll = false
@keep = opts.getopt(:keep, :as => Integer)
ext = ::File.extname(fn)
bn = ::File.join(::File.dirname(fn), ::File.basename(fn, ext))
if @keep
@rgxp = %r/\.(\d+)(-\d+)?#{Regexp.escape(ext)}\z/
@glob = "#{bn}.*#{ext}"
end
if %w[daily weekly monthly].include?(opts.getopt(:age)) and !opts.getopt(:size)
@logname_fmt = "#{bn}.%Y%m%d#{ext}"
else
@logname_fmt = "#{bn}.%Y%m%d-%H%M%S#{ext}"
end
end
def roll_files
return unless @roll and ::File.exist?(@fn_copy)
# reanme the copied log file
::File.rename(@fn_copy, Time.now.strftime(@logname_fmt))
# prune old log files
if @keep
files = Dir.glob(@glob).find_all {|fn| @rgxp =~ fn}
length = files.length
if length > @keep
files.sort {|a,b| b <=> a}.last(length-@keep).each {|fn| ::File.delete fn}
end
end
ensure
@roll = false
end
end
# :startdoc:
end # class RollingFile
end # module Logging::Appenders