class EventMachine::FileTail

Tail a file.

Example

class Tailer < EventMachine::FileTail
  def receive_data(data)
    puts "Got #{data.length} bytes"
  end

  # Optional
  def eof
    puts "Got EOF!"
    # If you want to stop
    stop
  end
end

# Now add it to EM
EM.run do
  EM.file_tail("/var/log/messages", Tailer)
end

# Or this way:
EM.run do
  Tailer.new("/var/log/messages")
end

See also: EventMachine::FileTail#receive_data

Constants

CHUNKSIZE

Maximum size to read at a time from a single file.

FORCE_ENCODING

MAXSLEEP = 2

Attributes

closed[R]

If this tail is closed

missing_file_check_interval[RW]

Check interval for looking for a file if we are tailing it and it has gone missing.

path[R]

The path of the file being tailed

position[R]

The current file read position

Public Class Methods

new(path, startpos=-1, &block) click to toggle source

Tail a file

  • path is a string file path to tail

  • startpos is an offset to start tailing the file at. If -1, start at end of

file.

If you want debug messages, run ruby with '-d' or set $DEBUG

See also: EventMachine::file_tail

# File lib/event_machine/tail/filetail.rb, line 72
def initialize(path, startpos=-1, &block)
  @path = path
  @logger = Logger.new(STDERR)
  @logger.level = ($DEBUG and Logger::DEBUG or Logger::WARN)
  @logger.debug("Tailing #{path} starting at position #{startpos}")

  @file = nil
  @want_eof_handling = false
  @want_read = false
  @want_reopen = false
  @reopen_on_eof = false
  @symlink_timer = nil
  @missing_file_check_timer = nil
  @read_timer = nil
  @symlink_target = nil
  @symlink_stat = nil

  @symlink_check_interval = 1
  @missing_file_check_interval = 1

  read_file_metadata

  if @filestat.directory?
    on_exception Errno::EISDIR.new(@path)
  end

  if block_given?
    @handler = block
    @buffer = BufferedTokenizer.new
  end

  EventMachine::next_tick do
    open
    next unless @file

    if (startpos == -1)
      @position = @file.sysseek(0, IO::SEEK_END)
      # TODO(sissel): if we don't have inotify or kqueue, should we
      # schedule a next read, here?
      # Is there a race condition between setting the file position and
      # watching given the two together are not atomic?
    else
      @position = @file.sysseek(startpos, IO::SEEK_SET)
      schedule_next_read
    end
    watch
  end # EventMachine::next_tick
end

Public Instance Methods

close() click to toggle source

Close this filetail

# File lib/event_machine/tail/filetail.rb, line 207
def close
  @closed = true
  @want_read = false
  EM.schedule do
    @watch.stop_watching if @watch
    EventMachine::cancel_timer(@read_timer) if @read_timer
    @symlink_timer.cancel if @symlink_timer
    @missing_file_check_timer.cancel if @missing_file_check_timer
    @file.close if @file
  end
end
closed?() click to toggle source

More rubyesque way of checking if this tail is closed

# File lib/event_machine/tail/filetail.rb, line 221
def closed?
  @closed
end
eof() click to toggle source

This method is called when a tailed file reaches EOF.

If you want to stop reading this file, call close(), otherwise this eof is handled as normal tailing does. The default EOF handler is to do nothing.

# File lib/event_machine/tail/filetail.rb, line 162
def eof
  @logger.debug { 'EOF' }
  # do nothing, subclassers should implement this.
end
on_exception(exception) click to toggle source
# File lib/event_machine/tail/filetail.rb, line 151
def on_exception(exception)
  @logger.error("Exception raised. Using default handler in #{self.class.name}")
  raise exception
end
receive_data(data) click to toggle source

This method is called when a tailed file has data read.

  • data - string data read from the file.

If you want to read lines from your file, you should use BufferedTokenizer (which comes with EventMachine):

class Tailer < EventMachine::FileTail
  def initialize(*args)
    super(*args)
    @buffer = BufferedTokenizer.new
  end

  def receive_data(data)
    @buffer.extract(data).each do |line|
      # do something with 'line'
    end
  end
# File lib/event_machine/tail/filetail.rb, line 139
def receive_data(data)
  if @handler # FileTail.new called with a block
    @buffer.extract(data).each do |line|
      @handler.call(self, line)
    end
  else
    on_exception NotImplementedError.new("#{self.class.name}#receive_data is not "\
      "implemented. Did you forget to implement this in your subclass or "\
      "module?")
  end
end

Private Instance Methods

handle_eof() click to toggle source
# File lib/event_machine/tail/filetail.rb, line 320
def handle_eof
  @want_eof_handling = false

  if @reopen_on_eof
    @reopen_on_eof = false
    schedule_reopen
  end

  # EOF actions:
  # - Check if the file inode/device is changed
  # - If symlink, check if the symlink has changed
  # - Otherwise, do nothing
  begin
    read_file_metadata do |filestat, linkstat, linktarget|
      handle_fstat(filestat, linkstat, linktarget)
    end
  rescue Errno::ENOENT
      # The file disappeared. Wait for it to reappear.
      # This can happen if it was deleted or moved during log rotation.
    @missing_file_check_timer = EM::PeriodicTimer.new(@missing_file_check_interval) do
      begin
        read_file_metadata do |filestat, linkstat, linktarget|
          handle_fstat(filestat, linkstat, linktarget)
        end
        @missing_file_check_timer.cancel
      rescue Errno::ENOENT
        # The file disappeared. Wait for it to reappear.
        # This can happen if it was deleted or moved during log rotation.
        @logger.debug "File not found, waiting for it to reappear. (#{@path})"
      end # begin/rescue ENOENT
    end # EM::PeriodicTimer
  end # begin/rescue ENOENT
