kickflow Tech Blog

株式会社kickflowのプロダクト開発本部によるブログ

Jaeger と Claude Code で始めるローカル環境でのパフォーマンスチューニング

Jaeger はドイツ語でハンターを意味します

こんにちは。開発チームでエンジニアリングマネージャーをしている森本です。

みなさんはローカル開発環境でのパフォーマンスチューニングはどのように行っていますか? 本番環境では、APM(Application Performance Management)ツールを導入しているケースが多いと思いますが、ローカル環境ではなかなか良い選択肢が見つからず、悩んでいるエンジニアも多いのではないでしょうか。 本記事では、APIモードのRailsアプリケーションにおけるパフォーマンスチューニングの課題と、その解決策としてJaegerとClaude Codeを組み合わせた方法をご紹介します。

まず、Jaegerに行きつくまでにいくつかの方法を試しましたが、どれも決定打に欠けていました。

試行錯誤

Claude Code × log/development.log

最初はClaude Codeに、Railsのログ(log/development.log)を直接読ませていました。 しかし、Railsのログは人間が読むには良いですが、非構造データの塊です。 AIに食わせてもN+1の検知や非効率な処理を見つけることは難しく「具体的にどのメソッドのどの行がボトルネックなのか」という核心には辿り着けませんでした。

rack-mini-profiler

次に試したのはrack-mini-profilerでした。 しかし、これもAPIモードでは期待した結果にはなりませんでした。 このツールは基本的に「HTMLレスポンスのbodyに管理画面のスクリプトを注入する」という仕組みで動作します。しかし、APIが返すのはJSONです。 レスポンスに情報は含まれますが画面側で実行されるAPIの情報を解析するのは手間でした。

{ "status": "ok", "data": [...] }

Jaeger

APMツールっぽい表示ができるローカル環境で動くものがないかとGeminiに相談したところ、何個か提案してもらった中に「Jaeger」(イェーガー)がありました。 JaegerはCNCF(Cloud Native Computing Foundation)がホストする分散トレーシングシステムで、主にマイクロサービス環境でのトレース収集に使われます。 通常は、マイクロサービス間のリクエストを追跡するために使われますが、単一のRailsアプリケーション内でのパフォーマンス分析にも応用できることがわかりました。 Dockerコンテナを立ち上げて簡単に導入できる点も魅力的でした。

www.jaegertracing.io

デモ環境があるので興味がある方は見てみてください。

Jaegerの導入

Dockerコンテナの立ち上げ

公式サイトにあるDockerコマンドを使って、Jaegerのコンテナを立ち上げます。

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

Gemの追加

OpenTelemetry関連のGemを追加します。

# Gemfile
group :development do
  gem "opentelemetry-exporter-otlp"
  gem "opentelemetry-instrumentation-all"
  gem "opentelemetry-sdk"
end

initializer の設定

開発環境でのみ有効になるよう設定します。

# config/initializers/opentelemetry.rb

require "opentelemetry/sdk"
require "opentelemetry/instrumentation/all"
require "opentelemetry-exporter-otlp"

# 開発環境、または環境変数で明示された場合のみ有効化する
if Rails.env.development? || ENV["OTEL_EXPORTER_OTLP_ENDPOINT"].present?
  OpenTelemetry::SDK.configure do |c|
    # 1. サービス名 (Jaegerの画面でこの名前で表示されます)
    # 複数のアプリがある場合、ここで識別します。
    c.service_name = "my-rails-api-local"

    # 2. 自動計装 (Auto Instrumentation) の有効化
    # Rails, ActiveRecord, Rack, Net::HTTP, Sidekiq などを一括でフックします
    c.use_all({
                "OpenTelemetry::Instrumentation::ActiveRecord" => {
                  # DBクエリのパラメータを記録するかどうか
                  # :include => 値をそのまま記録 (デバッグに便利だが機密情報に注意)
                  # :obfuscate => 値を ? に置き換える (セキュリティ重視)
                  db_statement: :obfuscate,
                  db_statement_limit: 100_000,
                },
                "OpenTelemetry::Instrumentation::Rack" => {
                  # 特定のエンドポイントを除外する場合の設定 (ヘルスチェックなど)
                  untraced_endpoints: ["/health", "/up"],
                },
              })

    # 3. ログ出力の設定 (デバッグ用)
    # 設定ミスでデータが送れない時にエラーログを出す設定です
    c.logger = Logger.new($stderr, level: ENV.fetch("OTEL_LOG_LEVEL", "INFO").to_s)
  end
