轮转日志

日志轮转在服务器管理中经常使用,最近学习raku,试着写了一个简单的轮转

#!/usr/bin/env perl6
# 轮转日志

class SizeParseAction {
    method TOP($/) {
        make $<number> * $<unit>.made;
    }

    method unit($/) {
        my $rate = do given $/.lc {
            when .contains: 'g' {
                when .contains: 'i' {10 ** 9}
                default {1 +< 30}
            }
            when .contains: 'm' {
                when .contains: 'i' {10 ** 6}
                default {1 +< 20}
            }
            when .contains: 'k' {
                when .contains: 'i' {10 ** 3}
                default {1 +< 10}
            }
            when .contains: 'b' {1}
            default { say 'wrong unit: ' ~ $_; fail}
        };
        make $rate
    }
}

grammar MemSize {
    token TOP { <number><unit> }
    token number { \d+[\.\d+]? }
    token unit { <[gmkb]>[i?b]? }
}

role SizeToNum {
    method Num() {
        MemSize.parse(self, :actions(SizeParseAction)).made.Num
    }
}

# 轮转
class Rotate {
    has Str $.file;
    has Int $.days;
    has Str $.size;
    has Bool $.verbose;
    has Num $!bytes = Num;
    has Str $.pid = Str;

    method !file-exists {
        $!file = $!file.IO.absolute;
        $!file.IO ~~ :e & :f
    }

    method check-file-exists {
        say 'No Such File!' andthen exit(2) unless self!file-exists
    }

    method !file-size {
        unless $!bytes.defined {
            $!bytes = ($!size does SizeToNum).Num;
        }
        $!file.IO.s >= $!bytes
    }

    method check-file-size {
        say '文件大小不足以开始轮转...' andthen exit(0) unless self!file-size;
    }

    method remove-last-file {
        my $last-file = "{$!file}-{$!days}.gz";
        if $last-file.IO ~~ :e & :f {
            unlink $last-file.IO;
            say 'removed file: ' ~ $last-file if $!verbose;
            CATCH {
                when X::IO::Unlink {
                    say 'unlink file failed: ' ~ $last-file andthen say .message andthen exit(3);
                }
                default {
                    .say andthen say 'Unknown error when unlink file: ' ~ $last-file andthen exit(4);
                }
            }
        } else {
            say "last file not exists. ignore it: $last-file" if $!verbose;
        }
    }

    method move-backup-files {
        for $!days - 1 ... 1 {
            my $file-name = "{$!file}-{$_}.gz";
            my $next-file = "{$!file}-{$_ + 1}.gz";
            if $file-name.IO ~~ :e & :f {
                say "start move $file-name to $next-file" if $!verbose;
                $file-name.IO.move: $next-file;
                CATCH {
                    when X::IO::Move {
                        say 'move file failed: ' ~ $file-name andthen say .message andthen exit(5);
                    }
                    default {
                        .say andthen say 'Unknown error when move file: ' ~ $file-name andthen exit(6);
                    }
                }
                say "move $file-name to $next-file success!" if $!verbose;
            } else {
                say "$file-name does not exists. ignore it" if $!verbose;
            }
        }
    }

    method last-work {
        # 将当前正在用的文件,移动为第一个gzip
        $!file.IO.move: "{$!file}-1";
        say "current file $!file move to {$!file}-1" if $!verbose;
        # 然后将这个文件压缩
        run 'gzip', "{$!file}-1" andthen
        do if $_ == 0 {say "compress file {$!file}-1 to {$!file}-1.gz successed!" if $!verbose}
        else {
            $*ERR.say: qq:to/END/; 
            gzip returns $_ , failed... Maybe you should gzip {$!file}-1 to {$!file}-1.gz by yourself. 
            don't run this file again. 
            cuz last file will be moved again. 
            so you will lose last 2 files..
            END
            run 'touch', $!file andthen exit(7);
        };
        # 新建一个同名的新文件
        if run 'touch', $!file {
            say 'create a new file: ' ~ $!file ~ ' Successed!' if $!verbose;
        } else {
            say 'create a new file: ' ~ $!file ~ ' Failed!' andthen exit(1);
        }
        True
    }

    method refresh-log {
        if $!pid.defined and $!pid.IO ~~ all(:e :f) {
            shell "kill -USR1 `cat {$!pid.IO.absolute}`"
        }
    }
}


multi MAIN(Str:D() $file, Int() :d(:$days) = 7, Str() :s(:$size) = '10m',Str() :p(:$pid), Bool :v(:$verbose)) {
    my $rotate = Rotate.new: :$file, :$days, :$size, :$verbose, :$pid;
    $rotate.check-file-exists;
    $rotate.check-file-size;
    say 'start rotating...' if $verbose;
    $rotate.remove-last-file;
    $rotate.move-backup-files;
    if $rotate.last-work {
        say 'Rorate Successed!';
    } else {
        say 'Rorate Failed';
    }
    $rotate.refresh-log;
}



sub USAGE() {
    say qq:to/END/;
    Usage:
      {$*PROGRAM-NAME} [-d|--days=Int] [-s|--size=Str] [-p|--pid=Str] [-v|--verbose] <file>

    Options:
      -d, --days = Int           要保存的天数,默认是7
      -s, --size = Str           最低要轮转的大小,默认10M
      -p, --pid = Str            pid文件地址,需要刷新日志写入,如果不是软件的日志,可以忽略此参数
      -v, --verbose = Bool       是否显示详细信息, 默认false
    
    Arguments:
      file = Str                 要轮转的文件

    Examples:
      {$*PROGRAM} -d=5 -s=20.5m -p=pid_path ./xxx

    ReturnCodes:
        1                         创建新的文件失败
        2                         目标文件不存在
        3                         删除过期的文件失败
        4                         删除文件未知错误
        5                         移动备份文件失败
        6                         移动备份未见未知错误
        7                         压缩第一个备份文件失败
        8                         pid文件不存在
    END
}

用法, 例如轮转nginx日志: ./rorate.p6 -d=5 -s=20.5m -p=/usr/local/nginx/nginx.pid /usr/local/nginx/logs/access.log