naopoyo.com
  • Docs
  • Tags
  • Bookmarks
  • Tools
  • About
  • Docs
  • Tags
  • Bookmarks
  • Tools
  • About

目次

© naopoyo

目次

最近更新された記事

🛬

Next.js 16 + next-intl で多言語化対応するための設定と注意点

11分前·2026年02月11日
  • Next.js
  • TypeScript
🔍

Claude Code の Explore エージェントについて

4日前·2026年02月07日
  • Claude Code
🚣

Tailwind CSS v4 で max-h-[300px] を max-h-75 に自動修正するための設定

5日前·2026年02月07日
  • Tailwind
  • ESLint
🍶

API-only Rails + GraphQL の rack-mini-profiler セットアップ

公開日約5時間前2026-02-11
履歴GitHubで見る
MarkdownRaw Content
  • Rails
  • GraphQL
  • Next.js

まとめ

  • API-only では auto_inject = false とスナップショット経由の確認が前提になる
  • pre_authorize_cb で UI アクセスとスナップショット記録を両立させる
  • graphql-ruby の trace_with で operation 名ごとにプロファイルを分類できる

背景

Rails の API-only アプリケーションで GraphQL を使っていると、パフォーマンスの可視化が難しい。rack-mini-profiler は定番のプロファイリングツールだけど、2つの前提が噛み合わない。

1つ目は、API-only アプリには HTML レスポンスがないこと。rack-mini-profiler はデフォルトで HTML にプロファイル UI を注入するため、JSON しか返さない API では意味がない。

2つ目は、GraphQL が単一エンドポイントであること。REST なら URL ごとにプロファイルが分かれるが、GraphQL は全て POST /graphql に集約されてしまう。

この2つを解決する設定とコードを書いた。

導入

Gemfile の development グループに require: false で追加する。

Gemfile
group :development do
  gem "rack-mini-profiler", require: false
end

API-only 向けの設定

config/initializers/rack_mini_profiler.rb
if Rails.env.development?
  require 'rack-mini-profiler'

  Rack::MiniProfilerRails.initialize!(Rails.application)

  Rack::MiniProfiler.config.tap do |c|
    c.auto_inject = false
    c.storage = Rack::MiniProfiler::MemoryStore
    c.snapshot_every_n_requests = 1
    c.snapshots_redact_sql_queries = false
    c.pre_authorize_cb = ->(env) { env['PATH_INFO'].start_with?('/mini-profiler-resources/') }
  end
end
設定説明
auto_injectレスポンス HTML へのプロファイル UI 自動注入
storageプロファイルデータの保存先
snapshot_every_n_requestsスナップショットを記録するリクエスト間隔
snapshots_redact_sql_queriesスナップショット内の SQL クエリの墨消し
pre_authorize_cbリクエストごとの認可判定コールバック

pre_authorize_cb は少し補足が要る。rack-mini-profiler のスナップショットは「認可されていないリクエスト」に対してだけ記録される。pre_authorize_cb で mini-profiler UI へのリクエストだけ認可し、それ以外をスナップショットとして記録させている。

GraphQL トレースモジュール

graphql-ruby 2.x の Trace API を使って、rack-mini-profiler の step にクエリ情報を記録する。

rack_mini_profiler_trace.rb
module Tracers
  module MiniProfilerTrace
    STEP_KEY = :mini_profiler_dataloader_step

    def execute_query(query:)
      op_name = query.operation_name || 'anonymous'
      op_type = query.selected_operation&.operation_type || 'query'
      label = "GraphQL #{op_type}: #{op_name}"

      current = Rack::MiniProfiler.current
      if current
        current.page_struct[:name] = label
        current.page_struct[:request_path] = "/graphql [#{op_name}]"
      end

      Rack::MiniProfiler.step(label) { super }
    end

    def execute_query_lazy(query:, multiplex:)
      Rack::MiniProfiler.step('GraphQL lazy resolve') { super }
    end

    def begin_dataloader_source(source)
      step = Rack::MiniProfiler.start_step("Dataloader: #{source.class.name}")
      Fiber[STEP_KEY] = step
      super
    end

    def end_dataloader_source(source)
      step = Fiber[STEP_KEY]
      Rack::MiniProfiler.finish_step(step) if step
      Fiber[STEP_KEY] = nil
      super
    end
  end
