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::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.
# [:safe] When set to true, extra checks are made to ensure that
# only once process can roll the log files; this option
# should only be used when multiple processes will be
# logging to the same log file (does not work on Windows)
#
def initialize( name, opts = {} )
# raise an error if a filename was not given
@fn = opts.getopt(:filename, name)
raise ArgumentError, 'no filename was given' if @fn.nil?
::Logging::Appenders::File.assert_valid_logfile(@fn)
# 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}"
# grab our options
@keep = opts.getopt(:keep, :as => Integer)
@size = opts.getopt(:size, :as => Integer)
@lockfile = if opts.getopt(:safe, false) and !::Logging::WIN32
::Lockfile.new(
@fn + '.lck',
:retries => 1,
:timeout => 2
)
end
code = 'def sufficiently_aged?() false end'
@age_fn = @fn + '.age'
case @age = opts.getopt(:age)
when 'daily'
FileUtils.touch(@age_fn) unless test(?f, @age_fn)
code = <<-CODE
def sufficiently_aged?
now = Time.now
start = ::File.mtime(@age_fn)
if (now.day != start.day) or (now - start) > 86400
return true
end
false
end
CODE
when 'weekly'
FileUtils.touch(@age_fn) unless test(?f, @age_fn)
code = <<-CODE
def sufficiently_aged?
if (Time.now - ::File.mtime(@age_fn)) > 604800
return true
end
false
end
CODE
when 'monthly'
FileUtils.touch(@age_fn) unless test(?f, @age_fn)
code = <<-CODE
def sufficiently_aged?
now = Time.now
start = ::File.mtime(@age_fn)
if (now.month != start.month) or (now - start) > 2678400
return true
end
false
end
CODE
when Integer, String
@age = Integer(@age)
FileUtils.touch(@age_fn) unless test(?f, @age_fn)
code = <<-CODE
def sufficiently_aged?
if (Time.now - ::File.mtime(@age_fn)) > @age
return true
end
false
end
CODE
end
meta = class << self; self end
meta.class_eval code, __FILE__, __LINE__
# if the truncate flag was set to true, then roll
roll_now = opts.getopt(:truncate, false)
roll_files if roll_now
super(name, open_logfile, opts)
end
# Returns the path to the logfile.
#
def filename() @fn.dup end
private
# call-seq:
# write( event )
#
# 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 write( event )
str = event.instance_of?(::Logging::LogEvent) ?
@layout.format(event) : event.to_s
return if str.empty?
check_logfile
super(str)
if roll_required?(str)
return roll unless @lockfile
@lockfile.lock {
check_logfile
roll if roll_required?
}
end
end
# call-seq:
# roll
#
# Close the currently open log file, roll all the log files, and open a
# new log file.
#
def roll
@io.close rescue nil
roll_files
open_logfile
end
# call-seq:
# roll_required?( str ) => true or false
#
# Returns +true+ if the log file needs to be rolled.
#
def roll_required?( str = nil )
# check if max size has been exceeded
s = if @size
@file_size = @stat.size if @stat.size > @file_size
@file_size += str.size if str
@file_size > @size
end
# check if max age has been exceeded
a = sufficiently_aged?
return (s || a)
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))
# touch the age file if needed
FileUtils.touch(@age_fn) if @age
end
# call-seq:
# open_logfile => io
#
# Opens the logfile and stores the current file szie and inode.
#
def open_logfile
@io = ::File.new(@fn, 'a')
@io.sync = true
@stat = ::File.stat(@fn)
@file_size = @stat.size
@inode = @stat.ino
return @io
end
#
#
def check_logfile
retry_cnt ||= 0
if ::File.exist?(@fn) then
@stat = ::File.stat(@fn)
return unless @lockfile
return if @inode == @stat.ino
@io.close rescue nil
end
open_logfile
rescue SystemCallError
raise if retry_cnt > 3
retry_cnt += 1
sleep 0.08
retry
end
end # class RollingFile
end # module Logging::Appenders
# EOF