ログに猫を召喚する:HTTPエラー検知CLIツール「http-catlog」
はじめに
前回、HTTPリクエストを送って猫画像でステータスを表示する「httpcat」を作りました。
今回はその応用編として、「リアルタイムでログを監視して、エラーが出たら猫を召喚する」という、より実用的な(?)ツールに挑戦しました。
サーバーログを監視していて、404や500エラーが出た時に少し癒やされたいと思ったことはありませんか?
そこで開発したのが、ログをリアルタイムで監視し、HTTPエラーコードを検知すると可愛い猫の画像を表示してくれるCLIツール「http-catlog」です。http.catの猫画像をターミナルに直接表示することで、エラー監視を少しだけ楽しくします。
http-catlogとは
http-catlogは、ログファイルや標準入力からHTTPステータスコード(特に4xx/5xx)を検出し、そのコードに対応する猫の画像をhttp.catから取得してターミナルに表示するCLIツールです。
使い方
パイプモード
$ tail -f /var/log/nginx/access.log | catlog
192.168.1.1 - - [09/Nov/2025:10:23:45] "GET /notfound HTTP/1.1" 404 162
🐱 404 Detected! 🐱
[ターミナルに猫の画像が表示される]
ファイル監視モード
$ catlog -f /var/log/app.log
# tail -fのようにファイルを監視し、エラーが出たら猫を表示
コマンド実行モード
$ catlog -e "docker logs -f mycontainer"
# コマンドの出力を監視
実行例

401エラーを検出すると、猫の画像がターミナルに表示される
技術スタック
このプロジェクトでは以下のクレートを使用しました:
- clap: CLIの引数パーサー(derive機能を使用)
- tokio: 非同期ランタイム
- reqwest: HTTP通信(猫画像の取得)
- image: 画像処理
- viuer: ターミナルでの画像表示
- regex: HTTPステータスコードの検出
- notify: ファイル監視
- colored: カラフルな出力
- anyhow: エラーハンドリング
実装のポイント
1. 3つの動作モードの実装
http-catlogは3つの異なる入力モードをサポートしています:
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if let Some(ref file_path) = args.follow {
follow_file(file_path, &args, status_filter.as_deref()).await?;
} else if let Some(ref command) = args.exec {
exec_command(command, &args, status_filter.as_deref()).await?;
} else {
pipe_mode(&args, status_filter.as_deref()).await?;
}
Ok(())
}
パイプモード
標準入力から1行ずつ読み取り、リアルタイムで処理します:
async fn pipe_mode(args: &Args, status_filter: Option<&[u16]>) -> Result<()> {
let stdin = io::stdin();
let reader = stdin.lock();
for line in reader.lines() {
let line = line?;
process_line(&line, args, status_filter).await?;
}
Ok(())
}
ファイル監視モード
notifyクレートを使ってファイルシステムの変更を監視します:
async fn follow_file(file_path: &str, args: &Args, status_filter: Option<&[u16]>) -> Result<()> {
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Config::default())?;
watcher.watch(path, RecursiveMode::NonRecursive)?;
loop {
if rx.try_recv().is_ok() {
// ファイルが更新されたら新しい行を読み取る
while reader.read_line(&mut line)? > 0 {
process_line(trimmed, args, status_filter).await?;
line.clear();
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
2. HTTPステータスコードの検出
正規表現を使ってログからHTTPステータスコードを抽出します:
async fn process_line(line: &str, args: &Args, status_filter: Option<&[u16]>) -> Result<()> {
// 元の行を常に出力
println!("{}", line);
// HTTPステータスコードを検出(4xx, 5xx)
let re = Regex::new(r"\b([45]\d{2})\b")?;
if let Some(caps) = re.captures(line) {
if let Some(code_match) = caps.get(1) {
let code: u16 = code_match.as_str().parse()?;
// フィルタリングロジック
let should_show = if let Some(filter) = status_filter {
filter.contains(&code)
} else if args.all {
true
} else {
(400..600).contains(&code)
};
if should_show {
display_cat_for_status(code, args).await?;
}
}
}
Ok(())
}
3. 猫画像の取得と表示
http.cat APIから画像を取得し、viuerを使ってターミナルに表示します:
// 画像取得
async fn fetch_cat_image_for_status(status_code: u16) -> Result<Vec<u8>> {
let url = format!("https://http.cat/{}", status_code);
let client = reqwest::Client::new();
let response = client.get(&url).send().await?;
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
// ターミナルに表示
fn display_image(image_data: &[u8], size: u32) -> Result<()> {
let img = ImageReader::new(Cursor::new(image_data))
.with_guessed_format()?
.decode()?;
let conf = Config {
transparent: true,
absolute_offset: false,
width: Some(size),
height: None,
..Default::default()
};
viuer::print(&img, &conf)?;
Ok(())
}
4. CLIインターフェースの設計
clapのderive機能を使って、宣言的にCLIを定義します:
#[derive(Parser, Debug, Clone)]
#[command(name = "catlog")]
#[command(about = "Monitor logs and display cat images on HTTP errors")]
struct Args {
/// Follow a file (like tail -f)
#[arg(short, long)]
follow: Option<String>,
/// Execute a command and monitor its output
#[arg(short, long)]
exec: Option<String>,
/// Image size in characters
#[arg(long, default_value = "60")]
size: u32,
/// Don't display images
#[arg(long)]
no_image: bool,
/// Show cats for all status codes
#[arg(long)]
all: bool,
/// Comma-separated list of specific status codes to match
#[arg(long)]
status: Option<String>,
}
5. 柔軟なフィルタリング
ステータスコードのフィルタリングをOptionとして実装:
// コンマ区切りのステータスコードをパース
let status_filter = args.status.as_ref().map(|codes| {
codes
.split(',')
.filter_map(|s| s.trim().parse::<u16>().ok())
.collect::<Vec<_>>()
});
// 特定のコードのみ監視
// $ catlog --status 404,503
6. コマンド実行モードの非同期処理
外部コマンドの標準出力と標準エラー出力を並行して処理:
async fn exec_command(command: &str, args: &Args, status_filter: Option<&[u16]>) -> Result<()> {
let mut child = Command::new("sh")
.arg("-c")
.arg(command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
// 並行処理
let stdout_handle = tokio::task::spawn_blocking(move || {
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
// 処理
}
});
let stderr_handle = tokio::task::spawn_blocking(move || {
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
// 処理
}
});
stdout_handle.await?;
stderr_handle.await?;
Ok(())
}
ターミナル互換性
画像表示はviuerクレートに依存しています。iTerm2やKittyなどの最新ターミナルでは綺麗に表示されますが、環境によっては表示品質が異なる場合があります。
画像が表示されない場合は、--no-imageオプションでテキストのみの表示も可能です:
$ catlog -f app.log --no-image
Error 503 Service Unavailable
🐱 503 Detected! 🐱
まとめ
http-catlogは、ログ監視という実用的な機能に「癒やし」という要素を加えたCLIツールです。実装を通じて学べた技術:
- 複数の入力ソースの処理(stdin、ファイル監視、コマンド実行)
- 正規表現によるパターンマッチング
- 非同期HTTP通信と画像処理
- ターミナルでのリッチな出力
エラーログに疲れた時、猫が癒やしてくれるかもしれません。
$ tail -f /var/log/nginx/error.log | catlog
# 503エラーが出たら猫が現れる 🐱
インストール方法
http-catlogはcrates.ioで公開されており、cargoを使って簡単にインストールできます:
$ cargo install http-catlog
インストール後は、すぐに使い始められます:
$ echo "Error 404 Not Found" | catlog
Discussion