sixty

サーバーエージェント

PHP

バックグラウンドスレッドなしで動かなければならない唯一のエージェントです。PHP のワーカーはタイマーを走らせられず、学んだことはリクエストとともに死ぬので、ウィンドウは共有メモリにプールして統合します — 無損失で。でなければパーセンタイルは作り話になります。

パッケージ
sixty-sh/sixty / Packagist
動作環境
PHP 8.1 以降。Laravel 10〜12、Symfony 6.4/7、Doctrine DBAL 3 と 4。
ソース
sixty-sh/sixty-php

導入方法

Laravel and Symfony install themselves; anything on PDO or MongoDB takes two lines. Postgres, MySQL and MongoDB, with query plans on Postgres.

導入手順は、あなた向けのチェックリストではなく、すでに開いているコーディングエージェント向けのプロンプトとして書かれています。これは意図的です。どのファイルを編集するかではなく、導入が終わった時点で何が成り立っていなければならないかを述べています。コードをどこに置くかはフレームワーク次第で、置き場所を間違えると静かに失敗するからです。エージェントはリポジトリを読んでそれを判断できますが、ドキュメントの段落にはできません。

同じ文面は、 install_sixty が MCP サーバー 経由で返すものであり、コレクタが次の場所で配信しているものでもあります: /v1/setup?kind=php. 実体は1つだけです。

PHP の導入手順(全文)
Install the sixty agent in this PHP application so its controllers, queries
and the code between them report to sixty.

1. Add the package to the main require block of composer.json — not to
   require-dev. It runs in production; that is the entire point. Its only
   requirements are ext-json and ext-curl.

      composer require sixty-sh/sixty

2. Wire it up, which depends on the framework:

   a. Laravel — nothing to do. The service provider is discovered, and it
      registers the request span, the query instrumentation and the flush.
      Do NOT publish a config file or call Sixty::init() from a provider;
      that is a second boot path for the same thing.

   b. Symfony — add the bundle to config/bundles.php. It does the same three
      things, including the Doctrine DBAL middleware.

   c. Anything else — call \Sixty\Sixty::init() once at startup, then
      \Sixty\Instrument\Pdo::instrument($pdo) on the connection the
      application already has.

3. Measure the layer between the controller and the database. PHP has no
   build step to rewrite functions and no hook that fires when a method is
   defined, so this step is a line of code and it is the step the install
   exists for — without it the feed has routes and SQL and nothing in
   between, and an N+1 can be seen but not attributed to what causes it.

      return \Sixty\Sixty::trace('OrdersQuery#forUser', fn () => ...);

   The name is an identity compared across releases, so it must not contain
   anything that varies per call. An id in a name mints an operation per id.

4. Set these environment variables wherever the application is deployed:
      SIXTY_API_KEY  = a secret key starting sixty_sk_ — ask me for it. Do not
                       invent one, and do not commit it.
      SIXTY_SERVICE  = my-app
      SIXTY_ENDPOINT = https://ingest.sixty.sh

   The release identifier is picked up automatically on Vercel, Render,
   Railway, Fly, Heroku and GitHub Actions. If this deploys some other way,
   set SIXTY_RELEASE to the commit SHA — without one, every measurement lands
   in a single nameless bucket and no comparison can ever be made.

Constraints — correctness requirements, not style preferences:

- Do NOT change any application behaviour. This is instrumentation only: no
  refactors, no reordering of business logic, no "while I was in here" fixes.
- Do NOT write a PDO subclass or replace the application's connection. A PDO
  subclass has to open its own connection, which doubles every deployment's
  connection count silently and only fails under load. The agent sets
  PDO::ATTR_STATEMENT_CLASS on the connection that already exists; use that,
  or TracedPdo for a connection you construct yourself.
- Do NOT add any analytics, user id, session id, or cookie to what is
  reported. The agent is deliberately anonymous and must stay that way.
- If this application runs under Swoole coroutines, STOP and tell me. The
  agent refuses to enable there on purpose — requests share a worker and
  switch at every I/O boundary, so spans would be credited to the wrong
  request — and that refusal must not be worked around. FPM, CLI, RoadRunner
  and FrankenPHP are process-per-request and fully supported.

If ext-apcu is not installed, note that: with it, one request per interval
merges every worker's window and sends one payload, and without it each
request sends its own. Both are correct; the first is far less traffic.

When you are done, tell me which files you changed, so I can confirm data is
arriving.

秘密鍵が必要です — sixty_sk_ で始まり、サーバー側に留まります。ログイン後、設定ページで発行してください。

何を測るか

