Log4cppでログファイルをローテーションする

プログラム開発でログの出力を行いたいと思います。
今回はC++のプログラムで実装してみます。

まず、なんで今更Log4cppについて記述しようと思ったか説明します。
log4cppのサンプルや記述はネット検索ですぐに見つかります。
でも、上位で検索されたサンプルには間違いがそのまま残されていて、実際に動かしてみるとローテーションは行われませんでした。
また、注意深く見てみると、設定のパラメタすら間違ったままになっているのです。
私は、そのWebサイトの間違いに過去引っかかり、無駄な時間を費やしました。なので、そんな無駄な時間を今後費やすことのないように、ここに記述します。
そちらのサイトを批判するつもりはありませんので、当該サイトのURL等については記述しません。
なお、ここの記載内容に間違いがあった場合には、お手数ですがご指摘して頂けますようお願いします。
こちらのサイトを参考にされる方には、コメント欄に指摘されている内容も含め参考にされることを期待します。
なるべく、修正は行っていくつもりですが・・・将来のことなので、ご注意ください。また、Web上の情報は参考情報です。ご利用に際しては、ご自身の責任でご利用ください。

環境

今回実装を試したのは以下の環境です。

OS:CentOS 7.4
gcc バージョン 4.8.5

 

パッケージインストール

yum install log4cpp

 

プログラム

#include <log4cpp/Category.hh>
#include <log4cpp/PropertyConfigurator.hh>

bool initLog4cpp() {
        // log configuration file
        const char *file_log4cpp_conf = "log.conf";

        // log configuration
        try {
                log4cpp::PropertyConfigurator::configure("log.conf");
                return true;
        } catch (log4cpp::ConfigureFailure& e) {
                // log4cpp Error messages
                std::cout
                        << "log4cpp::ConfigureFailure : "
                        << e.what()
                        << file_log4cpp_conf
                        << std::endl;
                return false;
        }
}

int main(int argc, char *argv[])
{
  bool configured = initLog4cpp();
  if (!configured) {
    return -1;
  }

  log4cpp::Category& logger = log4cpp::Category::getInstance("logging");

  int i = 0;


  for(i=0;i < 4096; i++){
    logger.warn("warning message");
    logger.info("Information message");
    logger.debug("Debug message");
  }

  return 0;
}

 

設定

# カテゴリの設定
#log4cpp.rootCategory=DEBUG, rootAppender
log4cpp.rootCategory=INFO, rootAppender
log4cpp.category.logging=INFO, loggingAppender
#log4cpp.category.logging=WARN, logginAppender
log4cpp.category.logging.calc=NOTICE

# アペンダの設定
# rootAppenderの設定
log4cpp.appender.rootAppender=ConsoleAppender
log4cpp.appender.rootAppender.layout=BasicLayout
# sampleAppenderの設定
log4cpp.appender.loggingAppender=RollingFileAppender
log4cpp.appender.loggingAppender.fileName=sample.log
log4cpp.appender.loggingAppender.maxFileSize=102400
log4cpp.appender.loggingAppender.maxBackupIndex=8
log4cpp.appender.loggingAppender.layout=BasicLayout

ファイルサイズを102400バイトまでとする。
ログは最大8世代まで残す。

ざっとこんな感じ。

コンパイルは以下のコマンドで行う。

g++ sampl.cpp -lpthread -llog4cpp

 

CentOS 7 NTP設定・・・ちょっと待て!?

こんな感じではじまった

CentOS 7で時刻同期を行おうとNTPの設定を行った。
デフォルトでインストールされているだろうと思い込んで進んだのだが、ntpdが無いではないか!?

