lib/simple_rotate.rb in simple_rotate-1.0.0 vs lib/simple_rotate.rb in simple_rotate-1.1.0

- old
+ new

@@ -1,1127 +1,6 @@ -# SimpleRotate - simple logger for ruby -# @autor: Kazuya Hotta (nyanko) +# SimpleRotate # -require "singleton" -require "monitor" - -class SimpleRotate - VERSION = "1.0.0" - include Singleton - include MonitorMixin - - #---------------- - # modlues - #---------------- - module LogLevel - LOG_LEVEL_5 = "FATAL" - LOG_LEVEL_4 = "ERROR" - LOG_LEVEL_3 = "WARN" - LOG_LEVEL_2 = "INFO" - LOG_LEVEL_1 = "DEBUG" - LEVEL_ID_FATAL = 5 - LEVEL_ID_ERROR = 4 - LEVEL_ID_WARN = 3 - LEVEL_ID_INFO = 2 - LEVEL_ID_DEBUG = 1 - end - include LogLevel - - module RotateTerm - TERM_DAILY = 1 - TERM_WEEKLY = 7 - TERM_MONTHLY = 30 - end - include RotateTerm - - module Validator - def valid_file_name - # stdout only - if @file_name.is_a?(Symbol) && @file_name == :STDOUT - @only_stdout = true - return true - end - - # not string - if !@file_name.is_a?(String) - SimpleRotate::Error.argv("file_name", @file_name) - end - - # directory? - if File.directory?(@file_name) - msg = "ERROR => #{@file_name} is a Directory!" - SimpleRotate::Error.warning(msg) - SimpleRotate::Error.argv("file_name", @file_name) - end - - return true - end - - def valid_int(param, argv) - if !argv.is_a?(Integer) - SimpleRotate::Error.argv(param, argv) - - elsif argv < 0 - msg = %{You can't specify the negative value!} - SimpleRotate::Error.warning(msg) - SimpleRotate::Error.argv(param, argv) - end - end - - def valid_bool(param, argv) - argv = true if argv == 1 - argv = false if argv == 0 - if !(argv.instance_of?(TrueClass) || argv.instance_of?(FalseClass)) - SimpleRotate::Error.argv(param, argv) - end - return true - end - end - include Validator - - #---------------- - # classes - #---------------- - # - # error class for SimpleRotate - # - class Error < RuntimeError - msg = "aborted the log file rotation process," - msg += " because an unexpected error has occured." - ROTATION_FAILED = msg - - @@silence = false - - # - # skip warning message - # - def self.silence - @@silence = true - end - - # - # argument error - # - def self.argv(param, argv) - msg = "'#{param}'='#{argv}' is invalid argument value!" - self.throw_error(msg) - end - - # - # method missing - # - def self.missing(name) - msg = "undifined method 'SimpleRotate##{name}'" - self.throw_error(msg) - end - - # - # file open error - # - def self.open(name) - msg = "Couldn't open a '#{name}'" - self.throw_error(msg) - end - - # - # load error - # - def self.load(name) - msg = "Couldn't load a '#{name}'" - self.throw_error(msg) - end - - # - # exist error - # - def self.exist(name, type) - msg = "Already exists this #{type} => '#{name}'" - self.throw_error(msg) - end - - # - # warning - don't throw error - # - def self.warning(msg) - warn "[WARNING] #{msg} - (SimpleRotate::Error)" if !@@silence - end - - # @param msg string - # - def self.throw_error(msg) - exeption = self.new(msg) - warn exeption.message if !@@silence - raise SimpleRotate::Error - end - end - - # - # The module for the process safe - # This module will be included by ProcessSync class - # - module ProcessSyncMixin - @@scheduled_del_lockfile = false - @@tempf_name = nil - @@tempf = nil - - def locked? - return false if !tempf_exists? - - # return false, if locked by another - status = @@tempf.flock(File::LOCK_EX | File::LOCK_NB) - - return status == false - end - - # lock the temp file - def lock - create_tempfile if !tempf_exists? - - # if don't reopen temp file, can't lock... - reopen_temp_file - - cnt = 0 - begin - @@tempf.flock(File::LOCK_EX) - - rescue - cnt += 1 - if (cnt <= @try_limit) - sleep(0.5) - create_tempfile if !tempf_exists? - retry - else - SimpleRotate::Error.warning("It was not possible to lock (tried 3times) => #{@@tempf_name}") - return false - end - end - end - - # unlock the temp file - def unlock - return nil if !tempf_exists? - - begin - @@tempf.flock(File::LOCK_UN) - rescue - SimpleRotate::Error.warning("It was not possible to unlock => #{@@tempf_name}") - end - end - end - - # - # The classes for process-safe - # - class ProcessSync - include ProcessSyncMixin - - ProcessSyncMixin.instance_methods.each do |method_name| - method = instance_method(method_name) - define_method(method_name) do |*args| - ################### - # common execution - ################### - # Processing to be performed at the beginning of the method - return nil if !@enable - ################### - method.bind(self).call(*args) - end - end - - def initialize(sr) - @sr = sr - @enable = sr.instance_variable_get(:@psafe_mode) - @file_name = sr.instance_variable_get(:@file_name) - - # #init not called - return self if @file_name == nil - - @logf = sr.instance_variable_get(:@logf) - @try_limit = 3 - @@tempf_name = File.dirname(@file_name) + File::SEPARATOR + ".SimpleRotate_tempfile_#{File.basename($0)}" - - create_tempfile if @enable && !@@scheduled_del_lockfile - end - - # Create the temp file for locking - private - def create_tempfile - if File.exist?(@@tempf_name) - open_temp_file - return nil - end - - begin - @@tempf = File.open(@@tempf_name, File::RDWR|File::CREAT|File::EXCL) - - rescue - SimpleRotate::Error.warning("Couldn't create temp file => #{@@tempf_name}") - - ensure - set_delete_tempfile - end - end - - private - def tempf_exists? - return File.exist?(@@tempf_name) - end - - # Delete the lock file at the end of the script - private - def set_delete_tempfile - return true if @@scheduled_del_lockfile - - if File.exists?(@@tempf_name) - # is it empty? - if File.size(@@tempf_name) == 0 - delete_at_end - else - # it is not empty - msg = "File is not empty => #{@@tempf_name}#{$-0}" - msg += "Skip to delete temp file!" - SimpleRotate::Error.warning(msg) - end - end - @@scheduled_del_lockfile = true - end - - private - def delete_at_end - at_exit do - begin - File.delete(@@tempf_name) - rescue - #SimpleRotate::Error.warning("Couldn't delete temp file => #{@@tempf_name}") - end - end - end - - private - def reopen_temp_file - close_temp_file - open_temp_file - end - - private - def open_temp_file - if @@tempf.is_a?(IO) && @@tempf.closed? || !@@tempf.is_a?(IO) - begin - @@tempf = File.open(@@tempf_name, File::RDWR|File::CREAT|File::APPEND) - rescue - SimpleRotate::Error.warning("Couldn't open temp file => #{@@tempf_name}") - end - end - end - - private - def close_temp_file - if @@tempf.is_a?(IO) && !@@tempf.closed? - begin - @@tempf.close - rescue - SimpleRotate::Error.warning("Couldn't close temp file => #{@@tempf_name}") - end - end - end - end - - #-------------------- - # access definitions - #-------------------- - attr_accessor :threshold, - :date_format, - :logging_format, - :rename_format, - :allow_overwrite, - :sleep_time - - attr_reader :limit - - #---------------- - # public methods - #---------------- - # - # return method missing error - # - def method_missing(name, *argv) - SimpleRotate::Error.missing(name) - end - - # - # @param string|symbol $file_name - # @param string|int $limit - # @param int $rotation - # @return self - # - def init(file_name=File.absolute_path($0+".log"), limit='1M', rotation=0) - @file_name = file_name - @limit = limit - @rotation = rotation - - # load defaults - include_defaults - - # validation - valid_file_name - valid_int("rotation", @rotation) - - # stdout only - return self if @only_stdout - - if rotate_by_term? - # term rotation - set_days_cnt_of_term - @limit_term = @limit - @rotate_by_term = true - - else - # file_size rotation - @limit_size = trim_byte(@limit) - if @limit_size <= 0 - SimpleRotate::Error.argv("limit", @limit) - end - @rotate_by_term = false - end - - # for process safe - @psync = ProcessSync.new(self) - - # open or generate the log file - synchronize do - @psync.lock - - prepare_logf - - @psync.unlock - end - - # if block given, colse IO - if defined? yield - yield self - e - end - - return self - end - - # - # log message out to STDOUT when use SimpleRotate#w method - # - def with_stdout - @with_stdout = true - end - - # - # enable compress - # - def compress - @compress = true - use_zlib - end - - # - # define the compression level - # this method load 'zlib' - # this method enable compress flag - # @param int level - 0-9 - # default is 6 - # - def compress_level(level) - @compress_level = level - valid_int("compress_level", @compress_level) - compress - - return level - end - - # - # @param string $log message write to log file - # @return string - # - def w(log) - if @file_name == nil - msg = "file_name is Nil Class! Please call #init method" - SimpleRotate::Error.throw_error(msg) - end - - # don't out log message if Doesn't reach threshold - return nil if (!over_threshold?) - - content = get_trimmed_log(log) - - # return and end func, if only_stdout enable - if @only_stdout - puts content - return log - end - - # write message to file - synchronize do - @psync.lock - - sync_inode - @logf.puts(content) - @logf.flush if @enable_wflush - @logf.fsync if @enable_wflush - - @psync.unlock - end - - # dump log message STDOUT, if with_stdout enable - puts content if @with_stdout - - # rotate if necessary - rotate_if if !@no_wcheck - - return log - end - - # - # disable call File#flush after #w method - # - def enable_wflush - @enable_wflush = true - end - - # - # enable call File#flush after #w method - # - def disable_wflush - @enable_wflush = false - end - - # - # close file - # - def e - return nil if logf_not_usable - - synchronize do - @psync.lock - - @logf.close - - @psync.unlock - end - end - - # - # file reopen - # - def reopen - return nil if logf_not_usable - - if !file_closed? - SimpleRotate::Error.warning("File is already open!") - return nil - end - - openadd - return @logf - end - - # - # force rotation - # - def flush - return nil if logf_not_usable - return nil if @rotate_by_term - rotation(:FLUSH) - end - - # - # don't check can to rotate at #w method - # - def no_wcheck - @no_wcheck = true - end - - # - # is log file open? - # @return nil|bool - # - def file_closed? - return nil if logf_not_usable - return @logf.closed? - end - - # - # skip warning message - # - def silence - SimpleRotate::Error.silence - end - - # - # set log level FATAL - # @return self - # - def fatal - @log_level = 5 - return self - end - - # - # set log level ERROR - # @return self - # - def error - @log_level = 4 - return self - end - - # - # set log level WORN - # @return self - # - def warn - @log_level = 3 - return self - end - - # - # set log level INFO - # @return self - # - def info - @log_level = 2 - return self - end - - # - # set log level DEBUG - # @return self - # - def debug - @log_level = 1 - return self - end - - # - # try to be a safe process - # - def psafe_mode(sleep_time=0.1) - @psafe_mode = true - @enable_wflush = true - @sleep_time = sleep_time - - @psync = ProcessSync.new(self) - end - - # - # Reopen file necessary - # @return bool|nil - # - def sync_inode - return nil if logf_not_usable - - cnt = 0 - begin - # check i-node number - open_inode = @logf.stat.ino - logf_inode = File.stat(@file_name).ino - raise if open_inode != logf_inode - rescue - cnt += 1 - sleep(0.1) - e - openadd - - if cnt <= @sync_inode_limit - retry - else - SimpleRotate::Error.warning(%{inode number didn't not match, tried #{cnt} times!}) - return false - end - end - - return true - end - - # - # Disable #sync_inode - # - def no_sync_inode - @no_sync_inode = true - end - - #---------------- - # private methods - #---------------- - # - # log file is not IO class or stdout only - # @return bool - # - private - def logf_not_usable - !@logf.is_a?(IO) || @only_stdout - end - - # - # load zlib lib - # - private - def use_zlib - begin - require "zlib" - @compress_level = Zlib::DEFAULT_COMPRESSION if @compress_level == nil - - rescue LoadError - SimpleRotate::Error.load("zlib") - end - end - - # - # open or generate the log file - # - private - def prepare_logf - if File.exist?(@file_name) - # open the exists file, add mode - openadd - - # rotate it if necessary - rotate_if - - else - gen_new_logf - end - end - - # - # generate new log file - # - private - def gen_new_logf - begin - @logf = File.open(@file_name, File::RDWR|File::CREAT|File::TRUNC) - gtime = Time.new.to_i - @logf.puts("created@#{gtime}@Please don't delete this line") - @logf.close - - rescue - SimpleRotate::Error.open(@file_name) - end - - openadd - end - - # - # if file or directory exist, call error - # - private - def exist_error(file) - SimpleRotate::Error.exist(file, "File") if File.exist?(file) - SimpleRotate::Error.exist(file, "Directory") if Dir.exist?(file) - - return true - end - - # - # define default instance vars - # - private - def include_defaults - que = [] - que << [%{@threshold}, %{LOG_LEVEL_2}] - que << [%{@log_level}, %{2}] - que << [%{@logging_format}, %{"[$DATE] - $LEVEL : $LOG"}] - que << [%{@date_format}, %{"%Y/%m/%d %H:%M:%S"}] - que << [%{@term_format}, %{"%Y%m%d"}] - que << [%{@rename_format}, %{"."}] - que << [%{@with_stdout}, %{false}] - que << [%{@only_stdout}, %{false}] - que << [%{@no_wcheck}, %{false}] - que << [%{@sync_inode_limit}, %{3}] - que << [%{@no_sync_inode}, %{false}] - que << [%{@enable_wflush}, %{false}] - que << [%{@compress}, %{false}] - que << [%{@psafe_mode}, %{false}] - que << [%{@sleep_time}, %{0}] - - que.each do |q| - if eval(%{#{q[0]} == nil}) - eval(%{#{q[0]} = #{q[1]}}) - end - end - end - - # - # Whether to rotate by file age? - # @return bool - # - private - def rotate_by_term? - if @limit.is_a?(Integer) - return false - - elsif @limit.is_a?(String) - return @limit.to_i == 0 - - else - SimpleRotate::Error.argv("limit", @limit) - end - end - - # - # Open the log file, add mode - # - private - def openadd - @logf = File.open(@file_name, File::RDWR|File::CREAT|File::APPEND) - - # refresh object - @psync = ProcessSync.new(self) - end - - # - # get cretated time of the log file - # @return Time - # - private - def get_logf_generate_time - pos = @logf.pos - begin - @logf.rewind - stamp = @logf.readline.split("@") - @logf.seek(pos) - gtime = Time.at(stamp[1].to_i) - - rescue StandardError, SyntaxError - msg = "Can't get file creation time!" - gtime = Time.now - SimpleRotate::Error.warning(msg) - end - - return gtime - end - - # - # @return int - # - private - def set_days_cnt_of_term - begin - @dayc = eval("TERM_#{@limit}") - rescue NameError - SimpleRotate::Error.argv("limit", @limit) - end - end - - # - # log file size over 'limit_size'? - # @return bool|nil - # - private - def over_size? - return nil if logf_not_usable - - begin - rst = File.size(@file_name) > @limit_size - rescue - rst = false - end - - return rst - end - - private - def safe_over_size? - rst = nil - synchronize do - @psync.lock - rst = over_size? - @psync.unlock - end - - return rst - end - - # - # logfile's elapsed days is over limit? - # @return bool - # - private - def over_term? - return nil if logf_not_usable - - begin - now_time = Time.now - gen_time = get_logf_generate_time - estimated_time = gen_time + (60 * 60 * 24 * @dayc) - rst = estimated_time <= now_time - rescue - rst = false - end - - return rst - end - - private - def safe_over_term? - rst = nil - synchronize do - @psync.lock - rst = over_term? - @psync.unlock - end - - return rst - end - - # - # Format the text for logging - # the following characters are recognized - # $DATE => date - # $LEVEL => log's severity - # $LOG => your log message - # $PID => process ID - # $FILE => execute file name - # - # @param string $log - # @return string - # - private - def get_trimmed_log(log) - log = log.to_s - date = Time.now.strftime(@date_format) - level = eval("LOG_LEVEL_#{@log_level}") - return @logging_format.gsub("$DATE", date) - .gsub("$LEVEL", level) - .gsub("$LOG", log) - .gsub("$PID", $$.to_s) - .gsub("$FILE-FUL", File.absolute_path($0)) - .gsub("$FILE", File.basename($0)) - end - - # - # Whether that is the output of the log level that exceeds the threshold - # @return boolean - # - private - def over_threshold? - begin - return (@log_level >= eval("LEVEL_ID_#{@threshold}")) - rescue NameError - SimpleRotate::Error.argv("threshold", @threshold) - end - end - - # - # need rotate? - # @return bool - # - private - def reached_limit?(mode=:NO_LOCK) - # file age rotation - if @rotate_by_term - is_over_term = nil - if mode == :NO_LOCK - is_over_term = over_term? - elsif mode == :LOCK - is_over_term = safe_over_term? - end - - return is_over_term - end - - # file size rotation - is_over_size = nil - if mode == :NO_LOCK - is_over_size = over_size? - elsif mode == :LOCK - is_over_size = safe_over_size? - end - - return is_over_size - end - - # - # Rotates as necessary - # @return bool - # - private - def rotate_if - if reached_limit?(:LOCK) - rotation - return true - - else - # no need to rotate - return false - end - end - - # - # prepare & call #do_rotation - # - private - def rotation(mode=:NO_SPEC) - synchronize do - # if rotationing now by another process, return - if @psync.locked? #=> if not process safe mode, will be return nil - return false - end - - # lock another process if enable - @psync.lock - - do_rotate(mode) - - # unlock another process if enable - @psync.unlock - end - end - - # - # Rotate the log file now, and open a new one - # - private - def do_rotate(mode) - return nil if logf_not_usable - - # check already executed rotation? - if mode != :FLUSH && !reached_limit? - return false - end - - # file age rotation - if @rotate_by_term - rtn = do_term_rotate - return rtn - end - - # file size rotation - cnt = 1 - rotate_name = "#{@file_name}#{@rename_format}#{cnt}" - rotate_name += ".gz" if @compress - - if File.exist?(rotate_name) - while File.exist?(rotate_name) - cnt += 1 - rotate_name = "#{@file_name}#{@rename_format}#{cnt}" - rotate_name += ".gz" if @compress - end - - rename_wait_que = Array.new - for nc in 1...cnt - break if @rotation == 1 - if (@compress) - rename_wait_que << "File.rename('#{@file_name}#{@rename_format}#{nc}.gz', '#{@file_name}#{@rename_format}#{nc+1}.gz')" - else - rename_wait_que << "File.rename('#{@file_name}#{@rename_format}#{nc}', '#{@file_name}#{@rename_format}#{nc+1}')" - end - - if @rotation - next if @rotation == 0 - break if @rotation <= nc+1 - end - end - - rename_wait_que.reverse! - - begin - rename_wait_que.each do |do_rename| - eval(do_rename) - end - rescue - SimpleRotate::Error.warning(SimpleRotate::Error::ROTATION_FAILED) - return false - end - end - - most_recent_name = "#{@file_name}#{@rename_format}1" - post_execute_rotate(most_recent_name) - end - - # - # Rotate the log file now, and open a new one - # for rotate by term - # - private - def do_term_rotate - date = Time.now.strftime(@term_format) - rotate_name = "#{@file_name}#{@rename_format}#{date}" - - # Don't rotation If a file with the same name already exists - return false if File.exists?(rotate_name) - return false if File.exists?("#{rotate_name}.gz") - - post_execute_rotate(rotate_name) - end - - # - # rename log_file & generate new one - # - private - def post_execute_rotate(after_name) - begin - @logf.close - File.rename(@file_name, after_name) - do_compress(after_name) if @compress - prepare_logf - - # sleep after rotation - sleep(@sleep_time) if @sleep_time > 0 - - rescue - SimpleRotate::Error.warning(SimpleRotate::Error::ROTATION_FAILED) - reopen if file_closed? - end - end - - # - # compress rotated file - # - private - def do_compress(file) - contents = nil - File.open(file, File::RDONLY) do |f| - contents = f.read - end - - newf = "#{file}.gz" - - io = File.open(newf, File::WRONLY|File::CREAT|File::TRUNC) - Zlib::GzipWriter.wrap(io, @compress_level) do |writer| - writer.mtime = File.mtime(file).to_i - writer.orig_name = file - writer.write(contents) - end - - File.delete(file) - end - - # - # convert 'limit_size' to integer - # @param string|int $limit_size - # @return int - # - private - def trim_byte(limit) - return limit if limit.is_a?(Integer) - - kiro = "000" - mega = kiro + "000" - giga = mega + "000" - tera = giga + "000" - limit_size = limit - - if /K$/ =~ limit_size - limit_size = limit_size.sub(/K$/, "") + kiro - elsif /M$/ =~ limit_size - limit_size = limit_size.sub(/M$/, "") + mega - elsif /G$/ =~ limit_size - limit_size = limit_size.sub(/G$/, "") + giga - elsif /T$/ =~ limit_size - limit_size = limit_size.sub(/T$/, "") + tera - end - - return limit_size.to_i - end - - #-------------------- - # method alias - #-------------------- - alias_method :<<, :w -end +# Simple and safety logger +# @autor: Kazuya Hotta +# +require_relative "simple_rotate/core.rb"