Skip to content
This repository
Fetching contributors…

Cannot retrieve contributors at this time

file 177 lines (139 sloc) 5.747 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
# encoding: utf-8

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+)\) \[(.+)\] -\*-$/


      setup do |options|
        @date, @date_strftime = nil, nil # default; do not override --R

        # check whether to write the log header (default true)
        self.header = options.fetch(:header, true)

        # check the date pattern on the filename (default "%Y%m%d")
        self.date_pattern = options.fetch(:date_pattern, DefaultDatePattern)

        # check whether to cleanup old files of the same pattern (default false)
        self.keep = options.fetch(:keep, false)

        # check whether to symlink the otiginal filename (default false)
        self.symlink = options.fetch(:symlink, false)

        @original_filename = ::File.expand_path options.fetch(:filename, default_filename)
        options[:filename] = @original_filename
      end

      write do |event|
        return unless close? # do nothing when not closing
        close

        cleanup! if cleanup?
        symlink! if symlink?

        return if ::File.exist?( @filename ) # exit when file ready present
        stream.puts( Header.call(@date, date_pattern) ) if header? # write the header if applicable
      end

      close do
        @filename = filename_for( @date )
      end


      # 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

      # 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" ) ].select do |file|
          created, 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] )
      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( @filename, @original_filename )
      end

      # Symlink the original filename?
      #
      # @return [Boolean] true or false
      def symlink?; !!symlink; 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

    end

    register( :datefile, Yell::Adapters::Datefile )

  end
end
Something went wrong with that request. Please try again.