はじめに
国内のネットワーク機器やサーバから syslog を集めていると、日本語のメッセージだけが
????? や 譁�蟄怜喧縺� になることがあります。受信側の設定をいくら変えても直らない、
という質問をよく見かけます。
原因は単純で、RFC 3164 には文字コードの規定がないからです。そして解決策として
真っ先に思いつく「自動判定」は、実際に作ってみると期待どおりには動きません。
自分たちで syslog サーバを作っていて、自動判定を 二回実装して二回とも間違えました。
その記録を残します。
1. なぜ文字化けするのか
syslog には二つの世代があります。
| 文字コードの規定 | |
|---|---|
| RFC 5424 (2009) | UTF-8 を要求する |
| RFC 3164 (2001) | 規定なし |
現実の機器の多くは RFC 3164 で送ります。規定がないので、送信側は素直に
OS のローカルコードページでバイト列を作ります。
- 日本語 Windows のイベントログ転送エージェント → Shift_JIS (CP932)
- 韓国語 Windows → CP949
- 簡体字中国語 → GBK
受信側がこれを UTF-8 として解釈すると、非 ASCII のバイトはすべて U+FFFD
(置換文字) になります。
ここが重要で、U+FFFD になった時点で元のバイトは失われます。あとから
「やっぱり Shift_JIS でした」と設定を変えても戻りません。デコードは
受信した瞬間に正しくなければならないのです。
ログを保存してから直せばいい、が通用しない数少ない場面です。
2. 自動判定を作ってみた(一回目)
素直に考えると、バイト列を候補のコードページで読んでみて、一番「それらしい」
ものを選べばよさそうです。文字種の分布でスコアをつけました。
結果:
- 中国語を韓国語と判定した
- ドイツ語を日本語と判定した
短い syslog の一行は統計を取るには短すぎました。%LINK-3-UPDOWN のような
ASCII が大半を占める行で、数バイトの日本語部分から言語を当てる、というのは
そもそも無理があります。
3. スコアリングを直して、また間違えた(二回目)
判定ロジックを書き直しました。それでも外しました。
いちばん厄介なのが Shift_JIS と GBK の組み合わせです。同じバイト列が
どちらでも「読める漢字」になります。実際に試すとこうなります。
| 送ったもの | Shift_JIS で読む | GBK で読む |
|---|---|---|
| 日本語 | 日本語 | 擔杮岅 |
| 接続 | 接続 | 愙懕 |
| 障害発生 | 障害発生 | 忈奞敪惗 |
| 認証失敗 | 認証失敗 | 擣徹幐攕 |
右側は日本語話者には明らかにおかしいのですが、「CJK の漢字が並んでいるか」で
点をつけるアルゴリズムには区別がつきません。どちらも正しく漢字です。
一方で EUC-KR はこのバイト列をデコーダごと拒否します(置換文字になる)。
つまり「明らかに違う」ものは機械が落とせます。落とせないのは、
どちらも成立してしまう組み合わせです。
ここで方針を変えました。
4. 「壊れている」の判定だけは機械にできる
全部あきらめたわけではありません。間違ったコードページで読んだときにだけ
現れる痕跡があります。
C1 制御文字 (U+0080–U+009F) です。
function looksMisdecoded(text) {
let controls = 0;
for (let i = 0; i < text.length; i += 1) {
const code = text.charCodeAt(i);
if (code >= 0x80 && code <= 0x9f) controls += 1;
}
return controls >= 2;
}
普通の文章にこの範囲の文字はまず出てきません。一方、Shift_JIS や CP949 の
バイト列を windows-1252 として読むと、この範囲がぼろぼろ出てきます。
誤検知がほとんどない、信号として非常に素直な指標です。
つまり:
- 「何かおかしい」の判定 → 機械にできる(C1 制御文字の検出)
- 「では何が正しいか」の判定 → 機械には無理(二回失敗した)
5. 結論 — 人間は自分の言語を一目で見分ける
最終的にこうしました。
- まず UTF-8 で読む
- C1 制御文字が出たら「おかしい」と判定する
- 候補のコードページそれぞれで読んだプレビューを並べて見せる
- 人間が選ぶ
実際の出力です(インターフェース Gi0/3 がダウン を CP932 で送ったとき):
shift_jis インターフェース Gi0/3 がダウン
gbk 僀儞僞乕僼僃乕僗 Gi0/3 偑僟僂儞
windows-1251 ѓCѓ"ѓ^Ѓ[ѓtѓFЃ[ѓX Gi0/3 ‚Єѓ_ѓEѓ"
これを見て、日本語話者が一行目を選び間違えることはありません。
機械が二回失敗した問題を、人間は一瞬で解きます。
ちなみに候補が 3 つしか出ていないのは、EUC-KR や Big5 では
デコーダが拒否したか C1 制御文字が出たため除外されたからです。
「確実に違う」ものは機械が落とせます。落とせないものだけ人に見せます。
自動判定は、当たれば楽で、外れると気づかないまま壊れたログが溜まるという
性質の悪い機能でした。外したときのコストが大きいものを機械に任せない、という
判断です。
6. 送信側で直せるなら、そちらが確実
受信側の工夫より、送信側を UTF-8 にできるならそれが一番です。
- Windows のイベントログ転送: 転送ツールに UTF-8 送信の設定があれば有効にする
-
rsyslog:
$OMFileEncodingではなく、そもそもの入力を UTF-8 に揃える - RFC 5424 で送れる機器: フォーマットを 5424 にすると UTF-8 が前提になる
ただし国内の機器では選べないことが多く、その場合は受信側で対処するしかありません。
7. もう一つの落とし穴 — アプリのロケールと OS のロケール
実装中に踏んだので書いておきます。
Electron の app.getLocale() はアプリの UI 言語を返します。OS の設定とは
別物です。日本語 Windows でも、アプリが英語で動いていれば en-US が返ります。
これをフォールバックの推測に使っていて、日本語のログが windows-1252 で
読まれた事故がありました。app.getSystemLocale() と
app.getPreferredSystemLanguages() も合わせて見るようにしています。
「ロケールを取る」だけでも複数の意味があります。
まとめ
- RFC 3164 に文字コードの規定はない。機器は OS のコードページで送ってくる
- UTF-8 として読んだ時点で元のバイトは失われる。あとから直せない
- 自動判定は二回作って二回外した。短い syslog 一行では統計が足りない
- Shift_JIS と GBK は同じバイト列がどちらでも「読める漢字」になる
- 「壊れている」の判定は機械にできる。「何が正しいか」は人間のほうが速い
自分たちは syslog を受けるデスクトップアプリ
(SyslogWatch) を作っていて、
この記事の内容はそこで実際に通った道です。無料枠があるので、同じ問題で
困っている方は試してみてください。
開示: 筆者は SyslogWatch の開発者です。