🐱

ログに猫を召喚する: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"
# コマンドの出力を監視

実行例

http-catlogの実行例

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