メインコンテンツに移動

log4c+

log4cplusは, プログラムの出力を書き出すライブラリーです.

は? write(*,*)  'FORTRAN'とか puts("C99") で,かけるじゃんか,と思う人もいるでしょう,でも,それではダメなケースが多いのです.いや,本格計算ではほとんどダメなんじゃないか?

  • MPIやOpenMPでプログラムを書きました.そしたら,こげな画面になってしまいました.よ,読めねえよ・・・
nt=12n542432nt=1244 col=2t=32n
542444 col=212542444 col=3
232t=232
nt=125412542444 col=2324
44 col=2323223232
  • ジョブで実行してたらタイムアウトしてしまいました.すると,タイムアウトで画面出力を全て失いました.
    • 困りましたね,途中でデータ保存した時の時間ステップの現在の数値が不明です.バックアップが役に立たねえ・・・全部やり直しだ
  • ジョブで,わさわさ画面に書き出していたら,
    • 計算ノードのディスクを使い果たしししまい,肝心の最終データが出力できませんでした.無念
    • 画面出力が900GBになりました.その巨大ファイルをエディターで開こうとしたら2日かかりました.開きましたが,コンピュータが反応しなくなりました.
    • ディスクIOが渋滞して計算が前に進みません.CPU利用率が20%になった
    • 大量に画面出力するジョブを打ってしまった.ジョブなので終わるまでミスに気づかず,3TBも書いちまった
    • 計算自体は30分で終わったのに,ジョブが終了しない・・・と思ったら,3TBのログのお持ち帰りに,あと2時間かかるのであった.途中でキャンセルしたら計算結果も初期データも全部壊れたwww
      • 嫌なこと思い出したぞ・・・この時潰れた初期データを作るのに1ヶ月かかった
  • ジョブで実行して,画面出力をファイルに書き出すことにしました.
    • バッファーされちまって,画面出力が計算終了まで出てこないよ・・・
    • どのくらいの計算速度か,見たかったのに・・・

画面出力なんて,役に立たないのです.そこで,ログ書きライブラリーの登場です.ログ書きサーバーにログを送りつけることが可能になるので,データファイルとログが別個に管理されることになり,上記の問題が解消されるわけです.ま,もちろん

  • プログラム実行中に,画面に何も出力しなくなる
    • そりゃ指定した記録デバイスに全部書かれているんだから,そこの画面には出ねえってば

という欠点が出てきますけどね.あ,それから,以下の説明はVersion 2.0.4, CentOS8のやつだからね. Macも同じだけどな


ローカルロガー

最初にローカルファイルに書き出す「ローカルロガー」を説明しましょう.

接続インスタンス作成

#include <log4cplus/socketappender.h>  //ネットワークロガー
#include <log4cplus/configurator.h>         //ローカルロガー
#include <log4cplus/spi/loggingevent.h>  
#include <log4cplus/loggingmacros.h>
#include <log4cplus/logger.h>
#include <boost/filesystem.hpp>
...
namespace FS=boost::filesystem;
...
int main(){
...
    FS::path home_folder=std::getenv("HOME");
    FS::path log4_file=home_folder/"log"/"mymy.cfg";   //設定ファイル mymy.cfg の置き場所を指定
    if (!FS::exists(log4_file)) {
        std::cerr << "Can not find log4c++ configuration:" << log4_file.string() << std::endl;
        return EXIT_FAILURE;
    }
    log4cplus::PropertyConfigurator config(log4_file.string());
    config.configure();
    log4cplus::initialize();
    log4cplus::Logger logger=log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("mymy")); //設定ファイルの項目を指定

これでログ書きインスタンスloggerが作成される. 

設定ファイル

プログラムを動かすには, 設定ファイル(上の例では mymy.cfg )で項目mymyを指定しなければならない:

