# frozen_string_literals: true
module Lumberjack
class Device
# This is an abstract class for a device that appends entries to a file and periodically archives
# the existing file and starts a one. Subclasses must implement the roll_file? and archive_file_suffix
# methods.
#
# The :keep option can be used to specify a maximum number of rolled log files to keep.
# Older files will be deleted based on the time they were created. The default is to keep all files.
#
# The :min_roll_check option can be used to specify the number of seconds between checking
# the file to determine if it needs to be rolled. The default is to check at most once per second.
class RollingLogFile < LogFile
attr_reader :path
attr_accessor :keep
def initialize(path, options = {})
@path = File.expand_path(path)
@keep = options[:keep]
super(path, options)
@file_inode = stream.lstat.ino rescue nil
@@rolls = []
@next_stat_check = Time.now.to_f
@min_roll_check = (options[:min_roll_check] || 1.0).to_f
end
# Returns a suffix that will be appended to the file name when it is archived.. The suffix should
# change after it is time to roll the file. The log file will be renamed when it is rolled.
def archive_file_suffix
raise NotImplementedError
end
# Return +true+ if the file should be rolled.
def roll_file?
raise NotImplementedError
end
# Roll the log file by renaming it to the archive file name and then re-opening a stream to the log
# file path. Rolling a file is safe in multi-threaded or multi-process environments.
def roll_file! #:nodoc:
do_once(stream) do
archive_file = "#{path}.#{archive_file_suffix}"
stream.flush
current_inode = File.stat(path).ino rescue nil
if @file_inode && current_inode == @file_inode && !File.exist?(archive_file) && File.exist?(path)
begin
File.rename(path, archive_file)
after_roll
cleanup_files!
rescue SystemCallError
# Ignore rename errors since it indicates the file was already rolled
end
end
reopen_file
end
rescue => e
$stderr.write("Failed to roll file #{path}: #{e.inspect}\n#{e.backtrace.join("\n")}\n")
end
protected
# This method will be called after a file has been rolled. Subclasses can
# implement code to reset the state of the device. This method is thread safe.
def after_roll
end
# Handle rolling the file before flushing.
def before_flush # :nodoc:
if @min_roll_check <= 0.0 || Time.now.to_f >= @next_stat_check
@next_stat_check += @min_roll_check
path_inode = File.lstat(path).ino rescue nil
if path_inode != @file_inode
@file_inode = path_inode
reopen_file
else
roll_file! if roll_file?
end
end
end
private
def reopen_file
old_stream = stream
new_stream = File.open(path, 'a', encoding: EXTERNAL_ENCODING)
new_stream.sync = true if buffer_size > 0
@file_inode = new_stream.lstat.ino rescue nil
self.stream = new_stream
old_stream.close
end
end
def cleanup_files!
if keep
files = Dir.glob("#{path}.*").collect{|f| [f, File.ctime(f)]}.sort{|a,b| b.last <=> a.last}.collect{|a| a.first}
if files.size > keep
files[keep, files.length].each do |f|
File.delete(f)
end
end
end
end
def do_once(file)
begin
file.flock(File::LOCK_EX)
rescue SystemCallError
# Most likely can't lock file because the stream is closed
return
end
begin
verify = file.lstat rescue nil
# Execute only if the file we locked is still the same one that needed to be rolled
yield if verify && verify.ino == @file_inode && verify.size > 0
ensure
file.flock(File::LOCK_UN) rescue nil
end
end
end
end