syslog-ngでログウォッチ
旧Blogでも書いたことですが、結構役に立つと思うので再掲します。
これからログウォッチを行おうと思っている方ならおそらくswatchの名前ぐらいは聞いたことがあるかもしれませんが、swatchはPerlで書かれている上、よくわからないロジックで実装されており正直バグだらけです(確証はありません)。ログというのは意外に負荷がかかるもので、それを独自にtail -fしつつperlのregexをかけるのはあまりにも現実的ではありません。それでswatchなどの(しょぼい)外部アプリよりログをそもそも出力している部分にログウォッチの処理を入れたくなるのが普通の欲求になるのですが、残念ながら一般的なログツールであるsyslogとsyslog-ngはともにその役割を十分に果たすには多少力が足りません。syslogはともかく、syslog-ngは冗長なconfig文法の割には拡張性がしょぼくて、かろうじて正規表現マッチはそなえているものの、同じ処理を一定期間停止するというログウォッチには必須なスロットル機能が書けているのです。まあおそらく作者の意図としては、そういうのは外部でやってくれということなのでしょうが、やはりあの冗長な文法の割にこのカスみたいな拡張性はいかんせん納得できないのです。 というわけで、スロットル機能の欲求をみたすべく簡単なアプリケーションを作ってみました。
throttle.cpp:
#include <iostream> #include <fstream> #include <string> #include <list> #include <utility> #include <cstdio> #include <cctype> #include <ctime> #include <cassert> #include <unistd.h> #include <fcntl.h> // recommended to fix static const char* record_file = "/var/state/throttle"; static const char* mailer = "/usr/sbin/sendmail"; using namespace std; struct span_t { int hour; int minute; int second; }; bool parse_hhmmss(const char* hhmmss, span_t* span) { assert(span); int* successor[3]; successor[0] = &span->second; successor[1] = &span->minute; successor[2] = &span->hour; memset(span, 0, sizeof(span_t)); const char* p = hhmmss + strlen(hhmmss) - 1; for (int i = 0; i < 3; i++) { int* cell = successor[i]; for (int m = 1; p >= hhmmss; p--, m *= 10) { if (*p == ':') { p--; break; } else if (isdigit(*p)) *cell += (*p - '0') * m; else return false; } } return true; } void exact_getline(string& s, istream& in) { char c; while (c = in.get(), !in.eof() && c != '\n') s.append(1, c); } void action_exec(int argc, char* argv[], const char* in) { cout << ">exec" << endl; string command; for (char** p = argv; p < argv + argc; p++) { command += string("\"") + *p + "\" "; } if (!command.empty()) system(command.c_str()); else cerr << "can not execute empty command" << endl; } void action_mail(int argc, char* argv[], const char* in) { if (argc < 2) { cout << "mail usage: to subject body" << endl; return; } cout << ">mail" << endl; string mail = string(mailer) + " -io -t"; FILE* fp = popen(mail.c_str(), "w"); if (fp == 0) { cerr << "can not open mailer pipe: " << mailer << endl; return; } fputs("To: ", fp); fputs(argv[0], fp); fputc('\n', fp); fputs("Subject: ", fp); fputs(argv[1], fp); fputc('\n', fp); fputc('\n', fp); if (argc > 2) fputs(argv[2], fp); else fputs(in, fp); pclose(fp); } int main(int argc, char** argv) { bool perline = false; if (argc > 1 && strcmp(argv[1], "-l") == 0) { perline = true; argc--; argv++; } if (argc > 3) { span_t span; if (!parse_hhmmss(argv[2], &span)) { cerr << "invalid span format: " << argv[2] << endl; return 1; } do { string line; if (perline) { exact_getline(line, cin); if (line.empty()) break; } time_t current_time; time(¤t_time); // read all id and recorded time from the file and action if expired or new bool action = true; list<pair<string, time_t> > records; ifstream ifs(record_file, fstream::in | fstream::binary); if (ifs.is_open()) { while (!ifs.eof()) { string id; time_t recorded_time; exact_getline(id, ifs); ifs.read(reinterpret_cast<char*>(&recorded_time), sizeof(time_t)); if (id.empty()) break; else if (id == argv[1]) { double seconds = difftime(current_time, recorded_time); if (seconds <= (span.hour * 360 + span.minute * 60 + span.second)) action = false; } else records.push_back(make_pair(id, recorded_time)); } ifs.close(); } records.push_back(make_pair(argv[1], current_time)); // write all records ofstream ofs(record_file, fstream::out | fstream::binary); if (ofs.is_open()) { for (list<pair<string, time_t> >::iterator ite = records.begin(); ite != records.end(); ++ite) { ofs << ite->first << endl; ofs.write(reinterpret_cast<const char*>(&ite->second), sizeof(time_t)); } ofs.close(); } else { cerr << "can not open the record file: " << record_file << endl; return 2; } if (action) { string command = argv[3]; int command_argc = argc - 4; char** command_argv = argv + 4; if (command == "exec") action_exec(command_argc, command_argv, line.c_str()); else if (command == "mail") { action_mail(command_argc, command_argv, line.c_str()); } else cerr << "unknown command: " << command << endl; } } while (perline); } else { cout << "Usage: [option] id span(HH:MM:SS) (exec|mail) [...]" << endl; } return 0; }
これを適当に保存してコンパイルしてインストールしてください。
% g++ throttle.cpp -o throttle && cp throttle /usr/local/bin
使いかたは簡単です。
% /usr/local/bin/throttle Usage: throttle [option] id span(HH:MM:SS) (exec|mail) [...]
optionには -l を指定することができます。これはsyslog-ngがプロセスを維持して使いまわすための対策オプションです。idにはthrottleのIDを指定します。spanにはHH:MM:SS形式で期間を指定します。HHとMMはともに省略可能です。execは...部のプログラムを実行します。mailは...部にto subject bodyというオプションを渡します。bodyを省略すればログ本文がbodyになります。
では実際にユーザーがログインに失敗したときにメールで管理者に通知するウォッチを書いてみましょう。
syslog-ng.conf:
filter f_authfail { match("authentication failure"); }; destination d_notifyadmin { program("/usr/local/bin/throttle -l notifyadmin 5:00 mail admin@somewhere \"Authentication failure!\""); }; log { source(s_local); filter(f_authfail); destination(d_notifyadmin); };
これで5分以内に何回ログインに失敗しようがメールが複数送られてくることはありません。sendmailといいprocmailといいsyslog-ngといい簡単なものをわざわざ難しくする才能には勝てません。
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/syslog-ng30ed30a630a930c330c1/tbping