sixty

Server-Agent

PHP

Der einzige Agent, der ohne Hintergrund-Thread auskommen muss. Ein PHP-Worker kann keinen Timer laufen lassen, und alles, was er gelernt hat, stirbt mit der Anfrage — also werden Fenster im Shared Memory gepoolt und zusammengeführt, verlustfrei, sonst wären die Perzentile Fiktion.

Paket
sixty-sh/sixty auf Packagist
läuft auf
PHP 8.1 oder neuer. Laravel 10–12, Symfony 6.4/7, Doctrine DBAL 3 und 4.
Quellcode
sixty-sh/sixty-php

Installation

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

Die Installation ist als Prompt für den Coding-Agenten geschrieben, den du ohnehin offen hast, nicht als Checkliste für dich. Das ist Absicht: sie benennt, was am Ende wahr sein muss, statt welche Dateien zu bearbeiten sind — denn wohin der Code gehört, hängt vom Framework ab, und ihn an die falsche Stelle zu setzen scheitert lautlos. Ein Agent kann dein Repository lesen und das herausfinden; ein Absatz auf einer Dokumentationsseite kann es nicht.

Denselben Text liefert install_sixty über den MCP-Server zurück, und den serviert der Collector unter /v1/setup?kind=php. Es gibt genau eine Kopie davon.

die vollständige PHP-Installation
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.

Er braucht einen geheimen Schlüssel — er beginnt mit sixty_sk_ und bleibt serverseitig. Erzeuge einen auf der Einstellungsseite, sobald du angemeldet bist.

Was er misst

SignalEinheitwas es bedeutet
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

Wo er sich einhängt

  • Laravel — Automatisch. Der Service Provider wird entdeckt und verdrahtet den Anfrage-Span, die Abfragen und den Flush.
  • Symfony — Trage das Bundle in config/bundles.php ein. Dieselben drei Dinge, einschließlich der Doctrine-DBAL-Middleware.
  • Alles andere — Sixty::init(), dann Pdo::instrument($pdo) auf der Verbindung, die die Anwendung ohnehin hat.
  • Dein eigener Code — Sixty::trace('OrdersQuery#forUser', fn () => …). PHP hat keinen Build-Schritt, der Funktionen umschreibt, und keinen Hook, der beim Definieren einer Methode feuert — deshalb ist das eine Zeile und kein Dekorator.

Datenbanken

  • PDO — Instrumentiert, ohne deine Verbindung zu ersetzen: eine PDO-Unterklasse müsste ihre eigene öffnen und damit still die Verbindungszahl jedes Deployments verdoppeln. Die Statement-Klasse wird auf der bereits bestehenden Verbindung gesetzt.
  • Laravel, jeder Treiber — Aufgegriffen bei ConnectionEstablished. Zeilen aus rowCount().
  • Doctrine DBAL — Eine vom Bundle registrierte Middleware. Zeilen aus dem count des Ergebnisses selbst.
  • MongoDB — Einschließlich Doctrine ODM, über das Command Monitoring des Treibers. Die Identität wird nur aus Schlüsseln gebaut, kein Wert hat also einen Weg hinein.

Was nur dieser kann

  • Ein Flush nach der Antwort — Zuerst fastcgi_finish_request(), dann die Zusammenführung und der POST. Ein Collector, der ausgefallen ist und ins volle Timeout läuft, kostet den Leser nichts, weil er seine Seite bereits hat.
  • Verlustfreie Zusammenführung über Worker hinweg — Mit ext-apcu schreibt jede Anfrage ihr eigenes Fenster, und eine Anfrage pro Intervall führt sie alle zu einer einzigen Nutzlast zusammen. Sketches vereinigen sich exakt, die Perzentile sind also das, was ein einzelner Prozess gemeldet hätte, der alles misst.

Was er nicht kann

  • Unter Swoole-Coroutinen weigert er sich zu starten, und sagt warum. Dort teilen sich viele Anfragen einen Worker und wechseln an jeder I/O-Grenze, der aktuelle Span würde also die Abfragen einer Anfrage dem Controller einer anderen zuschreiben. FPM, CLI, RoadRunner und FrankenPHP sind ein Prozess pro Anfrage und werden vollständig unterstützt.
  • Ohne ext-apcu sendet jede Anfrage ihr eigenes Fenster. Es funktioniert weiterhin, und der Agent sagt das einmal beim Start, statt etwas anderes vorzugeben — es ist nur erheblich mehr Verkehr.
  • PDO::query() und PDO::exec() erreichen nie eine Statement-Klasse, eine Verbindung, die du selbst baust und auf der du diese aufrufst, braucht also TracedPdo. prepare() + execute() — jede Abfrage, die Laravel und Doctrine absetzen — ist abgedeckt.
  • Wenn etwas anderes die Statement-Klasse bereits besitzt — ein Profiler, eine Debug-Bar — lässt der Agent sie in Ruhe und misst nichts, statt sie kaputtzumachen.
  • CPU und Warten werden nicht getrennt.

Konfiguration

Jeder Agent liest dieselben vier Variablen, und DRIFT_* antwortet weiterhin überall dort, wo SIXTY_* es tut — das Produkt wurde umbenannt, und dieser Name ist nicht unserer, um ihn aus fremden Deployments zu entfernen.

SIXTY_API_KEYOhne sie bleibt der Agent untätig und sagt das auch. Er rät nie, versucht es nie erneut gegen einen unbekannten Endpunkt, und wirft nie.
SIXTY_SERVICEWie dieser Dienst heißen soll. Standardmäßig der Projektname, wo einer lesbar ist.
SIXTY_RELEASEDie wichtigste. Wird auf Vercel, Render, Railway, Fly, Heroku und GitHub Actions automatisch abgeholt; überall sonst setze sie auf den Commit-SHA. Ohne sie landet jede Messung in einem einzigen namenlosen Eimer, und kein Vergleich ist je möglich.
SIXTY_ENDPOINTWohin gemeldet wird. Standardmäßig http://localhost:4319, was auf einem Laptop richtig ist und in dem Moment falsch, in dem die Anwendung jemand anderem ausgeliefert wird.

Der Rest — Flush-Intervall, Sample-Rate, was instrumentiert wird — steht im README des Pakets selbst, wo es wahr bleiben kann, während sich der Agent verändert.

Der PHP-Agent von sixty — was er misst und wie man ihn installiert