Raspberry Pi OSでプログラム独自のリングバッファロギング

Raspberry Pi OSで`dmesg`のオレオレ版的なモノが欲しくなって探したら、期待したものを見つけたので紹介します。

何かしらのdaemonを作って走らせる際に、「ログを吐いておきたいけど`journald`に貯めておくほどではなく、気になるときに最新のを少し見れればいい」というぐらいの温度感で標準出力を見たいときに便利です。`syslog`にゲロゲロ吐かれてるけどそんなにいらんねん、とかいう時にも使えそう。

# emlog -- the EMbedded-system LOG-device
https://github.com/nicupavel/emlog

開発は2018年で止まっていますが、2021年1月時点でのRasperry Pi OS 5.4.72+でビルド[^raspi]して普通に動きました。

`dmesg`っぽく動くリングバッファを任意の名前で作ることが出来、このリングバッファは名前付きパイプと違い非同期で読み込みと書き込みを行えます。

[^raspi]: ビルドしたマシンはRaspberry Pi Zero W。こんな小さなやつでもビルドできちゃうのすごい。

## ビルド方法
カーネルモジュールのビルドについては、[Raspberry Pi でカーネルモジュールビルドの準備](https://qiita.com/newtypehiro919/items/336e09340e728a94a9f7)を参照しました。

書いてあるとおりに準備し、githubをcloneして`make`するだけ。わたしの環境では`KDIR`で参照する`/lib/modules/5.4.72+/build`のsymlinkがなんか辿れなかった[^symlink]ので、`make`時に`KDIR`を別途指定しましたが、それぐらい。

## インストール

ビルドして出来た`emlog.ko`を適当な場所において`modprobe`で入れたり`/etc/modules`に追記して再起動すると、`/dev/emlog`が生えます。

同時にログバッファを作るコマンド`mkemlog`が出来ているので、`/usr/local/sbin`とかに入れましょう。

### mkemlogのつかいかた
> $ mkemlog
> mkemlog: usage: mkemlog <logdevname> [size_in_kilobytes] [mode] [uid]

引数には「ログバッファのファイルパス」「バッファサイズ」「ファイルモード」「ファイルオーナUID」を取ることが出来ます。このコマンドは内部で`mknod`を呼ぶので`root`で起動しないとコケます。

ファイルモードはデフォルトだと`660`[^mkemlog]のようです。

[^symlink]: なにかあるんでしょうけど、理由を調べてない。。。
[^mkemlog]: githubの[mkemlog.c](https://github.com/nicupavel/emlog/blob/master/mkemlog.c#L33)を参照。

# systemdで管理する
Raspberry Pi OSはsystemdでdaemonを管理しているので、それに乗ります。

なお最近は`StandardOutput`に`file:/foo/bar`とか書けます[^systemd.exec]が、`mkemlog`が走る前に普通のファイルを生成するので衝突します。`ExecStart`で起動する際に`sh`経由で起動しリダイレクトさせる古の手法を使います。

[^systemd.exec]: 詳細は[systemd.exec](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)を参照。

```ini
[Service]
StandardOutput = null
StandardError  = journal
```

### rootで起動し、終了時にログバッファを消す

```ini
[Service]
ExecStartPre = mkemlog /tmp/hoge.log 4 0644
ExecStart    = /bin/sh -c 'exec /usr/local/sbin/hoge > /tmp/hoge.log'
ExecStopPost = rm /tmp/hoge.log
```

特段コメントすることのない、普通の`Service`セクションです。

### rootで起動し、終了時にログバッファを消さない

```ini
[Service]
ExecStartPre = -mkemlog /tmp/hoge.log 4 0644
ExecStart    = /bin/sh -c 'exec /usr/local/sbin/hoge > /tmp/hoge.log'
```

ログバッファを消していないので、`restart`した場合に`mkemlog`が`File exists`で失敗します。そこで`ExecStartPre`で実行するコマンドにプレフィクス`-`をつけて、実行結果によらず成功判定[^systemd.service]させます。


### nobodyで起動、終了時にログバッファを消す

```ini
[Service]
User         = nobody
ExecStartPre = +mkemlog /tmp/hoge.log 4 0644 65534
ExecStart    = /bin/sh -c 'exec /usr/local/sbin/hoge > /tmp/hoge.log'
ExecStopPost = rm /tmp/hoge.log
```

`ExecStartPre`で実行するコマンドにプレフィクス`+`をつけるとrootでコマンドを起動[^systemd.service]します。

[^systemd.service]: 詳細は[systemd.service](https://www.freedesktop.org/software/systemd/man/systemd.service.html)を参照。

### nobodyで起動、終了時にログバッファを消さない

```ini
[Service]
User         = nobody
ExecStartPre = -+mkemlog /tmp/hoge.log 4 0644 65534
ExecStart    = /bin/sh -c 'exec /usr/local/sbin/hoge > /tmp/hoge.log'
```

`ExecStartPre`でプレフィクスを2つつける、合せ技。

## バッファリングについて
`mkemlog`で生成するログバッファは書き込まれたものをすぐに読むことが出来ますが、書き込む側がバッファリングしているとそれに準じるので注意が必要です。

例えばPerlの場合STDOUTをターミナル以外へリダイレクトするとブロックバッファリングになります[^perlvar]。

バッファリングはPerlだと`$|`で制御できますし、プログラム実装をいじらない場合は[stdbuf](https://linuxjm.osdn.jp/html/GNU_coreutils/man1/stdbuf.1.html)や[unbuffer](http://expect.sourceforge.net/example/unbuffer.man.html)のようなツール[^pb]を使うことになります。

[^perlvar]: [perlvar - Perl で定義済みの変数](https://perldoc.jp/docs/perl/perlvar.pod) ([英語版](https://perldoc.perl.org/perlvar)) の「`$|`」を参照。
[^pb]: ちなみにPerlで試した場合、unbufferは効果があったけどstdbufは効果がありませんでした。