log4cplus.rootLogger=DEBUG,mymy
log4cplus.appender.mymy=log4cplus::RollingFileAppender
log4cplus.appender.mymy.File=${HOME}/log/mymy.log    ←ログを記録するファイル名を指定. 青字のとこ間違えるとハマるよ
log4cplus.appender.mymy.MaxFileSize=50MB          ← 1ファイル50MBで切り替える
log4cplus.appender.mymy.MaxBackupIndex=1000   ← 1000回分は保存しておく
log4cplus.appender.mymy.layout=log4cplus::PatternLayout
log4cplus.appender.mymy.layout.ConversionPattern=%D{%m/%d/%y %H:%M:%S} [%i] %p <%x> %m%n

最後の行の雰囲気から分かるように, この設定では, ログファイルに

01/13/20 09:33:25 [12234] INFO <context> メッセージ

という出力になる. 最初はメッセージの日時,[プロセスID] メッセージ種別 <後で述べるコンテキスト> ,である.また, 1ファイルのサイズは最大50MBで, 1000回分以上は自動で消去されるので, 50GB以上のログサイズには決して到達しないことになる.

*mymy.Fileのとこで,「そのマシンでのHOMEフォルダ」を適用したいなんて思うあなたは,${HOME}/相対パス と書いてね.

*不可解なハナモゲラ記号の意味は

%d 日付。これは %d{yyyy-MMM-dd HH:mm:ss,SSS} みたいに細かく書けるし、TTCCLayout で 
使ったシンボルを使って、 %d{ISO8601} みたいに書いても良い。
%t ログを生成したスレッド名
%p ログレベル(FATAL, INFO, ....)
%x NDC名
%m メッセージ
%l %F%L
%F ファイル名
%L 行番号
%n 改行

 

 

ログ書き

実際のログ出力は,下のように記述する:

LOG4CPLUS_INFO(logger,"Hell World");   // INFO文字列を出力
LOG4CPLUS_INFO(logger,std::hex << std::this_thread::get_id() << std::dec << "<<" << message);  // 画面出力みたい
LOG4CPLUS_WARN(logger,"This is warning"); // ログファイルにWARNをつけて出力
LOG4CPLUS_ERROR(logger,"This is error"); // ログファイルにERRORをつけて出力

ビルド

あんたのCMakeLists.txtにちょびっと追加したらふつーにビルドできると思うよ:

#....
find_package(PkgConfig)
pkg_check_modules(LOG4CPLUS REQUIRED log4cplus)
#.....
link_directories(${LOG4CPLUS_LIBRARY_DIRS})
target_link_libraries(貴様の実行ファイル ${LOG4CPLUS_LIBRARIES} ${Boost_Libraries} )

コンテキスト

メッセージをどの部分で出力したのか,が明らかになるように, コンテキストを追加することができる.

log4cplus::getNDC().push(LOG4CPLUS_TEXT("con1"));;
//旧 log4cplus::NDCContextCreator* _context=new log4cplus::NDCContextCreator(LOG4CPLUS_TEXT("con1"));

以上により,ログファイルに<con1>が追加されるようになる. さらに

log4cplus::getNDC().push(LOG4CPLUS_TEXT("con2"));
//旧 log4cplus::NDCContextCreator* _context2=new log4cplus::NDCContextCreator(LOG4CPLUS_TEXT("con2"));

とすると, どちらも有効になって, ログファイルに<con1><con2>が追加される. pushしたコンテキストはremove(全部消す)とかpop(最後のだけ消す)可能である:

log4cplus::getNDC().pop();
log4cplus::getNDC().remove();

なお, コンテキストはスレッド毎に別の値になる:

log4cplus::getNDC().push(LOG4CPLUS_TEXT("main"));;
std::thread v([&]{ LOG4CPLUS_INFO(logger,"New thread"); });
LOG4CPLUS_INFO(logger,"Org thread"); 

であると, ログは

01/13/20 14:05:25 [18411] INFO <main> Org thread
01/13/20 14:05:25 [18411] INFO <> New thread

