für PHP- und Laravel-Apps
Kein Hintergrund-Thread. Nichts verloren.
Ein PHP-Worker kann keinen Timer laufen lassen, und alles, was er gelernt hat, stirbt mit der Anfrage. Das ist das Problem, das dieser Agent lösen musste, bevor er überhaupt etwas messen konnte: Fenster werden im Shared Memory gepoolt und zusammengeführt — verlustfrei, sonst wären die Perzentile Fiktion — und der Flush passiert, nachdem die Antwort schon raus ist, ein Collector mit einem schlechten Tag kostet deine Leserin also nichts.
Was es misst
- Jede Anfrage, und die Methoden darunter
- Laravel und Symfony verdrahten den Anfrage-Span selbst. Sixty::trace('OrdersQuery#forUser', fn () => …) markiert den Code zwischen Controller und Datenbank — eine Zeile statt eines Dekorators, weil PHP keinen Build-Schritt hat, der Funktionen umschreibt, und keinen Hook, der feuert, wenn eine Methode definiert wird.
- Jede Abfrage, ohne deine Verbindung zu ersetzen
- PDO wird instrumentiert, indem die Statement-Klasse auf der bereits bestehenden Verbindung gesetzt wird. Eine PDO-Unterklasse müsste ihre eigene öffnen und würde damit still die Verbindungszahl jedes Deployments verdoppeln — genau die Art von Sache, die ein Monitoring-Werkzeug niemals tun darf.
- Laravel und Doctrine, auf jedem Treiber
- Laravel-Verbindungen werden bei ConnectionEstablished aufgegriffen, mit Zeilen aus rowCount(). Doctrine DBAL bekommt eine vom Bundle registrierte Middleware, mit Zeilen aus dem count des Ergebnisses selbst. Beide geben dir die Statement-Form und die Zuordnung.
- MongoDB, einschließlich Doctrine ODM
- Über das Command Monitoring des Treibers. Die Identität wird nur aus Schlüsseln gebaut, kein Wert von dir hat also einen Weg hinein — und Cursor-Roundtrips werden gezählt, das Signal, das einen Lesevorgang fängt, der in dreihundert Raten ankommt.
- Was die Datenbank getan hat, um zu antworten
- Ein EXPLAIN mit generischem Plan auf Postgres, pro Statement gecacht und außerhalb des Spans des Aufrufers abgesetzt, sodass „dieses Statement benutzt seinen Index nicht mehr“ ankommt, ohne dass je ein Parameter von dir gebunden wurde.
Was es findet
Jedes davon wird gegen das Release davor verglichen, der Befund benennt also die Änderung und nicht den Tag.
Eine Abfrage, die ihren Index nicht mehr benutzt
Der Befund, der eine Ursache statt eines Symptoms benennt. Er kommt oft an, während alles noch schnell ist, weil die Tabelle noch klein ist — und wird einige Wochen später zum Ausfall, wenn sie es nicht mehr ist.
Ein N+1, das im letzten Release nicht da war
Aus einer Abfrage pro Anfrage wurden zwanzig. Jede von ihnen schnell, jede von ihnen indiziert, und das Einzige, was sich geändert hat, ist die Anzahl.
Eine Abfrage, die angefangen hat, die ganze Tabelle zu lesen
Zeilen pro Aufruf sind von 30 auf 30.000 gegangen, meist ein where, das bei einem Refactoring aus der Abfrage heraus und nach PHP gewandert ist.
Ein Lesevorgang, aus dem hunderte Netzwerkwartezeiten wurden
MongoDB beantwortet einen Cursor in Batches. Wenn ein Ergebnis über einen Batch hinauswächst, werden aus einem Lesevorgang dutzende oder hunderte aufeinanderfolgende Roundtrips — identische Dokumente, und nichts bewegt sich außer der Uhr.
Dein eigener Code, der langsamer wird
Zeit in der Methode selbst statt in dem, was sie aufgerufen hat, getrennt, damit du beim ersten Versuch in die richtige Datei schaust.
Eine Abfrage, die still nichts zurückgibt
Erfolgreich, nichts geworfen, nicht langsam, und leer zurückgekommen, wo früher Zeilen kamen.
Was in deinem Verlauf landet
Kein Dashboard zum Lesen. Eine Karte pro Befund, mit den Zahlen, dem Release, das sie verändert hat, und den Belegen, die dein Coding-Agent braucht, um die Lösung zu schreiben.
Das sind die zwei Regressionen, die apps/demo-laravel absichtlich ausliefert, gegen den Datensatz, den sein Seeder anlegt — etwa 30.000 Bestellungen. Du kannst es laufen lassen und ihnen beim Ankommen zusehen.
OrdersQuery#forUserDie Einschränkung ist bei einem Refactoring aus der Abfrage heraus und nach PHP gewandert, jeder Aufruf lädt also die ganze Tabelle. Dieselbe Antwort, dasselbe Code-Review, und ein paar Millisekunden auf einer Entwicklungsdatenbank.
seit Release v2, gegen v1
OrdersQuery#enrichAus einer gebündelten Abfrage wurde eine pro Bestellung. Jede Abfrage ist schnell und indiziert, keine Anfrage sieht also falsch aus — die Anzahl ist das Einzige, was sich bewegt hat.
seit Release v2, gegen v1
Wie es installiert wird
Laravel entdeckt den Service Provider; bei Symfony muss das Bundle in config/bundles.php eingetragen werden. Beide verdrahten danach den Anfrage-Span, die Abfrage-Instrumentierung und den Flush — es gibt keinen Initializer zu schreiben und nichts aufzurufen.
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:
composer require sixty-sh/sixty
SIXTY_API_KEY=sixty_sk_…
SIXTY_SERVICE=checkout-api
# On Laravel, that is the install — the provider is discovered.
# On Symfony, add the bundle to config/bundles.php.
# Anything else:
\Sixty\Sixty::init();
\Sixty\Instrument\Pdo::instrument($pdo);
# Your own code:
\Sixty\Sixty::trace('OrdersQuery#forUser', fn () => ...);Installiere zusätzlich ext-apcu, und die Fenster jedes Workers werden zu einer Nutzlast pro Intervall zusammengeführt. Ohne es funktioniert der Agent weiterhin und sagt das einmal beim Start, statt etwas anderes vorzugeben — es ist schlicht erheblich mehr Verkehr. Der Release-Bezeichner kommt von allein von deiner Plattform, oder aus SIXTY_RELEASE.
Was es nicht tut
- 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 es dir einmal beim Start — aber die verlustfreie Zusammenführung über Worker hinweg ist das, was die Perzentile zu dem macht, was ein einzelner Prozess gemeldet hätte, der alles misst, und ohne sie zahlst du in 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. Das ist der richtige Tausch und er ist still, es lohnt sich also zu wissen, dass es passieren kann.
- CPU und Warten werden nicht getrennt, und Abfragepläne gibt es nur für Postgres. MySQL und MongoDB können nur eine Abfrage erklären, die ihre Werte noch enthält, und dieser Agent verwirft Werte in dem Moment, in dem er sie sieht.
Falls du wegen einer dieser Sachen hier bist
Finde heraus, was deine letzte Änderung angerichtet hat.
Probier es aus!Du baust etwas anderes? Sieh dir Node-Dienste, Ruby- und Rails-Apps oder Python-Dienste an — diese Seiten sind anders, und was wir sehen können, auch.