[ruby/logger] Enable log file rotation on Windows
[ruby.git] / lib / logger / log_device.rb
blob4c46278edd1268e37efb11e01d8064d8ba4093bf
1 # frozen_string_literal: true
3 require_relative 'period'
5 class Logger
6   # Device used for logging messages.
7   class LogDevice
8     include Period
10     attr_reader :dev
11     attr_reader :filename
12     include MonitorMixin
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
16       @binmode = binmode
17       @reraise_write_errors = reraise_write_errors
18       mon_initialize
19       set_dev(log)
20       if @filename
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)
28         end
29       end
30     end
32     def write(message)
33       handle_write_errors("writing") do
34         synchronize do
35           if @shift_age and @dev.respond_to?(:stat)
36             handle_write_errors("shifting") {check_shift_log}
37           end
38           handle_write_errors("writing") {@dev.write(message)}
39         end
40       end
41     end
43     def close
44       begin
45         synchronize do
46           @dev.close rescue nil
47         end
48       rescue Exception
49         @dev.close rescue nil
50       end
51     end
53     def reopen(log = nil)
54       # reopen the same filename if no argument, do nothing for IO
55       log ||= @filename if @filename
56       if log
57         synchronize do
58           if @filename and @dev
59             @dev.close rescue nil # close only file opened by Logger
60             @filename = nil
61           end
62           set_dev(log)
63         end
64       end
65       self
66     end
68   private
70     # :stopdoc:
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
76     def set_dev(log)
77       if log.respond_to?(:write) and log.respond_to?(:close)
78         @dev = log
79         if log.respond_to?(:path) and path = log.path
80           if File.exist?(path)
81             @filename = path
82           end
83         end
84       else
85         @dev = open_logfile(log)
86         @filename = log
87       end
88     end
90     if MODE_TO_OPEN == MODE
91       def fixup_mode(dev, filename)
92         dev
93       end
94     else
95       def fixup_mode(dev, filename)
96         return dev if @binmode
97         dev.autoclose = false
98         old_dev = dev
99         dev = File.new(dev.fileno, mode: MODE, path: filename)
100         old_dev.close
101         PathAttr.set_path(dev, filename) if defined?(PathAttr)
102         dev
103       end
104     end
106     def open_logfile(filename)
107       begin
108         dev = File.open(filename, MODE_TO_OPEN)
109       rescue Errno::ENOENT
110         create_logfile(filename)
111       else
112         dev = fixup_mode(dev, filename)
113         dev.sync = true
114         dev.binmode if @binmode
115         dev
116       end
117     end
119     def create_logfile(filename)
120       begin
121         logdev = File.open(filename, MODE_TO_CREATE)
122         logdev.flock(File::LOCK_EX)
123         logdev = fixup_mode(logdev, filename)
124         logdev.sync = true
125         logdev.binmode if @binmode
126         add_log_header(logdev)
127         logdev.flock(File::LOCK_UN)
128         logdev
129       rescue Errno::EEXIST
130         # file is created by another process
131         open_logfile(filename)
132       end
133     end
135     def handle_write_errors(mesg)
136       yield
137     rescue *@reraise_write_errors
138       raise
139     rescue
140       warn("log #{mesg} failed. #{$!}")
141     end
143     def add_log_header(file)
144       file.write(
145         "# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
146       ) if file.size == 0
147     end
149     def check_shift_log
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 }
154         end
155       else
156         now = Time.now
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)) }
160         end
161       end
162     end
164     def lock_shift_log
165       retry_limit = 8
166       retry_sleep = 0.1
167       begin
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)
171             yield # log shifting
172           else
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)
176           end
177         end
178       rescue Errno::ENOENT
179         # @filename file would not exist right after #rename and before #create_logfile
180         if retry_limit <= 0
181           warn("log rotation inter-process lock failed. #{$!}")
182         else
183           sleep retry_sleep
184           retry_limit -= 1
185           retry_sleep *= 2
186           retry
187         end
188       end
189     rescue
190       warn("log rotation inter-process lock failed. #{$!}")
191     end
193     def shift_log_age
194       (@shift_age-3).downto(0) do |i|
195         if FileTest.exist?("#{@filename}.#{i}")
196           File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
197         end
198       end
199       @dev.close rescue nil
200       File.rename("#{@filename}", "#{@filename}.0")
201       @dev = create_logfile(@filename)
202       return true
203     end
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.
210         idx = 0
211         # .99 can be overridden; avoid too much file search with 'loop do'
212         while idx < 100
213           idx += 1
214           age_file = "#{@filename}.#{suffix}.#{idx}"
215           break unless FileTest.exist?(age_file)
216         end
217       end
218       @dev.close rescue nil
219       File.rename("#{@filename}", age_file)
220       @dev = create_logfile(@filename)
221       return true
222     end
223   end
226 File.open(IO::NULL) do |f|
227   File.new(f.fileno, autoclose: false, path: "").path
228 rescue IOError
229   module PathAttr               # :nodoc:
230     attr_reader :path
232     def self.set_path(file, path)
233       file.extend(self).instance_variable_set(:@path, path)
234     end
235   end