その他
    ホーム 技術発信 DoRuby systemd でデーモン化しつつ logrotate に対応したスクリプトを書く
    systemd でデーモン化しつつ logrotate に対応したスクリプトを書く
     

    systemd でデーモン化しつつ logrotate に対応したスクリプトを書く

    この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

    ときどき、「rubyでちょっとしたデーモンを動かしたい」かつ、「出力されるログファイルもローテーションさせたい」ということたびたび発生します。 そんなとき、どのようにスクリプトを組むのが良いかという点についてまとめてみました。

    TL;DR

    ポイントは以下です。

    • ふつうにRubyのスクリプト書いて systemd でデーモン化する
    • logrotate の postscript の中で実行中のスクリプトに対してUSR1を送出する
    • スクリプト側ではUSR1シグナルを受け取ったらログファイルをreopenする

    スクリプト本体

    デーモンとなるスクリプト本体です。

    log_file = File.expand_path("hoge.log", File.dirname(__FILE__))
    pid_file = File.expand_path("hoge.pid", File.dirname(__FILE__))
    
    # pid 作成
    File.write(pid_file, Process.pid)
    
    # logger 初期化
    logger = Logger.new(log_file)
    logger.level = Logger::INFO
    
    $reopen_log = false
    
    # USR1シグナルでログのreopenフラグを立てる
    trap("USR1") do
      $reopen_log = true
    end
    
    # 終了時に pid ファイルを削除
    END {
      File.delete(pid_file)
    }
    
    loop do
      # フラグ立ってたらログを reopen する
      if $reopen_log
        logger.reopen(log_file)
        $reopen_log = false
      end
    
      logger.info "hogehoge---"
      sleep 10
    end
    

    スクリプトのポイント

    いくつかポイントとなる箇所があるので解説。

    プロセスIDをファイルに出力する

    理由は、logrotate の postrotate のスクリプトで kill -USR1 でプロセスに対してシグナルを送出するためです。

    # pid 作成
    File.write(pid_file, Process.pid)
    
    # 終了時に pid ファイルを削除
    END {
      File.delete(pid_file)
    }
    

    シグナルを受け取ってログを再オープンする

    普通にログローテートすると、対象ファイルがローテーションした際、既存のファイルがリネームされて新しいファイルが作られるが、reopenしないとずっと古いファイル(リネームされた方)に出力されてしまう。
    そこで、USR1(たいていUSR1が使われるらしい)のシグナルを捕捉して、シグナルを受け取ったらreopenする処理を組み込む。

    # USR1シグナルでログのreopenフラグを立てる
    trap("USR1") do
      $reopen_log = true
    end
    
      # フラグ立ってたらログを reopen する
      if $reopen_log
        logger.reopen(log_file)
        $reopen_log = false
      end
    

    ちなみに、trap()のブロック内(シグナルハンドラと呼ぶらしい)でreopenしてみましたが、Threadがなんちゃらというエラーが出て落ちてしまいました。
    一般的にシグナルハンドラの中では入出力を伴う処理等を行うのはよくないらしく、フラグを立てるくらいしたほうが良いらしいです。

    systemd に登録するやつ

    特に解説はしません。
    Systemdを使ってさくっと自作コマンドをサービス化してみる – Qiita などを参考に。

    [Unit]
    Description = xxxx daemon
    
    [Service]
    ExecStart = /home/xxxx/.rbenv/shims/ruby /path/to/hoge.rb
    Restart = always
    Type = simple
    User = hoge
    Group = hoge
    
    [Install]
    WantedBy = multi-user.target
    

    このファイル名 hoge.service がサービス名(hogeと省略可能)となります。

    logrotate

    日毎のローテーションで、履歴を10件保持する設定です。
    この設定のポイントは、postrotateのブロックに記述されたスクリプトで、kill -USR1 $(cat $pid) の部分です。
    これによってローテーション完了後、スクリプトにUSR1シグナルが送出され、それによってスクリプト側ではログファイルがreopenされ、結果としてローテーション後の新しいログファイルにログが出力されるようになるという仕組みになっています。

    /path/to/hoge.log {
        daily
        rotate 10
        missingok
        su hoge hoge
        create 0600 hoge hoge
        postrotate
            pid=/path/to/hoge.pid
            test -e $pid && kill -USR1 $(cat $pid) || true
        endscript
    }
    
    

    デーモンの起動/停止

    以下のコマンドでデーモンが起動します。

    $ sudo systemctl start hoge
    

    停止は以下

    $ sudo systemctl stop hoge
    

    さいごに

    こんな感じで、意外なほど簡単にRubyで気軽にデーモンが作れるのでbotとかいろいろ作ってみたらどうでしょうか。

    記事を共有