シグナル単位意味
rowsrows per callthis query returns more rows than it used to
fanoutqueries per callthis operation now issues more database calls per invocation — an N+1
latencyms per callthis operation takes longer end to end than it used to
self_latencyms per callthe time spent in this function itself got longer — its children did not
payloadbytes per callthe serialized result of this operation got bigger
errorserror ratea larger fraction of calls are throwing
runawaycalls per minutethis operation is being called far more often than anything triggers it
repeated_querytimes per requestthe identical query runs several times within one request
overfetchrows per callfar more rows are fetched than the code appears to use
unboundedrows per callthis query has no upper bound on what it can return
recursionlevels deepthis operation calls itself, deeper than it should
new_erroroccurrencesan error that did not occur in the previous release
missing_tenancy—This reads a table of per-person data without saying whose rows it wants. Unless your database is filtering it for you, everyone gets everyone else's.
collapse—This is handing back roughly half the data it used to, or less. If that was not deliberate, something is filtering out rows that somebody expects to see.
vanished—It was being used steadily until this release and has not been used once since. Usually the link, button, or redirect that led here stopped working.
traffic_drop—This is still being used, but a fraction as often, and its share of your traffic fell too — so it is not just a quiet period.
round_tripsround trips per readone read now waits on the database many times instead of once
plan—the database chose a different plan for this query

どこに入り込むか

  • Laravel — 自動。サービスプロバイダが検出され、リクエストのスパン、クエリ、送信を配線します。
  • Symfony — config/bundles.php にバンドルを追加。同じ3つで、Doctrine DBAL のミドルウェアも含みます。
  • それ以外 — Sixty::init() のあと、アプリがすでに持っている接続に対して Pdo::instrument($pdo)。
  • あなた自身のコード — Sixty::trace('OrdersQuery#forUser', fn () => …)。PHP には関数を書き換えるビルド段階も、メソッド定義時に発火するフックもないので、これはデコレータではなく1行です。

データベース

  • PDO — 接続を置き換えずに計測します。PDO のサブクラスなら自前の接続を開くことになり、あらゆるデプロイの接続数を黙って倍にします。statement クラスは、すでに存在する接続の上に設定します。
  • Laravel、どのドライバでも — ConnectionEstablished で拾います。行数は rowCount() から。
  • Doctrine DBAL — バンドルが登録するミドルウェア。行数は結果自身の count から。
  • MongoDB — Doctrine ODM も含め、ドライバのコマンド監視を通して。identity はキーだけから作るので、値がそこへ入る経路はありません。

これだけができること

  • レスポンスのあとに走る送信 — まず fastcgi_finish_request()、そのあとに統合と POST。コレクタが落ちていてタイムアウトいっぱいまでかかっても、読んでいる人にはコストがありません。ページはもう手元にあるからです。
  • ワーカーをまたぐ無損失の統合 — ext-apcu があれば各リクエストが自分のウィンドウを書き、1間隔につき1つのリクエストがそれらをまとめて単一のペイロードにします。スケッチは厳密に合併するので、パーセンタイルは「すべてを測る単一プロセスが報告したはずの値」になります。

できないこと

  • Swoole のコルーチン下では有効化を拒み、その理由を言います。そこでは多数のリクエストが1つのワーカーを共有し、I/O のたびに切り替わるので、現在のスパンがあるリクエストのクエリを別のリクエストのコントローラに帰属させてしまいます。FPM、CLI、RoadRunner、FrankenPHP はリクエストごとに1プロセスで、完全に対応しています。
  • ext-apcu がないと各リクエストが自分のウィンドウを送ります。それでも動きますし、エージェントは起動時に一度そう伝えます。ごまかしはしません — 単に通信量がかなり増えます。
  • PDO::query() と PDO::exec() は statement クラスに届かないので、自分で作った接続に対してそれらを呼ぶ場合は TracedPdo が必要です。prepare() + execute() — Laravel と Doctrine が出すすべてのクエリ — は対象です。
  • 他の何かがすでに statement クラスを持っている場合 — プロファイラやデバッグバー — エージェントはそれに触れず、壊す代わりに何も測りません。
  • CPU と待ち時間は分けません。

設定

どのエージェントも同じ4つの変数を読みます。そして SIXTY_* が答えるところでは DRIFT_* も引き続き答えます — 製品名は変わりましたが、その名前は他人のデプロイから引き上げてよい類のものではありません。

SIXTY_API_KEYこれがないとエージェントは何もせず、そのことを伝えます。推測もせず、未知のエンドポイントに再試行もせず、例外も投げません。
SIXTY_SERVICEこのサービスを何と呼ぶか。読み取れる場合はプロジェクト名が既定になります。
SIXTY_RELEASEいちばん重要なもの。Vercel、Render、Railway、Fly、Heroku、GitHub Actions では自動で拾われます。それ以外ではコミットの SHA を設定してください。これがないとすべての計測が名前のない1つのバケツに入り、比較は永遠にできません。
SIXTY_ENDPOINTどこへ報告するか。既定は http://localhost:4319で、ノートPCの上では正しく、そのアプリが他人に配信された瞬間に間違いになります。

残り — 送信間隔、サンプリング率、何を計測するか — はパッケージ自身の README にあります。エージェントが変わっても正しいままでいられる場所だからです。

sixty の PHP エージェント — 何を測り、どう導入するか