sixty

Server-Agent

Go

Die Installation ist ausdrücklich — eine Middleware, ein Treiber-Wrapper und zwei Zeilen am Anfang der messenswerten Funktionen — weil Go keinen Build-Schritt zum Einhaken hat und keine Möglichkeit, den Kontext des Aufrufers zu erreichen, ohne ihn übergeben zu bekommen.

Paket
github.com/sixty-sh/sixty-go auf Go modules
läuft auf
Go 1.22 oder neuer. Keine Abhängigkeiten, auch nicht in der go.sum, die es hinzufügt.
Quellcode
sixty-sh/sixty-go

Installation

Any net/http server and any database/sql driver: functions, HTTP routes and SQL. Spans are threaded through context, so the install is explicit rather than automatic.

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=go. Es gibt genau eine Kopie davon.

die vollständige Go-Installation
Install the sixty agent in this Go service so its functions, HTTP routes and
database queries report to sixty.

The package is github.com/sixty-sh/sixty-go, imported as "sixty". It
has no dependencies of its own.

Before editing, inspect whether this service makes LLM or agent calls. If it
does, ask the user: "Do you want LLM monitoring?" If yes, use StartAgent,
StartGeneration and StartTool, thread each returned context through the work,
and call generation.SetUsage with token/cost metadata before End. The API
does not accept prompts, outputs, tool arguments or tool results.

1. In main(), before the server starts:

      defer sixty.Init(sixty.Config{}).Shutdown(context.Background())

   The zero Config reads the environment. Shutdown sends the last window, so
   a short-lived process still reports; on a long-lived server it runs at
   exit. If this service already traps signals for graceful shutdown, call
   Shutdown there instead of deferring.

2. Wrap the HTTP handler — sixty.Middleware(handler) — at the outermost level,
   so the span covers the other middleware rather than sitting inside it.
   Anything that speaks net/http works: chi, gorilla/mux, echo, the standard
   library's own mux.

   If this project uses a router that knows its route patterns, add one more
   middleware AFTER routing that calls sixty.SetRoute(r.Context(), pattern) —
   chi.RouteContext(r.Context()).RoutePattern(), or the equivalent. Without
   it, /users/42/orders is templated to /users/:id/orders, which is close and
   occasionally wrong.

3. Measure the database. Replace the sql.Open call with sixty.Open, passing
   the same driver name and DSN:

      db, err := sixty.Open("pgx", dsn)

   If this project builds its pool some other way — a Connector, a pgx pool
   through stdlib, a wrapper library — use sixty.WrapDriver(d) around the
   driver it registers instead. Postgres is what the detector understands;
   another database will report timings and nothing else.

4. Measure the functions worth measuring. This is the step that turns "this
   endpoint got slow" into "this function started issuing 14 queries", and
   skipping it leaves the feed with routes and queries and nothing between:

      func (s *Store) GetUserOrders(ctx context.Context, id int) (_ []Order, err error) {
          ctx, span := sixty.Start(ctx, "orders.GetUserOrders")
          defer span.Capture(&err)
          ...
      }

   Put it on the service or repository layer — the code between the handler
   and the database — not on handlers the middleware already covers. Name
   operations package.Function, and keep the names stable: the name is the
   identity, and renaming one starts its history over.

   Do NOT put it on functions called hundreds of thousands of times a second.
   A span costs a few hundred nanoseconds, which is nothing next to a request
   and everything next to a tight loop.

5. Set these environment variables wherever the service 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 usually needs nothing: the Go toolchain stamps the
   commit into the binary and the agent reads it back. If this builds with
   -buildvcs=false, or from a source tarball with no repository, 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.
- Thread the context. sixty.Start returns a new ctx and the work must use it,
  or the queries underneath are recorded as belonging to nobody. There is no
  ambient-context version of this and you should not build one: no
  goroutine-local storage, no //go:linkname, no global "current span".
- A goroutine started inside a measured function must be passed that ctx if
  its work should count towards the operation. One that outlives the request
  should NOT be — it would attribute background work to whoever happened to
  start it.
- 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.
- Leave the sql.Rows handling alone. The agent counts rows as the caller
  reads them and closes the span when the rows close, so a query whose rows
  are never closed reports late — which is also a connection leak worth
  fixing on its own terms.

When you are done, tell me which files you changed and what the deployed start
command now is, 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.

Wo er sich einhängt

  • net/http — sixty.Middleware umschließt jeden Handler, einschließlich des Pattern-Mux von Go 1.22, chi, gorilla und echo.
  • Routennamen — sixty.SetRoute(r, "/orders/{id}") dort, wo der Router das Muster kennt und der Pfad nicht.
  • Deine eigenen Funktionen — ctx, done := sixty.Start(ctx, "orders.List"); defer done() — zwei Zeilen, und der Kontext muss durchgereicht werden.

Datenbanken

  • database/sql — Jeder Treiber. Der Agent umschließt den Treiber statt die Verbindung, zählt Zeilen, während der Aufrufer sie durchläuft, und spiegelt jede optionale Schnittstelle, die der echte Treiber implementiert.

Was nur dieser kann

  • Ein abhängigkeitsfreies Modul — Es fügt der go.sum nichts hinzu. Das wird in fremde Produktionsbinaries geladen, und eine Abhängigkeit wäre ein Versionskonflikt, verursacht von einem Monitoring-Werkzeug.
  • Zeilen, gezählt beim Lesen — Nicht aus einem zurückgegebenen Slice — database/sql hat keines. Der Span schließt, wenn die Rows schließen, eine Abfrage, deren Rows nie geschlossen werden, meldet also spät.

Was er nicht kann

  • 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 mit implizitem Kontext, und wir werden keine bauen.
  • Abfragepläne werden nicht erfasst.
  • CPU und Warten werden nicht getrennt. Goroutinen wandern zwischen Threads, eine Uhr pro Thread beschreibt also keinen Span.

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 Go-Agent von sixty — was er misst und wie man ihn installiert