sixty

für Go-Dienste

Zwei Zeilen, und die Abfrage hat einen Aufrufer.

Die Installation ist hier ausdrücklich — eine Middleware, ein Treiber-Wrapper und zwei Zeilen am Anfang der Funktionen, die es wert sind, gemessen zu werden —, weil Go keinen Build-Schritt zum Einhaken hat und keine Möglichkeit, den Kontext des Aufrufers zu erreichen, ohne ihn übergeben zu bekommen. Was du für diese zwei Zeilen bekommst, ist das, worauf der Rest aufbaut: eine Abfrage mit einer Funktion darüber, damit ein N+1 irgendwo landen kann.

Was es misst

Jede Funktion, die du markierst
ctx, span := sixty.Start(ctx, "orders.List") und ein deferred capture. Zwei Zeilen, und der Kontext muss durchgereicht werden — das ist der Preis dafür, dass es in Go keinen impliziten Kontext gibt, und der Grund, warum die Zahl vertrauenswürdig ist, wenn sie ankommt.
Jede Abfrage, auf jedem database/sql-Treiber
Der Agent umschließt den Treiber statt die Verbindung, sodass pgx, lib/pq, MySQL und alles andere, was gegen database/sql registriert ist, gemessen wird — und jede optionale Schnittstelle, die der echte Treiber implementiert, wird gespiegelt, sodass nichts, was er könnte, aufhört zu funktionieren.
Zeilen, gezählt beim Lesen
Nicht aus einem zurückgegebenen Slice, weil database/sql keines hat. Der Span schließt, wenn die Rows schließen, und genau das macht die Zahl echt statt abgeleitet.
Welche Funktion welche Abfrage abgesetzt hat
Der Kontext trägt den aktuellen Span, eine Abfrage wird also der Funktion gutgeschrieben, die sie abgesetzt hat. Das ist es, was aus „der Endpunkt wurde langsam“ ein „diese Funktion hat angefangen, zwanzig Aufrufe zu machen“ macht.
Deine HTTP-Routen, rein und raus
sixty.Middleware umschließt alles, was net/http spricht — chi, gorilla, echo, den Pattern-Mux von Go 1.22. Wo der Router ein Muster kennt, das der Pfad nicht hat, benennt sixty.SetRoute es.

Was es findet

Jedes davon wird gegen das Release davor verglichen, der Befund benennt also die Änderung und nicht den Tag.

Ein N+1, das im letzten Release nicht da war

Eine Funktion, die einen Datenbankaufruf pro Anfrage machte, macht jetzt zwanzig. An das Deployment verankert, der Befund benennt also das Release statt der Stunde.

Eine Abfrage, die angefangen hat, die ganze Tabelle zu lesen

Zeilen pro Aufruf sind von 30 auf 30.000 gegangen — und hier wird diese Zahl genommen, während der Aufrufer iteriert, es ist also das, was dein Code tatsächlich gelesen hat, und nicht das, was das Statement hätte zurückgeben können.

Dein eigener Code, der langsamer wird

Zeit in der Funktion selbst, getrennt von der Zeit in dem, was sie aufgerufen hat, damit du beim ersten Versuch die richtige Datei öffnest.

Etwas, das jetzt in einer Schleife aufgerufen wird

Gleiche Geschwindigkeit pro Aufruf, gleiche Daten, hunderte Male aufgerufen, wo es früher einmal aufgerufen wurde.

Eine Abfrage, die still nichts zurückgibt

Kein Fehler, kein langsamer Aufruf, und leer, wo früher Zeilen waren. Nichts in einem Go-Dienst meldet das als Fehlschlag, weil nichts fehlgeschlagen ist.

Fehler, gruppiert danach, wo sie herkamen

Nach Code-Ort statt nach Meldung — ein Bug ist ein Eintrag, wie viele Werte er auch hineinformatiert.

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-go absichtlich ausliefert, gegen den Datensatz, den sein Seed-Befehl anlegt — etwa 1.000 Nutzer und 30.000 Bestellungen. Du kannst es laufen lassen und ihnen beim Ankommen zusehen.

Zeilenorders.GetUserOrders
30 Zeilen30.000 Zeilen1000×

Ein Refactoring hat die WHERE-Klausel fallen lassen, jeder Aufruf holt also die ganze Tabelle und filtert sie in Go. Die Latenz hat sich auf einer warmen Datenbank kaum bewegt — die Zeilenzahl verrät es, und sie ist echt, weil sie beim Lesen der Zeilen genommen wird.

seit Release v2, gegen v1

N+1orders.EnrichOrders
1 Abfrage20 Abfragen20×

Aus einer einzelnen gebündelten Abfrage wurde eine Suche pro Bestellung. Jede ist schnell und indiziert, in einem Latenzdiagramm bewegt sich also nichts — nur die Anzahl.

seit Release v2, gegen v1

Wie es installiert wird

Gib es dem Coding-Agenten, den du ohnehin offen hast. Er findet main(), den Handler und das sql.Open, reicht den Kontext dort durch, wo er durchgereicht werden muss, und markiert die Funktionen, die es wert sind — der Schritt, der leicht zu beschreiben und mühsam von Hand zu machen ist.

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)
    ...
}

Der Release-Bezeichner wird von allein von Vercel, Render, Railway, Fly, Heroku oder GitHub Actions abgeholt, und SIXTY_RELEASE setzt ihn überall sonst. Der erste Vergleich kommt nach deinem nächsten Deployment, weil es nichts gibt, womit man ein Release vergleichen könnte.

Was es nicht tut

  • Der Kontext muss durchgereicht werden. Eine Goroutine, der der ctx nicht übergeben wird, macht ihre Arbeit außerhalb der Operation — richtig für Hintergrundarbeit, falsch für ein Fan-out, das du messen wolltest. Es gibt keine Variante davon mit implizitem Kontext, und wir werden keine bauen, weil die Variante, die rät, die Variante ist, die die Abfragen einer Anfrage einer anderen zuschreibt.
  • Abfragepläne werden nicht erfasst. Das ist der Befund, der eine Ursache statt eines Symptoms benennt — „das benutzt seinen Index nicht mehr“ — und auf einem Go-Dienst kommt er nicht an.
  • CPU und Warten werden nicht getrennt. Goroutinen wandern zwischen Threads, eine Uhr pro Thread beschreibt also keinen Span, und eine zu melden wäre eine Zahl, die genau dann falsch ist, wenn es darauf ankommt.
  • Postgres ist das, was der Detektor in der Tiefe versteht. Eine andere Datenbank, die gegen database/sql registriert ist, meldet weiterhin Zeiten, Zeilen und Anzahlen; die Schlussfolgerungen auf Statement-Ebene sind schwächer.
  • Hier markiert nichts deine Funktionen für dich. Diesen Schritt auszulassen lässt den Verlauf mit Routen und Abfragen und nichts dazwischen zurück, und das ist die Hälfte, die den Rest lesenswert macht.

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, Python-Dienste oder Ruby- und Rails-Apps an — diese Seiten sind anders, und was wir sehen können, auch.

Performance-Monitoring für Go — net/http, database/sql und Postgres — Sixty