服务端 agent
Go
这里的安装是显式的 —— 一个中间件、一层驱动包装,以及在值得测量的函数开头加两行 —— 因为 Go 没有可以挂钩的构建步骤,也没有办法在不被传入的情况下拿到调用方的 context。
- 包
github.com/sixty-sh/sixty-go发布在 Go modules- 运行环境
- Go 1.22 及以上。没有依赖,它添加的 go.sum 里也没有。
- 源码
- sixty-sh/sixty-go
安装
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.
安装说明是写给你已经开着的那个编码助手的提示词,而不是给你的一份清单。这是有意的:它说的是安装完成之后什么必须成立,而不是要改哪些文件 —— 因为代码该放在哪里取决于框架,而放错地方是会静默失败的。助手可以读你的仓库并把这件事推断出来;文档页上的一个段落做不到。
同样这段文字,就是 install_sixty 通过 MCP 服务器 返回的内容,也是收集端在以下位置提供的内容: /v1/setup?kind=go. 它只有一份。
Go 的完整安装说明
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.它需要一个私密密钥 —— 以 sixty_sk_ 开头,并且只留在服务端。登录之后可以在设置页生成一个。
它测量什么
| 信号 | 单位 | 含义 |
|---|---|---|
rows | rows per call | this query returns more rows than it used to |
fanout | queries per call | this operation now issues more database calls per invocation — an N+1 |
latency | ms per call | this operation takes longer end to end than it used to |
self_latency | ms per call | the time spent in this function itself got longer — its children did not |
payload | bytes per call | the serialized result of this operation got bigger |
errors | error rate | a larger fraction of calls are throwing |
runaway | calls per minute | this operation is being called far more often than anything triggers it |
repeated_query | times per request | the identical query runs several times within one request |
overfetch | rows per call | far more rows are fetched than the code appears to use |
unbounded | rows per call | this query has no upper bound on what it can return |
recursion | levels deep | this operation calls itself, deeper than it should |
new_error | occurrences | an 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. |
它接在哪里
- net/http — sixty.Middleware 能包住任何 handler,包括 Go 1.22 的 pattern mux、chi、gorilla 和 echo。
- 路由名 — sixty.SetRoute(r, "/orders/{id}") —— 用在路由器知道模式、而路径本身不体现的地方。
- 你自己的函数 — ctx, done := sixty.Start(ctx, "orders.List"); defer done() —— 两行,而且 context 必须一路传下去。
数据库
- database/sql — 任何驱动。agent 包的是驱动而不是连接,在调用方遍历时统计行数,并镜像真实驱动实现的每一个可选接口。
只有它才做的事
- 一个没有依赖的模块 — 它不会往 go.sum 里加任何东西。这东西会被加载进别人的生产二进制里,而一个依赖就意味着一次由监控工具引起的版本冲突。
- 在读取时统计的行数 — 不是从返回的切片里数的 —— database/sql 根本没有切片。span 在 rows 关闭时关闭,所以一条 rows 从未关闭的查询会上报得很晚。
它做不到什么
- context 必须一路传下去。一个没被传入 ctx 的 goroutine,它的工作发生在这个操作之外 —— 对后台任务来说是对的,对一个你本想测量的扇出来说是错的。这件事没有隐式 context 的版本,我们也不会去做一个。
- 不采集查询计划。
- CPU 和等待没有分开。goroutine 会在线程之间迁移,所以按线程走的时钟无法描述一个 span。
配置
每个 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 变化而保持正确的地方。