sixty

服务端 agent

PHP

唯一一个必须在没有后台线程的情况下工作的 agent。PHP worker 跑不了定时器,而它学到的一切都会随这次请求消失,所以窗口在共享内存里汇集并合并 —— 而且必须无损,否则百分位数就是编的。

包
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. 它只有一份。

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 — 自动。service provider 会被发现,并接好请求的 span、查询和发送。
  • Symfony — 把 bundle 加进 config/bundles.php。同样这三件事,包括 Doctrine DBAL 的中间件。
  • 其他任何情况 — Sixty::init(),然后对应用已经有的那个连接调用 Pdo::instrument($pdo)。
  • 你自己的代码 — Sixty::trace('OrdersQuery#forUser', fn () => …)。PHP 既没有能重写函数的构建步骤,也没有在方法被定义时触发的钩子,所以这是一行,而不是一个装饰器。

数据库

  • PDO — 在不替换你连接的前提下埋点:一个 PDO 子类就得自己再开一个连接,等于悄悄把每次部署的连接数翻倍。statement 类是设置在那个已经存在的连接上的。
  • Laravel,任意驱动 — 在 ConnectionEstablished 时接住。行数取自 rowCount()。
  • Doctrine DBAL — 由 bundle 注册的一个中间件。行数取自结果自己的 count。
  • MongoDB — 包括 Doctrine ODM,通过驱动的命令监控。identity 只用键构造,所以任何值都没有路径进得去。

只有它才做的事

  • 在响应之后才跑的发送 — 先 fastcgi_finish_request(),然后才是合并和 POST。一个挂掉的收集端就算把超时耗满,对读页面的人也没有任何代价,因为页面早就到手了。
  • 跨 worker 的无损合并 — 有了 ext-apcu,每个请求写自己的窗口,每个间隔由其中一个请求把它们合并成一份负载。sketch 是精确合并的,所以百分位数就是一个测量一切的单进程会报出来的值。

它做不到什么

  • 在 Swoole 协程下它拒绝启用,并会说明原因。在那里很多请求共用一个 worker,并在每个 I/O 边界处切换,于是当前的 span 会把一个请求的查询算到另一个请求的控制器头上。FPM、CLI、RoadRunner 和 FrankenPHP 是每请求一个进程,完全支持。
  • 没有 ext-apcu 时,每个请求都会发送自己的窗口。它照样能用,agent 也会在启动时告诉你一次,而不是假装没事 —— 只是流量会大不少。
  • PDO::query() 和 PDO::exec() 永远到不了 statement 类,所以一个你自己构造、又在上面调用它们的连接需要用 TracedPdo。prepare() + execute() —— Laravel 和 Doctrine 发出的每一条查询 —— 都覆盖到了。
  • 如果已经有别的东西占着 statement 类 —— 一个 profiler、一条调试栏 —— agent 会放手不管,宁可什么都不测也不把它弄坏。
  • CPU 和等待没有分开。

配置

每个 agent 都读同样四个变量,而且凡是 SIXTY_* 能用的地方 DRIFT_* 依然有效 —— 产品改过名,但那个名字不是我们说撤就能从别人的部署里撤掉的。

SIXTY_API_KEY没有它,agent 就保持沉默不动,并且会说出来。它从不猜测,从不对着一个未知端点重试,也从不抛异常。
SIXTY_SERVICE这个服务叫什么。在能读出项目名的地方,默认用项目名。
SIXTY_RELEASE最重要的一个。在 Vercel、Render、Railway、Fly、Heroku 和 GitHub Actions 上会自动取到;其他地方请把它设成 commit 的 SHA。没有它,所有测量都会落进同一个没有名字的桶里,任何比较都无从谈起。
SIXTY_ENDPOINT往哪里上报。默认是 http://localhost:4319,这在笔记本上是对的,而在应用被交付给别人的那一刻就是错的。

其余的 —— 发送间隔、采样率、要给什么埋点 —— 都在这个包自己的 README 里,因为那里才是它能随着 agent 变化而保持正确的地方。

sixty 的 PHP agent —— 它测量什么、怎么安装