Go のサービス向け
2行で、クエリに呼び出し元がつく。
ここでの導入は明示的です — ミドルウェア、ドライバのラップ、そして測る価値のある関数の先頭に2行。Go にはフックできるビルド段階がなく、渡してもらわない限り呼び出し側のコンテキストに手が届かないからです。その2行で手に入るのは、他のすべてが前提にしているものです。上に関数のついたクエリ — つまり、N+1 が着地できる場所です。
何を測るか
- あなたが印を付けたすべての関数
- ctx, span := sixty.Start(ctx, "orders.List") と defer した capture。2行、そしてコンテキストを引き回す必要があります — Go に暗黙のコンテキストがないことの代償であり、届いた数値が信頼できる理由でもあります。
- database/sql のどのドライバでも、すべてのクエリ
- エージェントは接続ではなくドライバをラップします。だから pgx、lib/pq、MySQL、その他 database/sql に登録されたものはすべて測れます — そして本物のドライバが実装している任意インターフェースはすべて写し取るので、できていたことが動かなくなることはありません。
- 行数を、読まれるのに合わせて数える
- 返されたスライスからではありません。database/sql にはそれがないからです。スパンは rows が閉じたときに閉じます。それが、この数を推測ではなく実測にしています。
- どの関数がどのクエリを出したか
- コンテキストが現在のスパンを運ぶので、クエリはそれを出した関数に帰属します。これが「エンドポイントが遅くなった」を「この関数が20回呼ぶようになった」に変えるものです。
- あなたの HTTP ルート、受けと出し
- sixty.Middleware は net/http を話すものなら何でも包みます — chi、gorilla、echo、Go 1.22 のパターン mux。パスにはないパターンをルータが知っている場合は、sixty.SetRoute が名前を付けます。
何を見つけるか
どれも1つ前のリリースと比較されます。だから検出結果は、日付ではなく変更を名指しします。
前のリリースにはなかった N+1
1リクエストにつき1回だったデータベース呼び出しが、いまは20回。デプロイに紐づいているので、検出結果は時刻ではなくリリースを名指しします。
テーブル全体を読み始めたクエリ
1回あたりの行数が30から30,000へ — しかもここでは、呼び出し側が反復するのに合わせて数えているので、文が返しえた数ではなく、あなたのコードが実際に読んだ数です。
自分のコードが遅くなっている
関数自身の中の時間を、呼び出した先の時間と分けてあるので、1回目で正しいファイルを開けます。
いまはループの中で呼ばれているもの
1回あたりの速さも同じ、データも同じ、それでも以前は1回だった呼び出しが数百回に。
静かに何も返さなくなったクエリ
エラーもなく、遅い呼び出しもなく、以前は行があったのに空。Go のサービスの中で、これを失敗として報告するものは何もありません。何も失敗していないからです。
エラーを、出どころでまとめて
メッセージではなくコード上の位置で — 1つのバグは、いくつの値を埋め込もうと1件です。
フィードに何が届くか
読むためのダッシュボードではありません。検出1件につきカード1枚。数値と、それを変えたリリースと、コーディングエージェントが修正を書くのに必要な証拠が付いています。
これは apps/demo-go がわざと出荷している2つの回帰で、その seed コマンドが作るデータ — およそ1,000ユーザーと30,000件の注文 — が相手です。実際に走らせて、届くところを見られます。
orders.GetUserOrdersリファクタリングで WHERE 句が落ち、呼び出しのたびにテーブル全体を取ってきて Go で絞り込んでいます。温まったデータベースではレイテンシはほとんど動きませんでした — 気づかせるのは行数で、しかもそれは行が読まれるのに合わせて数えているので実測です。
リリース v2 以降、v1 との比較
orders.EnrichOrdersまとめて引く1本のクエリが、注文ごとの取得になりました。どれも速くインデックスも効いているので、レイテンシのグラフでは何も動きません — 動くのは回数だけです。
リリース v2 以降、v1 との比較
導入方法
すでに開いているコーディングエージェントに渡してください。main() とハンドラと sql.Open を見つけ、コンテキストを引き回すべきところに引き回し、測る価値のある関数に印を付けます — 説明は簡単で、手作業だと退屈な、まさにその工程です。
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:
go get github.com/sixty-sh/sixty-go
// main(), before the server starts
defer sixty.Init(sixty.Config{}).Shutdown(context.Background())
// requests — anything that speaks net/http
http.ListenAndServe(":8080", sixty.Middleware(mux))
// queries — the driver you already use
db, err := sixty.Open("pgx", dsn)
// functions — the ones worth measuring
func (s *Store) GetUserOrders(ctx context.Context, id int64) (_ []Order, err error) {
ctx, span := sixty.Start(ctx, "orders.GetUserOrders")
defer span.Capture(&err)
...
}リリース識別子は Vercel、Render、Railway、Fly、Heroku、GitHub Actions から自動で拾われ、それ以外の場所では SIXTY_RELEASE で指定します。最初の比較は次のデプロイのあとに届きます。1つのリリースには比べる相手がないからです。
できないこと
- コンテキストは引き回す必要があります。ctx を渡されなかった goroutine は、その操作の外側で仕事をします — バックグラウンド処理としては正しく、測るつもりだったファンアウトとしては誤りです。暗黙のコンテキスト版はありませんし、作るつもりもありません。推測する版とは、あるリクエストのクエリを別のリクエストに帰属させる版だからです。
- クエリプランは取得しません。それは症状ではなく原因を名指しする検出 —「これはインデックスを使わなくなった」— であり、Go のサービスでは届きません。
- CPU と待ち時間は分けません。goroutine はスレッド間を移動するので、スレッド単位の時計はスパンを説明できません。報告すれば、それは肝心なときにちょうど間違っている数値になります。
- 検出器が深く理解しているのは Postgres です。database/sql に登録された他のデータベースでも、時間・行数・回数は報告されます。文レベルの結論は弱くなります。
- ここではあなたの関数に印を付けてくれるものはありません。その工程を飛ばすと、フィードにはルートとクエリだけが残り、その間に何もなくなります。そこが、残りを読む価値のあるものにしている半分です。
これが理由でここに来たのなら
最後の変更が何をしたのか、確かめてください。
試してみる別のものを作っていますか? Node のサービス、Python のサービス、Ruby と Rails のアプリ のページもあります — 見えるものが違うので、内容も違います。