end
handle_fstat(filestat, symlinkstat, symlinktarget) click to toggle source

Handle fstat changes appropriately.

# File lib/event_machine/tail/filetail.rb, line 383
def handle_fstat(filestat, symlinkstat, symlinktarget)
  # If the symlink target changes, the filestat.ino is very likely to have
  # changed since that is the stat on the resolved file (that the link points
  # to). However, we'll check explicitly for the symlink target changing
  # for better debuggability.
  if symlinktarget
    if symlinkstat.ino != @symlink_stat.ino
      @logger.debug "Inode or device changed on symlink. Reopening..."
      @reopen_on_eof = true
      schedule_next_read
    elsif symlinktarget != @symlink_target
      @logger.debug "Symlink target changed. Reopening..."
      @reopen_on_eof = true
      schedule_next_read
    end
  elsif (filestat.ino != @filestat.ino or filestat.rdev != @filestat.rdev)
    @logger.debug "Inode or device changed. Reopening..."
    @logger.debug filestat
    @reopen_on_eof = true
    schedule_next_read
  elsif (filestat.size < @filestat.size)
    # If the file size shrank, assume truncation and seek to the beginning.
    @logger.info("File likely truncated... #{path}")
    @position = @file.sysseek(0, IO::SEEK_SET)
    schedule_next_read
  end
end
notify(status) click to toggle source

notify is invoked by EM::watch_file when the file you are tailing has been modified or otherwise needs to be acted on.

# File lib/event_machine/tail/filetail.rb, line 170
def notify(status)
  @logger.debug { "notify: #{status} on #{path}" }
  if status == :modified
    schedule_next_read
  elsif status == :moved
    # read to EOF, then reopen.
    schedule_next_read
  elsif status == :unbind
    # :unbind is called after the :deleted handler
    # :deleted happens on FreeBSD's newsyslog instead of :moved
    # clean up @watch since its reference is wiped in EM's file_deleted callback
    @watch = nil
  end
end
open() click to toggle source

Open (or reopen, if necessary) our file and schedule a read.

# File lib/event_machine/tail/filetail.rb, line 187
def open
  return if @closed
  @file.close if @file && !@file.closed?
  return unless File.exists?(@path)
  begin
    @logger.debug "Opening file #{@path}"
    @file = File.open(@path, "r")
  rescue Errno::ENOENT => e
    @logger.info("File not found: '#{@path}' (#{e})")
    on_exception(e)
  end

  @naptime = 0
  @logger.debug { 'EOF' }
  @position = 0
  schedule_next_read
end
read() click to toggle source

Read CHUNKSIZE from our file and pass it to .receive_data()

# File lib/event_machine/tail/filetail.rb, line 273
def read
  return if @closed

  data = nil
  @logger.debug "#{self}: Reading..."
  begin
    data = @file.sysread(CHUNKSIZE)
  rescue EOFError, IOError
    schedule_eof
    return
  end

  data.force_encoding(@file.external_encoding) if FORCE_ENCODING

  # Won't get here if sysread throws EOF
  @position += data.length
  @naptime = 0

  # Subclasses should implement receive_data
  receive_data(data)
  schedule_next_read
end
read_file_metadata() { |filestat, symlink_stat, symlink_target| ... } click to toggle source
# File lib/event_machine/tail/filetail.rb, line 355
def read_file_metadata(&block)
  begin
    filestat = File.stat(@path)
    symlink_stat = nil
    symlink_target = nil

    if filestat.symlink?
      symlink_stat = File.lstat(@path) rescue nil
      symlink_target = File.readlink(@path) rescue nil
    end
  rescue Errno::ENOENT
    raise
  rescue => e
    @logger.debug("File stat on '#{@path}' failed")
    on_exception e
  end

  if block_given?
    yield filestat, symlink_stat, symlink_target
  end

  @filestat = filestat
  @symlink_stat = symlink_stat
  @symlink_target = symlink_target
end
schedule_eof() click to toggle source

Do EOF handling on next EM iteration

# File lib/event_machine/tail/filetail.rb, line 298
def schedule_eof
  if !@want_eof_handling
    eof # Call our own eof event
    @want_eof_handling = true
    EventMachine::next_tick do
      handle_eof
    end # EventMachine::next_tick
  end # if !@want_eof_handling
end
schedule_next_read() click to toggle source
# File lib/event_machine/tail/filetail.rb, line 261
def schedule_next_read
  if !@want_read
    @want_read = true
    @read_timer = EventMachine::add_timer(@naptime) do
      @want_read = false
      read
    end
  end # if !@want_read
end
schedule_reopen() click to toggle source
# File lib/event_machine/tail/filetail.rb, line 309
def schedule_reopen
  if !@want_reopen
    EventMachine::next_tick do
      @want_reopen = false
      open
      watch
    end
  end # if !@want_reopen
end
to_s() click to toggle source
# File lib/event_machine/tail/filetail.rb, line 411
def to_s
  return "#{self.class.name}(#{@path}) @ pos:#{@position}"
end
watch() click to toggle source

Watch our file.

# File lib/event_machine/tail/filetail.rb, line 227
def watch
  @watch.stop_watching if @watch
  @symlink_timer.cancel if @symlink_timer
  return unless File.exists?(@path)

  @logger.debug "Starting watch on #{@path}"
  callback = proc { |what| notify(what) }
  @watch = EventMachine::watch_file(@path, EventMachine::FileTail::FileWatcher, callback)
  watch_symlink if @symlink_target
end