sixty

服务端 agent

Ruby & Rails

唯一一个没有第二步的安装。railtie 会在启动时加上中间件、订阅 sql.active_record,并包住控制器的 action —— 没有 initializer 要写。

包
sixty 发布在 RubyGems
运行环境
Ruby 2.7 及以上。Rails 6.1+ 会自己装好;其余情况有 Rack 就够了。
源码
sixty-sh/sixty-ruby

安装

Rails installs itself — the railtie adds the middleware, subscribes to sql.active_record and wraps controllers on boot. Postgres, MySQL and MongoDB, with query plans on Postgres.

安装说明是写给你已经开着的那个编码助手的提示词,而不是给你的一份清单。这是有意的:它说的是安装完成之后什么必须成立,而不是要改哪些文件 —— 因为代码该放在哪里取决于框架,而放错地方是会静默失败的。助手可以读你的仓库并把这件事推断出来;文档页上的一个段落做不到。

同样这段文字,就是 install_sixty 通过 MCP 服务器 返回的内容,也是收集端在以下位置提供的内容: /v1/setup?kind=ruby. 它只有一份。

Ruby & Rails 的完整安装说明
Install the sixty agent in this Ruby application so its controllers, service
objects and database queries report to sixty.

1. Add the gem to the main group of the Gemfile — not to :development and not
   to :test. It runs in production; that is the entire point. It has no
   dependencies of its own, so nothing else in the lockfile moves.

      gem 'sixty'

   Then bundle install.

2. Rails needs no further wiring. The railtie adds the middleware, subscribes
   to sql.active_record and wraps controller actions on boot. Do NOT write an
   initializer that calls Sixty.init — in a Rails app that is a second boot
   path for the same thing.

   If this is Sinatra, Hanami, a bare Rack app or a non-web process, then and
   only then do it by hand: require 'sixty' and call Sixty.init at startup,
   and add "use Sixty::Instrument::Rack" to config.ru.

3. Measure the layer between the controller and the database. This 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 the object that
   causes it. In a Rails app that object is a query object, a service object,
   a serializer or a job — rarely the controller.

      class OrdersQuery
        include Sixty::Instrumented
      end

   Every public instance method of that class becomes an operation, with its
   queries and rows attributed to it. Private methods and plain accessors are
   skipped. Add it to the few classes that do real work per request — not to
   models, and not to anything called in a tight loop.

   For a class you do not own: Sixty.instrument(Stripe::Charge, :create)
   For a block: Sixty.trace('nightly-reconciliation') { ... }

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 include Sixty::Instrumented in ActiveRecord models or in anything
  called per row. A span costs a few microseconds, which is nothing next to a
  request and everything next to a loop over a result set.
- 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 database configuration alone. pg, mysql2, trilogy and mongo are
  all instrumented already; if this application reaches its database some
  other way, tell me rather than wiring something up.

If this runs under Puma with workers, or under Sidekiq, note how many
processes it runs: each reports independently and the collector merges them,
which is correct, but it is worth knowing when you read the numbers.

When you are done, tell me which files you changed, so I can confirm data is
arriving.

它需要一个私密密钥 —— 以 sixty_sk_ 开头,并且只留在服务端。登录之后可以在设置页生成一个。

它测量什么

信号单位含义
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

它接在哪里

  • Rails — 自动。控制器成为操作,按 controller#action 命名。
  • Rack — 在 config.ru 里 use Sixty::Instrument::Rack,适用于 Sinatra、Hanami 或者一个裸应用。
  • 你自己的类 — include Sixty::Instrumented —— 每个公有实例方法都会成为一个操作。私有方法和访问器不动。
  • 其他任何情况 — Sixty.instrument(Stripe::Charge, :create),或者 Sixty.trace("job") { }。

数据库

  • pg — 行数、语句形状,以及通过 EXPLAIN 得到的查询计划。
  • mysql2 / trilogy — 按 MySQL 的引号规则读,而不是 Postgres 的 —— 一个双引号字符串在一边是字面量,在另一边是标识符。
  • mongo — Mongoid 用的就是它。命令形状作为 identity。

只有它才做的事

  • 没有东西要写 — Rails 有一个有文档的启动钩子,还有一条本来就会广播每条查询的事件总线,所以这个 gem 既不需要 initializer 也不需要包装层。
  • Postgres 上的查询计划 — 和 Node agent 一样的 EXPLAIN,按语句缓存。

它做不到什么

  • 查询计划只有 Postgres 有。MySQL 没有通用计划的 EXPLAIN,MongoDB 也没有可以解释的语句。
  • CPU 和等待没有分开。
  • 在带 worker 的 Puma 下,或者在 Sidekiq 下,每个进程各自上报,由收集端合并。

配置

每个 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 变化而保持正确的地方。

sixty 的 Ruby & Rails agent —— 它测量什么、怎么安装