そんじゃ・・・ということでyumにお願いしてインストールしてsystemctlをstartとenableやって、ntpqコマンドで動作確認。。。。おっ!動いた!OK(^^♪

と思ったよ!!俺でもこれくらいは出来るぞ!と、思ったよ!

しばらくして、マシンの再起動なんかもして、翌日ntpqで時刻同期はどうなってるかなぁ~なんて、楽しみに見てみたら・・・・

# ntpq -p
ntpq: read: Connection refused

あれ?動いてない?systemctl enable ntpdを忘れてたの?
と再度実行して、再起動してみたものの同じ現象やん!?

やばい!いつも簡単な設定だから適当決め込んでいたのだが、不味い!!((((;゚Д゚))))ガクガクブルブル

ググったら出てきた。CentOS 7からntpdは標準ではなく、代わりにChronydが標準になったのが原因だった。
当然、デフォルトで設定されてたよw

と言うことで本題!

でもって、/etc/chrony.confを開いて以下の部分を適当に変更。

# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (https://www.pool.ntp.org/join.html).
#server 0.centos.pool.ntp.org iburst
#server 1.centos.pool.ntp.org iburst
#server 2.centos.pool.ntp.org iburst
#server 3.centos.pool.ntp.org iburst
server 10.20.30.1 iburst
server 10.20.30.2 iburst

不要なサーバ設定を削除して、必要なサーバを指定(IPは適当なものに置き換えてください)して、再起動したらOKでした。

こんな感じで再起動

ちなみに、

# systemctl restart chronyd

確認はやらなくちゃ!

更に確認方法は・・・

# chronyc sources
210 Number of sources = 4
MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^+ v.dyn.im                      2  10   377   409  +1406us[+1406us] +/-   32ms
^- x.ns.gin.ntt.net              2  10   377   416  +1495us[+1495us] +/-   81ms
^+ jiro.paina.net                2  10   377   419  -1292us[-1292us] +/-   59ms
^* sv1.localdomain1.com          2  10   377   431  +3607us[+4336us] +/-   50ms
#

こんな感じで表示されたらOKらしい。(表示されているのはデフォルトの状態で試した時です。)

ほんま焦りますわ(^^♪

ついでにちょっとだけコメント

ちなみに、サーバとして動作させるには、/etc/chrony.confの以下の部分を設定してあげる必要があります。

# Allow NTP client access from local network.
#allow 192.168.0.0/16

allowする相手のネットワークを必要に応じて記載することが必要です。

サーバとして動作させる必要が無ければ、この状態でOKです。

ちにみに設定変更を行ったら、サービスの再起動を行うこと!

自動機能もデフォルトで設定されている様ですが、どうしてもやりたい人は、systemctl enableで実施してください。

chronydは嫌だぁ~!って人は、chronydをdisableしてntpdを設定してあげれば良いそうです。

 

プロセスの監視と再起動(monitを使ってみる)

前回はdummyプログラムを用いたサービスの登録方法について説明しました。
今回は、その登録したサービスを監視し異常発生時の再起動を自動的に行いたいと思います。

dummyプログラムは、TCP/IPによる通信を行うサーバ側のプログラムです。
ポート番号10050を使用してクライアントからの接続を待っています。

そのdummyプログラムが異常終了しゴースト化してしまっていたり、プロセス自体が動作していない状態(psコマンドで見つからない時)に、自動的に再起動を行わせます。

準備

プロセスの監視には色々と方法がありますが、今回はmonitを使用します。

monitのインストール

まず最初に、epelリポジトリをインストールします。
デフォルトではインストールされていないので、準備します。

# yum -y install epel-release

続けて、monitをインストールします。

# yum -y install monit

これだけです。

monitの初期設定

色々と他のサイトでも紹介されている様なので、今回は細かな設定については割愛します。
私がやったことは、以下の通りです。

1)monit.confの設定変更

/etc/monit.confもしくは/etc/monitrcの記述内容を編集します。

当該ファイルの一番下にこんな記述があります。

###############################################################################
## Includes
###############################################################################
##
## It is possible to include additional configuration parts from other files or
## directories.
#
include /etc/monit.d/*
#

このままでは、/etc/monit.d/にある全てのファイルがインクルードされることになります。
/etc/monit.d/には各監視対象毎の設定ファイルが置かれることになります。
そこで、以下の様に編集しました。

###############################################################################
## Includes
###############################################################################
##
## It is possible to include additional configuration parts from other files or
## directories.
#
#include /etc/monit.d/*
include /etc/monit.d/*.conf
#

/etc/moni.d/*.confとすることで、拡張子が.confのファイルだけが対象となります。
この設定を行う場合には、以下の作業も忘れずに行って下さい。

初期状態では、/etc/monit.d/にあるファイルはloggingだけになっています。

# ls -la
合計 xx
drwxr-xr-x.   2 root root    78  2月  6 10:48 .
drwxr-xr-x. 152 root root 12288  2月  5 19:58 ..
-rw-r--r--.   1 root root    51  9月 13  2015 logging

loggingファイルをlogging.confにコピーします。ファイル名の変更でも構いません。

cp -p /etc/monit.d/logging /etc/monit.d/logging.conf

これでmonitが動作する環境設定は準備出来ました。
それでは、monitをサービスとして起動します。

# systemctl start monit

OS起動時に自動起動させるためには更に以下のコマンドを実行する。

# systemctl enable monit

これで監視が行われる状態になっています。
でも、監視対象が何も指定されいませんので、以下でdummyサービスの監視を行います。

プロセスの監視

前回の設定ではサービスとして登録しましたが、ここでは敢えて単独のプロセスとして監視対象にしました。
サービスとして登録した場合の方法については、別途記載しています。
サービスと登録している状態で、この方法を取ることはお勧めできません。
方法が異なる起動方法によりプロセスが再起動する可能性が発生し、管理上良くないからです。
この方法を採用する場合には、サービスとしての登録は行わない方が賢明です。monit単独で利用されることが望ましいと判断します。この方法とサービスでの起動を両方採用することは、二重起動を引き起こす可能性を含んでいます。
多分、プログラムは正常に動作しますが、異常が発生した場合に何が原因なのか特定することが困難になる気がします。

/etc/monit.d/dummy.confを以下の内容で作成します。

check process dummy matching "dummy"
start program = "/hogehoge/dummy"
stop program  = "/usr/bin/kill -9 dummy"
if failed port 10050 then restart
if 10 restarts within 10 cycles then unmonitor

1行目:監視対象を指定します。
2行目:起動方法を指定します。
3行目:終了方法を指定します。
4行目:異常時の処置を指定します。今回はポート10050を使用しており、10050ポートが開いているかどうかを確認しています。
5行目:10回再起動をトライしてダメだったら再起動を取止めるという内容で、再起動の試行回数を指定しています。

後は、monitに設定を読込ませれば完了です。以下のコマンドでmonitに設定内容を読込ませます。

# monit reload

以上で完了です。
監視状況を確認するには以下のコマンドで確認出来ます。

# monit status

 

サービスの監視

サービスの監視も基本的には同じです。

設定ファイルが以下の通り変更になりますが、実際には同じでも大丈夫です。
サービスとして登録する方法は前回を参考にしてください。

check process dummy matching "dummy"
start program = "/usr/bin/systemctl start dummy"
stop program  = "/usr/bin/systemctl stop dummy"
if failed port 10050 then restart
if 10 restarts within 10 cycles then unmonitor

プロセスと異なり、systemctlを使用して起動と停止を行っています。
起動時にsystemctlコマンドを使用することで、サービス側との二重起動が避けられます。PIDはSystemd側で管理されていますので、Systemdを経由した起動の方が都合が良いわけです。
dummyプログラムのPIDを調べれば解ることなので、pkillやkillを使用して停止させることも出来るのですが、Systemd側における管理が煩雑になることを避ける意味から、systemctlコマンドを使うことになります。

サービス登録をしてRestartによる再起動を行った場合は、ポートの状態までは監視してくれませんでしたが、monitを使うことでポートの監視を行える様になっています。
ただし、固定のポートアドレスをハードコーディングで行う必要があるため、ちょっと使い勝手が良いとは言えません。

尚、この場合にはサービス登録側の設定も少し変更した方が良いでしょう。

こんな感じになります。

[Unit]
Description = Dummy TCP daemon

[Service]
#Restart = always
StartLimitInterval=60
StartLimitBurst=5
ExecStart = /hogehoge/dummy
ExecStop=/usr/bin/kill -9 $MAINPID

[Install]
WantedBy=multi-user.target

 

プロセスとして登録した場合とサービスとして登録した場合を敢えて記載しましたが、monitだけで十分では無いか?と考えると思います。通常はmonitだけで十分だと私も思います。

サービスとして登録する意味があまりある様には思えません。

敢えてサービスとして登録する意味はと言えば、一意のファイル名でプログラムが作成されていれば良いのですが、同名の実行ファイル名でプロセスが起動する様な場合には、サービスにしておく意味がある・・・と言うことぐらいでしょうか?

更に突っ込んで!monitが死んだら?

サービスで登録した場合でもプロセスとして登録した場合でも同じことなのですが、monit自体が死んでしまったら、プロセスを再起動することが出来ません。

CentOS 7では、UpStartの設定に従来の/etc/initが使えません。

ということで、/usr/lib/systemd/system/monit.serviceを変更します。

[Unit]
Description=Pro-active monitoring utility for unix systems
After=network.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/monit -I
ExecStop=/usr/bin/monit quit
ExecReload=/usr/bin/monit reload

[Install]
WantedBy=multi-user.target
[root@localhost system]# pwd
/usr/lib/systemd/system

Restart=alwaysを追加しただけです。

これで、自動的に再起動を行ってくれます。

 

 

独自コマンドのサービス登録(CentOS 7)

今回は、CentOS 7上で自分で作ったプログラムをサービスとして登録する方法について説明します。

CentOS 6までとは管理の方法が異なっているため、ご注意ください。

環境はCentOS 7です。

今回のお題は

独自に作成した常駐プログラムをサービスとして登録します。
仮に、dummyというプログラムをサービスとして登録します。
dummyプログラムは単体で動作する時、常に動作状態を維持し障害や何らかの人為的操作以外で停止することが無いプログラムであると仮定します。
なんか難しく書きましたが、要は異常がない限り動き続けるプログラムを準備します。
それをサービスとして登録します。

サービス登録内容の記述

サービス登録を行うためには、設定内容をファイルに記述する必要があります。
そのファイルは以下のフォルダーへ保存します。

/etc/systemd/system

試しにdummyプログラムをサービスとして登録する場合の記述内容を以下に記します。

[Unit]
Description = Dummy TCP daemon

[Service]
Restart = always
StartLimitInterval=60
StartLimitBurst=5
PIDFile=/var/run/dummy/dummy.pid
ExecStart = /root/work/xxxx/testcode/dummy -D
ExecStop=/usr/bin/kill -p $MAINPID

[Install]
WantedBy=multi-user.target

[Unit]ではこのサービスに関する説明を記しています。
Descriptionで簡単な名前を登録します。

[Service]では、以下の内容を登録しています。
Restart:always・・・サービスが停止していた場合に自動的に再起動を行います。
StartLimitInterval=60・・・①
StartLimitBurst=5・・・・・②

StartLimitInterval中にStartLimitBurst回の再起動を試みますが、それが失敗した場合には、次のStartLimitInterva時間は再起動を試みません。という設定になります。

PIDFileはPIDを出力する先を示しています。
ExecStartはコマンドの起動方法です。
ExecStopはコマンドの終了方法です。

[Install]
WanterByでは、OSの起動のセッションを指定しています。
multi-user.targetはinit 3の起動時に相当します。
他にgraphical-user.targetなどを指定することが可能です。
multi-user.targetを指定した場合、graphical-user.target時にも起動されます。これは、graphical-user.targetがmulti-user.targeをベースにしているためです

上記の設定では、プロセスが起動できない状態が発生した場合、永遠に再起動が繰り返されることになります。

また、プロセスがゴースト(defunct)になってしまった場合に、プロセス自体が残るため再起動が行われない可能性が残ります。
そういう意味では、少し問題を抱えていると思っても良いでしょう。

この解決方法については、別途説明したいと思います。

サービスの起動と終了

サービスを起動するには、通常通り以下のコマンドでサービスを起動します。

# systemctl start dummy
[root@localhost system]# systemctl status dummy
● dummy.service - Dummy TCP daemon
   Loaded: loaded (/etc/systemd/system/dummy.service; disabled; vendor preset: disabled)
   Active: active (running) since 火 2018-02-06 14:01:54 JST; 2s ago
 Main PID: 17011 (dummy)
   CGroup: /system.slice/dummy.service
           mq17011 /root/work/xxxx/testcode/dummy -D

 2月 06 14:01:54 localhost.localdomain systemd[1]: Started Dummy TCP daemon.
 2月 06 14:01:54 localhost.localdomain systemd[1]: Starting Dummy TCP daemon...

終了させるには、以下のコマンドです。

# systemctl stop dummy

startもstopも基本的に何もエコーバックされてこないのでstatusで確認を取るのが賢明でしょう。

startさせた後に、OS起動時の登録を行う場合には以下のコマンドを実行してください。

# systemctl enable dummy

disableすれば起動時の登録は解除されます。

課題

Systemdで登録したサービスの再起動には、先に述べた通り、プロセスがゴースト化してしまったり、何らかの異常が発生して実際の動作を行っていない場合の対処法がありません。
そんな時には、サービスとしての登録だけは行い、monitなどのプロセス監視を併用すると良いかと思います。

次回は、このDummyサービスをmonitを併用したプロセスの自動再起動をやってみようと思います。