となってしまい, スレ違いは文脈がちゃうという認識である.

log4cplus::NDCContextCreator* _context=new log4cplus::NDCContextCreator(LOG4CPLUS_TEXT("main-thread"));
std::thread v([&]{
    std::ostringstream oss; oss<<"new"<<std::hex<<std::this_thread::get_id();
    log4cplus::NDCContextCreator* _context=new log4cplus::NDCContextCreator(LOG4CPLUS_TEXT(oss.str()));
    LOG4CPLUS_INFO(logger,"New thread"); 
});
LOG4CPLUS_INFO(logger,"Org thread"); 

であると, ログは

01/13/20 14:19:19 [18761] INFO <main> Org thread
01/13/20 14:19:19 [18761] INFO <new7fe6d2232700> New thread

となる. スレッドごとに別なんてありえねえ,メインスレッドと全部同じにしやがれ的な場合,

auto KeepMainThreadNDCStackHere=log4cplus::getNDC().cloneStack(); //メインスレッドでNDC状態を保存
std::thread([...,&KeepMainThreadNDCStackHere](...){
    ...
    log4cplus::getNDC().inherit(KeepMainThreadNDCStackHere);
    ...
}):

これでスレッドも同じNDCだ!


ローカルSYSLOG

OS付属のロガーにファイルを書かせることも可能である. とはいえ, OSであるので,まずは許可を出さねばならぬ. rootユーザーで, /etc/rsyslog.d/mymy.conf に設定を書く:

/etc/rsyslog.d/mymy.conf:
local3.info /var/log/mymy.log

これは, このマシンのlocal3ログを, /var/log/mymy.log に出力せよ, という命令である. このようにユーザーが使って良いチャンネルは local0, local1,..., local7 の7つしかないので注意. 準備が終わったら

systemctl restart rsyslog

これで, /var/log/mymy.log が出現することを確認(出現しなければ logger -p local3.info STAND-UP-MYMY とでも入力してみる). 

設定ファイル

設定ファイルはこんな感じになる:

log4cplus.rootLogger=DEBUG,mymy
log4cplus.appender.mymy=log4cplus::SysLogAppender
log4cplus.appender.tcp.facility=local3  ←上で指定したやつだよ
log4cplus.appender.mymy.layout=log4cplus::PatternLayout
log4cplus.appender.mymy.layout.ConversionPattern=%p <%x> %m%n

これで, /var/log/mymy.log にはこんな記録が残ることになる:

Jan 13 16:46:23 h225 mymy[22111]: INFO <main> Org thread
Jan 13 16:46:23 h225 mymy[22111]: INFO <new7f986812b700> New thread

ええっと, /var/log/mymy.log がrootしか読み書きできねえのは, 起動時のなんかほれ,設定するやつでなんとかなるだろう.


リモートSYSLOG

どうせだから,そのソフトウェアシステムのログを1台のマシンに集約させたいと思うでしょう.これも簡単です.集約先のマシンで,SYSLOGの設定をするついでに,ネットワーク機能を有効にしときます.しゅなわち,

/etc/rsyslog.conf:
...
# Provides UDP syslog reception
# for parameters see http://www.rsyslog.com/doc/imudp.html
module(load="imudp") # needs to be done just once   ←餡コメント
input(type="imudp" port="514")   ←餡コメント
# Provides TCP syslog reception
# for parameters see http://www.rsyslog.com/doc/imtcp.html
module(load="imtcp") # needs to be done just once    ←餡コメント
input(type="imtcp" port="514")    ←餡コメント

準備が終わったら

systemctl restart rsyslog

設定ファイル

設定ファイルはこんな感じになる:

log4cplus.rootLogger=DEBUG,mymy
log4cplus.appender.mymy=log4cplus::SysLogAppender
log4cplus.appender.tcp.facility=local3
log4cplus.appender.mymy.layout=log4cplus::PatternLayout
log4cplus.appender.mymy.layout.ConversionPattern=%p <%x> %m%n
log4cplus.appender.mymy.host=10.249.229.225 ←集約先のホスト名とかアドレス
log4cplus.appender.mymy.tcp=true     ← こだわりがなければ mymy.udp=true でもいいけど
log4cplus.appender.mymy.port=514 