end

ActiveSupport::Notifications.subscribe("sql.active_record") do |*_args|
  # 現在実行中のスパン(Jaegerの記録単位)を取得
  span = OpenTelemetry::Trace.current_span

  # スパンが有効、かつアプリからの呼び出しである場合のみ実行
  if span.context.valid?
    # バックトレース(呼び出し履歴)から「app」フォルダを含み、かつ「gem」ではない行を探す
    location = caller.find do |line|
      line.include?(Rails.root.to_s) && line.exclude?("/vendor/") && line.exclude?("/gems/")
    end

    if location
      # ファイルパスと行番号を分離
      # 例: "/app/controllers/users_controller.rb:10:in `index'"
      file, line = location.split(":")[0..1]

      # 絶対パスだと長いので、プロジェクトルートからの相対パスにする
      relative_path = file.sub(Rails.root.join.to_s, "")

      # Jaegerのタグ(Attributes)に追加
      span.set_attribute("code.filepath", relative_path)
      span.set_attribute("code.lineno", line)
    end
  end
end
# config/environments/development.rb

# 「ソースコードの場所(ファイル名と行番号)」を含める設定(任意)
config.active_record.query_log_tags = [
  :application,
  :controller,
  :action,
  :job,
  :source_location,
]

環境変数に OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 を設定してRailsサーバを再起動します。 実際にチューニング対象の機能にアクセスして、 http://localhost:16686 を開くと、リクエストのウォーターフォールチャートが美しく表示されます。

検索画面

ウォーターフォールチャート

Claude Code × MCP による分析

Jaegerの画面を見るだけでも「ここが遅い」というのは一目瞭然ですが、ここからさらに一歩踏み込んで、「どう直すべきか」をClaude Codeに考えさせます。

Jaegerの画面からトレースデータをJSON形式でダウンロードできるのですが、1リクエスト分の詳細データ(発行された全SQLを含む)は数千行に及ぶこともあり、非常に巨大です。 これをそのままチャット欄に貼り付けると、トークン制限に引っかかるか、Claude CodeがCompactしてしまい、重要な詳細情報が失われてしまいます。

ここで活用したのが、Serena MCPです。元々、Serena MCPは使っていたので、Serena MCPのメモリ機能を活用することにしました。

挙動を変えない「安全な」リファクタリング提案

単に「速くして」と頼むと、AIはロジックを破壊しかねない大胆な変更を提案することがあります。 そこで、Claude Codeに対し、以下の制約とフォーマットで分析を依頼しました。

【プロンプトのポイント】

  1. 制約事項: 挙動を変えないリファクタリングのみに限定する。
  2. 出力形式: 分析結果をチャットに流すのではなく、外部ファイルにMarkdown形式で出力させる。
  3. 評価指標: 各改善案に対し、「効果の高さ」と「修正難易度」を付け加える。

あとはひたすらClaude Codeにコード修正をしてもらいながら実装とレビューを繰り返していきました。 修正したら、Jaegerで再度トレースを取得し、改善効果を確認、次の改善案をClaude Codeに出してもらう… というサイクルを回しました。

まとめ

APIモードの開発におけるパフォーマンスチューニング環境として、Jaeger + Claude Codeの組み合わせは非常に強力でした。 Jaegerは人間にも見やすいUIとAIにも解析しやすい構造化データを提供してくれるのでAIとの相性が良いものポイントです。 Opus 4.5になってから格段に性能が良くなったので今回のような大量のデータを分析するような用途でも粘り強く対応してくれました。

Jaeger + Claude Codeはパフォーマンスチューニングの方法に悩んでいる方の選択肢の一つになれば幸いです。

We are hiring!

kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。

kickflow.com

サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談・ご応募お待ちしています!

careers.kickflow.co.jp