# -----------------------------------------------------------------------------
#
# Sawmill formatter utility
#
# -----------------------------------------------------------------------------
# Copyright 2009 Daniel Azuma
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the copyright holder, nor the names of any other
# contributors to this software, may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
;
module Sawmill
class Rotater
# A rotation strategy that "shifts" log files by appending index numbers
# to the filename when the file reaches a certain size or age. So when
# the file "foo.log" is ready to rotate, it is renamed "foo.log.0", and
# a new "foo.log" is started. When that one is ready to rotate, the
# oldest "foo.log.0" is shifted down to "foo.log.1", "foo.log" is
# renamed to "foo.log.0", and a new "foo.log" is started. So the oldest
# logfile is always the one with the largest number suffix, and the
# file currently being written to has no suffix.
# This is a common rotation strategy for many unix tools.
class ShiftingLogFile
# Create a new shifting log file rotation strategy.
#
# Recognized options include:
#
# :basedir::
# The base directory used if the filepath is a relative path.
# If not specified, the current working directory is used.
# :filepath::
# The path to the log file. This may be an absolute path or a
# path relative to basedir.
# If not specified, defaults to "sawmill.log".
# :max_logfile_size::
# A logfile will try to rotate once it has reached this size in
# bytes. If not specified, the file size is not checked.
# :shift_period::
# A logfile will try to rotate once it has been in service for
# this many seconds. This parameter also recognizes the values
# :yearly, :monthly, :daily,
# and :hourly. If not specified, the file's age is
# not checked.
# :history_size::
# The maximum number of old logfiles (files with indexes) to
# keep. Files beyond this history size will be automatically
# deleted. Default is 1. This value must be at least 1.
def initialize(options_)
@max_logfile_size = options_[:max_logfile_size]
@shift_period = options_[:shift_period]
case @shift_period
when :yearly
@shift_period = 60*60*24*365
when :monthly
@shift_period = 60*60*24*30
when :daily
@shift_period = 60*60*24
when :hourly
@shift_period = 60*60
end
@history_size = options_[:history_size].to_i
@history_size = 1 if @history_size < 1 && (@max_logfile_size || @shift_period)
@normal_path = ::File.expand_path(options_[:filepath] || 'sawmill.log',
options_[:basedir] || ::Dir.getwd)
@preferred_handle = 0
@open_handles = {}
@last_shift = ::Time.now
end
# Implements the rotation strategy contract.
def preferred_handle
@preferred_handle
end
# Implements the rotation strategy contract.
def open_handle(handle_)
if handle_ == @preferred_handle
path_ = @normal_path
else
path_ = "#{@normal_path}.#{@preferred_handle-handle_-1}"
end
file_ = ::File.open(path_, ::File::CREAT | ::File::WRONLY | ::File::APPEND)
file_.sync = true
@open_handles[handle_] = true
file_
end
# Implements the rotation strategy contract.
def close_handle(handle_, io_)
io_.close
if @preferred_handle - handle_ > @history_size
::File.delete("#{@normal_path}.#{@preferred_handle-handle_-1}") rescue nil
end
@open_handles.delete(handle_)
nil
end
# Implements the rotation strategy contract.
def before_write
return unless @max_logfile_size || @shift_period
turnover_ = false
if @max_logfile_size && ::File.file?(@normal_path) && ::File.size(@normal_path) > @max_logfile_size
turnover_ = true
end
if @shift_period && (::Time.now - @last_shift) > @shift_period
turnover_ = true
end
if turnover_
max_ = @preferred_handle - @open_handles.keys.min + 1
max_ = @history_size if max_ < @history_size
::File.delete("#{@normal_path}.#{max_-1}") rescue nil
(max_-1).downto(1) do |index_|
::File.rename("#{@normal_path}.#{index_-1}", "#{@normal_path}.#{index_}") rescue nil
end
::File.rename("#{@normal_path}", "#{@normal_path}.0") rescue nil
@preferred_handle += 1
@last_shift = ::Time.now
end
end
end
end
end