rudionrails/yell

View on GitHub
lib/yell/adapters/datefile.rb

Summary

Maintainability
A
25 mins
Test Coverage
module Yell #:nodoc:
  module Adapters #:nodoc:

    # The +Datefile+ adapter is similar to the +File+ adapter. However, it
    # rotates the file at midnight (by default).
    class Datefile < Yell::Adapters::File

      # The default date pattern, e.g. "19820114" (14 Jan 1982)
      DefaultDatePattern = "%Y%m%d"

      # Metadata
      Header = lambda { |date, pattern| "# -*- #{date.iso8601} (#{date.to_f}) [#{pattern}] -*-" }
      HeaderRegexp = /^# -\*- (.+) \((\d+\.\d+)\) \[(.+)\] -\*-$/

      # The pattern to be used for the files
      #
      # @example
      #   date_pattern = "%Y%m%d"       # default
      #   date_pattern = "%Y-week-%V"
      attr_accessor :date_pattern

      # Tell the adapter to create a symlink onto the currently 
      # active (timestamped) file. Upon rollover, the symlink is 
      # set to the newly created file, and so on.
      #
      # @example
      #   symlink = true
      attr_accessor :symlink

      # Set the amount of logfiles to keep when rolling over.
      # By default, no files will be cleaned up.
      #
      # @example Keep the last 5 logfiles
      #   keep = 5
      #   keep = '10'
      #
      # @example Do not clean up any files
      #   keep = 0
      attr_accessor :keep

      # You can suppress the first line of the logfile that contains
      # the metadata. This is important upon rollover, because on *nix 
      # systems, it is not possible to determine the creation time of a file, 
      # on the last access time. The header compensates this.
      #
      # @example
      #   header = false
      attr_accessor :header


      private

      # @overload setup!( options )
      def setup!( options )
        self.header = Yell.__fetch__(options, :header, default: true)
        self.date_pattern = Yell.__fetch__(options, :date_pattern, default: DefaultDatePattern)
        self.keep = Yell.__fetch__(options, :keep, default: false)
        self.symlink = Yell.__fetch__(options, :symlink, default: true)

        @original_filename  = ::File.expand_path(Yell.__fetch__(options, :filename, default: default_filename))
        options[:filename]  = @original_filename

        @date = Time.now
        @date_strftime = @date.strftime(date_pattern)

        super
      end

      # @overload write!( event )
      def write!( event )
        # do nothing when not closing
        return super unless close?
        close

        # exit when file ready present
        return super if ::File.exist?(@filename)

        header! if header?
        symlink! if symlink?
        cleanup! if cleanup?

        super
      end

      # @overload close!
      def close!
        @filename = filename_for(@date)

        super
      end

      # Determine whether to close the file handle or not.
      #
      # It is based on the `:date_pattern` (can be passed as option upon initialize). 
      # If the current time hits the pattern, it closes the file stream.
      #
      # @return [Boolean] true or false
      def close?
        _date           = Time.now
        _date_strftime  = _date.strftime(date_pattern)

        if @stream.nil? or _date_strftime != @date_strftime
          @date, @date_strftime = _date, _date_strftime

          return true
        end

        false
      end

      # Removes old logfiles of the same date pattern.
      #
      # By reading the header of the files that match the date pattern, the
      # adapter determines whether to remove them or not. If no header is present, 
      # it makes the best guess by checking the last access time (which may result 
      # in false cleanups).
      def cleanup!
        files = Dir[ @original_filename.sub(/(\.\w+)?$/, ".*\\1") ].sort.select do |file|
          _, pattern = header_from(file)

          # Select if the date pattern is nil (no header info available within the file) or
          # when the pattern matches.
          pattern.nil? || pattern == self.date_pattern
        end

        ::File.unlink( *files[0..-keep-1] )
      end

      # Cleanup old logfiles?
      #
      # @return [Boolean] true or false
      def cleanup?
        !!keep && keep.to_i > 0
      end

      # Symlink the current filename to the original one.
      def symlink!
        # do nothing, because symlink is already correct
        return if ::File.symlink?(@original_filename) && ::File.readlink(@original_filename) == @filename

        ::File.unlink(@original_filename) if ::File.exist?(@original_filename) || ::File.symlink?(@original_filename)
        ::File.symlink(@filename, @original_filename)
      end

      # Symlink the original filename?
      #
      # @return [Boolean] true or false
      def symlink?
        !!symlink
      end

      # Write the header information into the file
      def header!
        stream.puts( Header.call(@date, date_pattern) )
      end

      # Write header into the file?
      #
      # @return [Boolean] true or false
      def header?
        !!header
      end

      # Sets the filename with the `:date_pattern` appended to it.
      def filename_for( date )
        @original_filename.sub(/(\.\w+)?$/, ".#{date.strftime(date_pattern)}\\1")
      end

      # Fetch the header form the file
      def header_from( file )
        if m = ::File.open(file, &:readline).match(HeaderRegexp)
          # in case there is a Header present, we can just read from it
          [ Time.at(m[2].to_f), m[3] ]
        else
          # In case there is no header: we need to take a good guess
          #
          # Since the pattern can not be determined, we will just return the Posix ctime. 
          # That is NOT the creatint time, so the value will potentially be wrong!
          [::File.ctime(file), nil]
        end
      end

      # @overload inspectables
      def inspectables
        super.concat %i[date_pattern header keep symlink]
      end

    end

  end
end