1 # frozen_string_literal: true
3 require_relative 'period'
6 # Device used for logging messages.
14 def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false, reraise_write_errors: [])
15 @dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
17 @reraise_write_errors = reraise_write_errors
21 @shift_age = shift_age || 7
22 @shift_size = shift_size || 1048576
23 @shift_period_suffix = shift_period_suffix || '%Y%m%d'
25 unless @shift_age.is_a?(Integer)
26 base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
27 @next_rotate_time = next_rotate_time(base_time, @shift_age)
33 handle_write_errors("writing") do
35 if @shift_age and @dev.respond_to?(:stat)
36 handle_write_errors("shifting") {check_shift_log}
38 handle_write_errors("writing") {@dev.write(message)}
54 # reopen the same filename if no argument, do nothing for IO
55 log ||= @filename if @filename
59 @dev.close rescue nil # close only file opened by Logger
72 MODE = File::WRONLY | File::APPEND
73 MODE_TO_OPEN = MODE | File::SHARE_DELETE | File::BINARY
74 MODE_TO_CREATE = MODE_TO_OPEN | File::CREAT | File::EXCL
77 if log.respond_to?(:write) and log.respond_to?(:close)
79 if log.respond_to?(:path) and path = log.path
85 @dev = open_logfile(log)
90 if MODE_TO_OPEN == MODE
91 def fixup_mode(dev, filename)
95 def fixup_mode(dev, filename)
96 return dev if @binmode
99 dev = File.new(dev.fileno, mode: MODE, path: filename)
101 PathAttr.set_path(dev, filename) if defined?(PathAttr)
106 def open_logfile(filename)
108 dev = File.open(filename, MODE_TO_OPEN)
110 create_logfile(filename)
112 dev = fixup_mode(dev, filename)
114 dev.binmode if @binmode
119 def create_logfile(filename)
121 logdev = File.open(filename, MODE_TO_CREATE)
122 logdev.flock(File::LOCK_EX)
123 logdev = fixup_mode(logdev, filename)
125 logdev.binmode if @binmode
126 add_log_header(logdev)
127 logdev.flock(File::LOCK_UN)
130 # file is created by another process
131 open_logfile(filename)
135 def handle_write_errors(mesg)
137 rescue *@reraise_write_errors
140 warn("log #{mesg} failed. #{$!}")
143 def add_log_header(file)
145 "# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
150 if @shift_age.is_a?(Integer)
151 # Note: always returns false if '0'.
152 if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
153 lock_shift_log { shift_log_age }
157 if now >= @next_rotate_time
158 @next_rotate_time = next_rotate_time(now, @shift_age)
159 lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
168 File.open(@filename, MODE_TO_OPEN) do |lock|
169 lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
170 if File.identical?(@filename, lock) and File.identical?(lock, @dev)
173 # log shifted by another process (i-node before locking and i-node after locking are different)
174 @dev.close rescue nil
175 @dev = open_logfile(@filename)
179 # @filename file would not exist right after #rename and before #create_logfile
181 warn("log rotation inter-process lock failed. #{$!}")
190 warn("log rotation inter-process lock failed. #{$!}")
194 (@shift_age-3).downto(0) do |i|
195 if FileTest.exist?("#{@filename}.#{i}")
196 File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
199 @dev.close rescue nil
200 File.rename("#{@filename}", "#{@filename}.0")
201 @dev = create_logfile(@filename)
205 def shift_log_period(period_end)
206 suffix = period_end.strftime(@shift_period_suffix)
207 age_file = "#{@filename}.#{suffix}"
208 if FileTest.exist?(age_file)
209 # try to avoid filename crash caused by Timestamp change.
211 # .99 can be overridden; avoid too much file search with 'loop do'
214 age_file = "#{@filename}.#{suffix}.#{idx}"
215 break unless FileTest.exist?(age_file)
218 @dev.close rescue nil
219 File.rename("#{@filename}", age_file)
220 @dev = create_logfile(@filename)
226 File.open(IO::NULL) do |f|
227 File.new(f.fileno, autoclose: false, path: "").path
229 module PathAttr # :nodoc:
232 def self.set_path(file, path)
233 file.extend(self).instance_variable_set(:@path, path)