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:

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

keep[R]

How long rotated files are kept (in seconds).

path[R]

Current path being written to.

rotation[R]

Rotation policy.

Public Class Methods

logger(opts) click to toggle source

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
new(opts) click to toggle source

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() click to toggle source

Close file.

# File lib/evanescent.rb, line 70
def close
  @mutex.synchronize do
    @io.close
  end
end
wait_compression() click to toggle source

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
write(string) click to toggle source

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

compress() click to toggle source
# 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
make_suffix(time) click to toggle source
# File lib/evanescent.rb, line 104
def make_suffix time
  time.strftime(PARAMS[rotation][:strftime])
end
mv_path(new_path) click to toggle source
# 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
open_io() click to toggle source
# 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
purge() click to toggle source
# 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
rotation_path() click to toggle source

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