ううん, TCPで送りつけると,ログマシンがダウンしている間に計算ノードの計算が止まるかもしれぬ.そのときの考え方は人間に対する依存性が存在する:

  • 記録が取れないのに,計算進めるなよボケが
  • 記録が取れないくらいで,計算止めるなクソ野郎

ううん.これはだからTCPかUDPかは,選ばないとダメなのでは.

これで, /var/log/mymy.log にはこんな記録が残ることになる(面白いので,一部別のマシンで実行した)

Jan 13 08:00:06 h225.229.249.10.1016485.vlan.kuins.net [22604] INFO <main> Org thread
Jan 13 08:00:06 h225.229.249.10.1016485.vlan.kuins.net [22604] INFO <new7ffa903a7700> New thread
Jan 13 08:00:58 h224.229.249.10.1016485.vlan.kuins.net [885] INFO <main> Org thread
Jan 13 08:00:58 h224.229.249.10.1016485.vlan.kuins.net [885] INFO <new7f403caf5700> New thread

ロギングサーバ

SYSLOGで集約できるとはいえ,OS付属のものですし動かない場合も多々ございましたので,自作ログサーバを使うという手もあります.

サーバコード

log4c++に添付されているサーバーコードを利用して作成します.WebMixのやつだとこんな感じ:

int netlog(std::string ip,boost::filesystem::path conf,unsigned int port){
    unsigned long serial=0;
    const log4cplus::tstring configFile = conf.string();
    log4cplus::PropertyConfigurator config(configFile);
    config.configure();
    log4cplus::helpers::ServerSocket serverSocket(port);
    if (!serverSocket.isOpen()) throw(std::runtime_error("netlog::Could not open socket, may
be port " << port << " is in use.")); Reaper reaper; ClientThread *thr; for (;;thr->start()) thr=new ClientThread(serverSocket.accept(),reaper,serial++); return static_cast<int>(EXIT_t::SUCCESS); };

重要そうなクラスReaperとClientThreadのコードは, log4c++のsimpleserver/フォルダーに転がっていますのでコピペです. あ?main()はテキトーに足す必要があります.

  • スレッドで上記関数を呼び出して,片方がサーバー,片方がクライアントで動かそうとしたら,動かなかった.
  • 仕方ねえので,fork()してexec()で,動いた. そりゃな.

SYSLOG方式と異なるのは,log4c++の設定ファイルはサーバー「のみ」で与えるってことです.

接続インスタンス作成

#include <log4cplus/socketappender.h>  //ネットワークロガー
#include <log4cplus/configurator.h>         //ローカルロガー
#include <log4cplus/spi/loggingevent.h>  
#include <log4cplus/loggingmacros.h>
#include <log4cplus/logger.h>
#include <boost/filesystem.hpp>
...
namespace FS=boost::filesystem;
...
int main(){
    ...
    std::string logger_host="サーバーのホスト名";
    unsigned int logger_port=サーバーのポート番号
    log4cplus::initialize();    ←いきなりのinitialize
    log4cplus::tstring serverName = logger_host;
    log4cplus::SharedAppenderPtr SockApp(new log4cplus::SocketAppender(logger_host, logger_port, serverName));
    SockApp->setName(LOG4CPLUS_TEXT("mymy"));  ← ん?今見ると,これ,必要なのかな?しらねーや
    log4cplus::Logger::getRoot().addAppender(SockApp);
    log4cplus::Logger logger=log4cplus::Logger::getInstance(LOG4CPLUS_TEXT("mymy")); //設定ファイルの項目を指定

これでログ書きインスタンスloggerが作成. クライアント側には,設定ファイルはありません.

File attachments