class Evanescent
IO like object, that can be used with any logging class (such as Ruby’s native Logger). This object will save its input to a file, and allows:
-
Hourly or daily rotation.
-
Compression of rotated files.
-
Removal of old compressed files.
This functionality supplement logging classes, allowing everything related to logging management, to be done within Ruby, without relying on external tools (such as logrotate).
Constants
- PARAMS
Attributes
How long rotated files are kept (in seconds).
Current path being written to.
Rotation policy.
Public Class Methods
Shortcut for: Logger.new(Evanescent.new(opts))
. Requires logger if needed.
# File lib/evanescent.rb, line 24 def self.logger opts unless Object.const_defined? :Logger require 'logger' end Logger.new( self.new(opts) ) end
Must receive a Hash with:
:path
-
Path where to write to.
:rotation
-
Either
:hourly
or:daily
. :keep
-
For how long to keep rotated files. It is parsed with chronic_duration Gem natural language features. Examples: ‘1 day’, ‘1 month’.
# File lib/evanescent.rb, line 37 def initialize opts @path = opts[:path] @rotation = opts[:rotation] @keep = ChronicDuration.parse(opts[:keep]) @mutex = Mutex.new @last_prefix = make_suffix(Time.now) @io = nil @compress_thread = nil end
Public Instance Methods
Close file.
# File lib/evanescent.rb, line 70 def close @mutex.synchronize do @io.close end end
Compression is done in a separate thread. This method suspends current thread execution until existing compression thread returns. If no compression thread is running, returns immediately.
# File lib/evanescent.rb, line 77 def wait_compression if @compress_thread begin @compress_thread.join rescue warn("Compression thread failed: #{$!} (#{$!.class})") ensure @compress_thread = nil end end end
Writes to path
and rotate, compress and purge if necessary.
# File lib/evanescent.rb, line 48 def write string @mutex.synchronize do if new_path = rotation_path # All methods here must have exceptions threated. See: # https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L647 purge mv_path(new_path) compress end open_io if @io # No exceptions threated here, they should be handled by caller. See: # https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L653 @io.write(string) else warn("Unable to log: '#{path}' not open!") 0 end end end
Private Instance Methods
# File lib/evanescent.rb, line 163 def compress wait_compression @compress_thread = Thread.new do Dir.glob("#{path}.#{PARAMS[rotation][:glob]}").each do |uncompressed| compressed = "#{uncompressed}.gz" Zlib::GzipWriter.open(compressed) do |gz| gz.mtime = File.mtime(uncompressed) gz.orig_name = uncompressed File.open(uncompressed, 'r') do |io| io.binmode io.each do |data| gz.write(data) end end end File.delete(uncompressed) end end rescue warn("Error compressing files: #{$!} (#{$!.class})") end
# File lib/evanescent.rb, line 104 def make_suffix time time.strftime(PARAMS[rotation][:strftime]) end
# File lib/evanescent.rb, line 157 def mv_path new_path FileUtils.mv(path, new_path) rescue warn("Error renaming '#{path}' to '#{new_path}': #{$!} (#{$!.class})") end
# File lib/evanescent.rb, line 108 def open_io unless @io @io = File.open(path, File::APPEND | File::CREAT | File::WRONLY) @io.sync = true end rescue warn("Unable to open '#{path}': #{$!} (#{$!.class})") end
# File lib/evanescent.rb, line 141 def purge Dir.glob("#{path}.#{PARAMS[rotation][:glob]}.gz").each do |compressed| time_extractor = Regexp.new( '^' + Regexp.escape("#{path}.") + "(?<time>.+)" + Regexp.escape(".gz") + '$' ) time_string = compressed.match(time_extractor)[:time] compressed_time = Time.strptime(time_string, PARAMS[rotation][:strftime]) age = Time.now - compressed_time if age >= keep File.delete(compressed) end end rescue warn("Error purging old files: #{$!} (#{$!.class})") end
Returns new path for rotation. If no rotation is needed, returns nil.
# File lib/evanescent.rb, line 118 def rotation_path if @io curr_suffix = make_suffix(Time.now) return nil if curr_suffix == @last_prefix # Same as https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L760 begin @io.close rescue warn("Error closing '#{path}': #{$!} (#{$!.class})") end @io = nil @last_prefix = curr_suffix "#{path}.#{curr_suffix}" else return nil unless File.exist?(path) curr_suffix = make_suffix(Time.now+PARAMS[rotation][:interval]) rotation_suffix = make_suffix(File.mtime(path) + PARAMS[rotation][:interval]) return nil if curr_suffix == rotation_suffix @last_prefix = curr_suffix "#{path}.#{rotation_suffix}" end end