end

operation 名でのグループ化

スナップショットのグループ名は rails_route_from_path で決まる。何もしなければ全て POST graphql#execute に集約されてしまう。

page_struct[:request_path] を /graphql [GetDocuments] のように書き換えると、rails_route_from_path がルート認識に失敗して nil を返し、フォールバックとして request_path がグループ名に使われる。スナップショット一覧では operation 名ごとにグループ化される。

page_struct[:name] は個別の結果ページのタイトルに表示される。設定しないとリクエスト URL がそのまま表示されるため、GraphQL query: GetDocuments のようなラベルを入れておくと見分けやすい。

Dataloader と Fiber

Dataloader のバッチロードは Fiber 上で実行されるため、step オブジェクトの保持に Fiber[](Fiber ローカルストレージ)を使っている。Thread ローカル変数だと Fiber をまたいで干渉してしまう。

フィールドレベルのトレーシングは Fiber 間の干渉が複雑になるため、あえてスキップした。SQL クエリは rack-mini-profiler が sql.active_record 通知を自動購読するので、別途対応は不要だった。

スキーマへの適用

BaseSchema に1行追加するだけで、継承先の全スキーマに適用される。

app/graphql/base_schema.rb
class BaseSchema < GraphQL::Schema
  trace_with Tracers::MiniProfilerTrace if defined?(Rack::MiniProfiler)
end

require: false にしているため、開発環境以外では Rack::MiniProfiler が定義されず、この条件は自然に false になる。

プロファイリングデータの確認方法

開発サーバー起動後、/mini-profiler-resources/snapshots にアクセスするとスナップショット一覧が表示される。個別のスナップショットをクリックすると、GraphQL の step や SQL クエリの詳細を確認できる。

おまけ: Next.js (urql) からプロファイル結果にアクセスする

API-only 構成ではフロントエンドとサーバーが別プロセスになるため、Next.js のサーバーサイドログからプロファイル結果に直接アクセスできると便利になる。

Rails 側: レスポンスヘッダーにプロファイル ID を設定

GraphQL コントローラーで authorize_request を呼び、プロファイル ID とグループ名をヘッダーに設定する。

app/controllers/graphql_controller.rb
def set_mini_profiler_header
  return unless defined?(Rack::MiniProfiler)

  current = Rack::MiniProfiler.current
  return unless current

  Rack::MiniProfiler.authorize_request

  page = current.page_struct
  request_path = page[:request_path] || '/graphql'
  response.set_header('MiniProfiler-Id', page[:id])
  response.set_header('MiniProfiler-Group', "POST #{request_path}")
end

authorize_request はミドルウェアに結果の保存を指示する。これがないと /results?id= でアクセスできない。スナップショットへの記録には影響しない。

Next.js 側: URQL のカスタム fetch でログ出力する場合

createClient の fetch オプションでレスポンスヘッダーを読み取り、結果ページの URL をログに出力する。

src/urql/urql-client.ts
const PROFILER_BASE_URL = 'http://localhost/mini-profiler-resources/results';

const fetchWithProfiler: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  const id = response.headers.get('MiniProfiler-Id');
  const group = response.headers.get('MiniProfiler-Group');
  if (id) {
    const operation = group?.match(/\[(.+)\]/)?.[1] ?? 'unknown';
    const params = new URLSearchParams({ id, ...(group && { group }) });
    console.log(`[MiniProfiler] ${operation} ${PROFILER_BASE_URL}?${params}`);
  }
  return response;
};

const makeClient = () => {
  return createClient({
    url: API_URL,
    fetch: process.env.NODE_ENV === 'development' ? fetchWithProfiler : undefined,
    // ...
  });
};

Next.js のサーバーログに以下のように出力される。

[MiniProfiler] operationName http://localhost/mini-profiler-resources/results?id=xxx&group=zzz

URL をブラウザで開くと、そのリクエストの step と SQL クエリの詳細を確認できる。