2014年4月4日

stderr(標準エラー)をsyslog にリディレクトする

カーネルからNETLINKで情報をデーモンプロセスに送るアプリケーションを書いている。

最初はメモリアロケーションだとかファイルのオープンだとかの「起こり得ない」エラーの処理はてきとーにスタブコードを書いておいた。機能が順調に動作してコードが洗練されてくるに連れ、エラーが起こってもメモリリークが起きないようにだとかの細々した配慮を施したが、エラーの報告は取りあえず warn(3)とかで簡単に済ませてきた。しかしプロジェクトが完成に近づいたところで、ロギングもちゃんと実現しなければならなくなった。

エラーが起こった場合メイルとか何かでユーザに知らせることも考えられるが、取りあえずはsyslogを使ってシステムのログファイルに記録すればよい、となった。
 
ところで、コードの開発中はプロセスはフォアグラウンドで動作させていたので、エラーメッセージはwarn(3)で標準エラーに書き出しているだけだったが、これをできるだけ(と言うより「できれば」)変更しないでsyslogに喰わせてやりたい。またトラブルシュート用にコマンドオプションで従来どおり標準出力にも書き出せるようにしたい。…ということを「簡単に」実現したい、というのが今回の課題。

stderr = popen("/usr/bin/logger -i -t mydaemon", "w");

のようにstderrをパイプでloggerプロセスにつないでやることもできるが、もしこのデーモンが死んだときにloggerプロセスがいつまでも残っているかも知れないなどの懸念がある。

ぐぐってみたら、あった、あった。ユーザ定義の入出力ハンドラを持つストリームをオープンしてstderrと置き換えるいう方法だ。

#include <stdio.h>
#include <err.h>

static bool dopt, lopt;
static FILE *stderr2;

static size_t logit(void *cookie, const char *buf, size_t len)
{
        if (stderr2)
                fwrite(buf, len, 1, stderr2);
        syslog(LOG_WARNING, "%.*s", (int)len, buf);
        return len;
}

static cookie_io_functions_t log_fns = {
        NULL, (cookie_write_function_t *)logit, NULL, NULL,
};

static void setup_logger(void)
{
        if (dopt) {
                int fd = dup(fileno(stderr));

                if (fd < 0)
                        err(1, "failed to dup stderr.");

                if (!(stderr2 = fdopen(fd, "w")))
                        err(1, "failed to fdopen stderr.");
        }

        fclose(stderr);

        stderr = fopencookie(NULL, "w", log_fns);
        setvbuf(stderr, NULL, _IOLBF, 0);
}


int main(int argc, char *argv[])
{
...
        if (lopt)
                setup_logger();
...
        warnx("This is output to stderr, which is redirected to logit().");

上が今回試してみて動作したコードの一部。

fprintf(3)warn(3)など、凡そ標準エラーストリームにこのプロセス内で書き出すデータはすべてlogit()にリディレクトされるから、その後は如何様にでも処理できる。

キモはsetup_logger()から呼ばれるfopencookie(3)が、log_fnsにユーザの定義する読み込み、書き出し、シーク、クローズのハンドラを持つストリームをオープンすること。GLIBCではstdin、stdout、stderrはマクロではなく普通の変数だから、新しい値をいつでも代入でき、その後はfprintf(3)warn(3)などは新しいstderrに書き込むようになる。

もう一つのキモはstderrを更新した後に呼んでいるsetvbuf(3)。これがないとストリームバッファがいっぱいになるまで出力ハンドラは呼ばれないから、エラーメッセージのように何か起きたらリアルタイムで出力したい用途には好ましくない。ここではバッファ単位を行にしてうまく行っている。

こうして標準出力をまるごとsyslog(3)にリディレクトすることに成功したが、問題点があることも分かった。
  • リディレクションはストリームレベルなのでプロセス内で完結しており、例えば子プロセスの動作を買えることはできない
  • warn(3)はストリームに出力する前にメッセージにプログラム名をプリフィックスとして付加するが、syslog(3)もまた同様の処理をするのでプログラム名が二重に付加されて見苦しい
二重プリフィックスの問題はちょっと致命的だったので、せっかくリディレクションに成功したものの、この方法は諦め、warn(3)を__warn()という包み込み関数(またはマクロ)で置き換えてその中で場合分けして処理するという平凡な構成を最終的な実装とすることにした。

お粗末さまでした。

0 件のコメント:

コメントを投稿