sixty

Ruby と Rails のアプリ向け

手順2はありません。

Rails には文書化された起動フックがあり、すでにすべてのクエリを知らせるイベントバスもあります。だからこのエージェントには initializer もラッパーも要りません。gem を足し、環境変数を2つ設定すれば、コントローラは操作になり、そのクエリが結び付けられます。以下のすべては、あなたが計装のコードを1行も書かずに起きます。

何を測るか

すべてのコントローラアクション
controller#action の形で名前が付くので、フィードはあなたのルートファイルと同じ読み心地になります。railtie が起動時に包みます — include するものはなく、次のコントローラで思い出すべきこともありません。
ActiveRecord が出すすべてのクエリ
Rails がすでに publish している sql.active_record を購読します。行数、正規化された文、レスポンスサイズ、そしてどのアクションが出したか。
3つのデータベースを、それぞれの流儀で
pg は EXPLAIN によるクエリプラン付き。mysql2 と trilogy は Postgres ではなく MySQL の引用符の規則で読みます。二重引用符の文字列が、一方ではリテラルで、もう一方では識別子だからです。そして Mongoid が使う mongo は、コマンドの形を identity として扱います。
データベースが答えるために何をしたか
Postgres には各文のプランを尋ねます — 汎用プランの EXPLAIN を、文ごとにキャッシュし、呼び出し側のスパンの外で発行するので、その人の仕事として測られることはありません。しかもパラメータを一度も束縛しないので、あなたの値は一切関わりません。
自分のクラスも、必要なら
include Sixty::Instrumented は、公開インスタンスメソッドをすべて操作にし、private メソッドとアクセサには手を触れません。あるいは他人のクラスの1メソッドだけなら Sixty.instrument(Stripe::Charge, :create) で。

何を見つけるか

どれも1つ前のリリースと比較されます。だから検出結果は、日付ではなく変更を名指しします。

インデックスを使わなくなったクエリ

ここで唯一、症状ではなく原因を名指しする検出です。まだ何も遅くないうちに届くことがよくあります。テーブルがまだ小さくてスキャンでも問題ない段階で、そして数週間後、小さくなくなったときに障害になります。

前のリリースにはなかった N+1

Rails が最もよく責められる欠陥を、時計ではなく回数で捕まえます。1アクションあたり1本だったクエリが20本になり、どれも速く、includes は3週間前のリファクタリングで落ちていました。

テーブル全体を読み始めたクエリ

1回あたりの行数が30から30,000へ — where が Ruby 側に移ったスコープです。同じ答えを返し、読んでみるとまったく自然です。

自分のコードが遅くなっている

呼び出した先ではなくメソッド自身の中の時間なので、「遅くなったのは自分のコードか、呼んだ先か」が午後ひとつではなく数値ひとつになります。

静かに何も返さなくなったクエリ

成功し、例外も出さず、以前は行が返っていたのに空で返ってきた。ActiveRecord はまったく満足しています。ページのほうが空です。

いまはループの中で呼ばれているもの

1回あたりは変わらず、以前は1回だった呼び出しが数百回に — 独立したプロセスとして報告されるバックグラウンドジョブ経由のものも含めて。

フィードに何が届くか

読むためのダッシュボードではありません。検出1件につきカード1枚。数値と、それを変えたリリースと、コーディングエージェントが修正を書くのに必要な証拠が付いています。

これは apps/demo-rails がわざと出荷している2つの回帰で、bin/rails db:prepare が入れるデータ — およそ30,000件の注文 — が相手です。実際に走らせて、届くところを見られます。

行数OrdersQuery#for_user
30行30,000行1000×

誰かがスコープをリファクタリングし、where が Ruby 側に移りました。だから呼び出しのたびにテーブル全体を読み込み、メモリ上で絞り込んでいます。返す答えは同じで、ノートPCなら数ミリ秒の話です。

リリース v2 以降、v1 との比較

N+1OrdersQuery#enrich
1本20本20×

まとめて引いていたものが、注文ごとの取得になりました。どのクエリも単体では速く、正しくインデックスも効いています。それが、この種のバグにコードレビューとステージングを通り抜けさせます。

リリース v2 以降、v1 との比較

導入方法

Rails ではこれが導入のすべてです。gem と環境変数2つ。railtie が起動時にミドルウェアを追加し、クエリのバスを購読し、コントローラのアクションを包むので、書くべき initializer も、呼ぶべきものもありません。

claude mcp add sixty \
  -e SIXTY_API_KEY=sixty_sk_… \
  -e SIXTY_ENDPOINT=https://ingest.sixty.sh \
  -- npx -y @sixty-sh/mcp

# or by hand:
gem 'sixty'                      # Gemfile

export SIXTY_API_KEY=sixty_sk_…
export SIXTY_SERVICE=checkout-api

# On Rails, that is the install. Outside it:
require 'sixty'
Sixty.init
use Sixty::Instrument::Rack      # config.ru — Sinatra, Hanami, a bare app

# Your own classes, if you want them measured:
class OrdersQuery
  include Sixty::Instrumented
end

リリース識別子は Vercel、Render、Railway、Fly、Heroku、GitHub Actions から自動で拾われ、それ以外の場所では SIXTY_RELEASE で指定します。最初の比較は次のデプロイのあとに届きます。1つのリリースには比べる相手がないからです。

できないこと

  • クエリプランは Postgres のみです。MySQL には汎用プランの EXPLAIN がなく、MongoDB には説明すべき文がありません — そしてどちらかを説明するということは、誰かのクエリの値を保持し、私たちが組み立てたコマンドでその人自身のデータベースへ送り返すということです。このエージェントはそれをしません。
  • CPU と待ち時間は分けません。それにはスレッド単位の時計と、自分のスレッドを占有するスパンが要り、それを持っているのは Python のエージェントだけです。
  • ワーカー付きの Puma や Sidekiq の下では、各プロセスが個別に報告し、コレクタがまとめます。これは正しい動作ですが、プロセス単位の数値を読むときには知っておく価値があります。
  • include Sixty::Instrumented が測るのは公開インスタンスメソッドです。実際の仕事をしている private メソッドは、別の方法で操作にするまで見えません。何を測る価値があるかを推測しないことの代償です。

これが理由でここに来たのなら

最後の変更が何をしたのか、確かめてください。

試してみる

別のものを作っていますか? Node のサービス、PHP と Laravel のアプリ、Python のサービス のページもあります — 見えるものが違うので、内容も違います。

Rails 向けパフォーマンス監視 — Postgres・MySQL・MongoDB — Sixty