Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,8 @@ only; computing durations or prices stays with the consumer.
id text NOT NULL,
vm text NOT NULL DEFAULT '',
key text NOT NULL DEFAULT '',
template text NOT NULL DEFAULT '',
size text NOT NULL DEFAULT '',
tenant text NOT NULL DEFAULT '',
volumes text[],
volumes_rw text[],
Expand Down
11 changes: 7 additions & 4 deletions docs/sandboxd-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1015,8 +1015,10 @@ Always on: every lifecycle transition appends one JSONL event to
`<data_dir>/usage.jsonl` — `{"t": <RFC3339>, "ev":
"claim|hibernate|wake|fork|checkpoint|promote|release|reap|archive|unarchive|archive_delete|egress|egress_bytes|vmm_exit|vmm_restart|vmm_failed",
"id": "sb_…", "vm": "sbx-…"}` plus `key` (the pool key's stable hash, claim
events), `tenant` (the owning tenant, on every event of a tenant's claim),
`children` (fork) and `ref` (the promoted
events), `template` and `size` (the claim's template and size tier, claim
events, so a consumer can price a sandbox by its shape without the claim
record, which is gone after release), `tenant` (the owning tenant, on every
event of a tenant's claim), `children` (fork) and `ref` (the promoted
template / checkpoint id, the egress host, or a `vmm_failed` reason). An egress event that injected credentials names
them in `secret` (`gh,claim:<entry>`, never a value). An egress event routed through an
[upstream proxy](egress.md#upstream-proxies) carries `upstream` (its `host:port`), and with
Expand Down Expand Up @@ -1047,11 +1049,12 @@ Auth: root only (tenant tokens get 403). Reads the raw usage events of every
node of the cell from the [shared usage table](deploy.md#shared-usage-table).

- `GET /v1/usage?tenant=<name>&since=<time>&until=<time>&after=<cursor>&limit=<n>` →
`{"events": [{"cursor": 41, "node": "n1", "t": "2026-10-30T08:00:00.123456Z", "ev": "claim", "id": "sb_…", "vm": "sbx-…", "key": "…", "tenant": "acme"}], "next": 41}`.
`{"events": [{"cursor": 41, "node": "n1", "t": "2026-10-30T08:00:00.123456Z", "ev": "claim", "id": "sb_…", "vm": "sbx-…", "key": "…", "template": "…", "size": "large", "tenant": "acme"}], "next": 41}`.
- **Events:** each is one journal line with its keys as in
[usage.jsonl](#usage-journal-usagejsonl), `t` in UTC to the microsecond, plus
`cursor` (its place in the table; one node's events follow its journal
order) and `node` (the node that recorded it).
order) and `node` (the node that recorded it). A `claim` line written by
v0.1.20 or earlier has no `template` or `size`.
- **Filters:** `tenant` keeps one tenant's events; `since` (inclusive) and
`until` (exclusive) are RFC 3339 times compared with `t`. Each is optional.
A journal written by v0.1.20 or earlier stamps `tenant` only on `claim`,
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/pool/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ func (m *Manager) finalizeBatch(ctx context.Context, sbs []*types.Sandbox, ttl t
m.recordUsage(ctx, usage.Event{
Event: "claim",
ID: sb.ID, VMName: sb.VMName,
KeyHash: sb.Key.Hash(), Tenant: sb.Tenant,
KeyHash: sb.Key.Hash(), Template: sb.Key.Template, Size: string(sb.Key.Size), Tenant: sb.Tenant,
Volumes: types.VolumeNames(sb.Volumes), VolumesRW: types.VolumeRWNames(sb.Volumes),
})
}
Expand Down
3 changes: 3 additions & 0 deletions sandboxd/pool/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ func TestTenantStampedInJournals(t *testing.T) {
if ev.Tenant != "acme" {
t.Errorf("claim event tenant %q, want acme", ev.Tenant)
}
if ev.Template != testKey.Template || ev.Size != string(testKey.Size) {
t.Errorf("claim event template %q size %q, want %q %q", ev.Template, ev.Size, testKey.Template, testKey.Size)
}
}
}
if !found {
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/server/usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ func TestUsageIsRootOnlyAndNamesAMissingStore(t *testing.T) {
func TestUsagePassesTheQueryAndPages(t *testing.T) {
t0 := time.Date(2026, 10, 30, 1, 2, 3, 400000000, time.UTC)
reader := &fakeUsage{recs: []usage.Record{
{Cursor: 7, Node: "n1", Digest: [32]byte{1}, Event: usage.Event{Time: t0, Event: "claim", ID: "sb_1", Tenant: "acme"}},
{Cursor: 7, Node: "n1", Digest: [32]byte{1}, Event: usage.Event{Time: t0, Event: "claim", ID: "sb_1", Template: "rt:24.04", Size: "xlarge", Tenant: "acme"}},
{Cursor: 9, Node: "n2", Event: usage.Event{Time: t0, Event: "release", ID: "sb_1"}},
}}
ts := usageTestServer(t, reader)
resp := doReq(t, http.MethodGet, ts.URL+"/v1/usage?tenant=acme&since=2026-10-30T00:00:00Z&until=2026-10-31T00:00:00.5%2B08:00&after=5&limit=2", "root", "")
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
want := `{"events":[{"cursor":7,"node":"n1","t":"2026-10-30T01:02:03.4Z","ev":"claim","id":"sb_1","tenant":"acme"},` +
want := `{"events":[{"cursor":7,"node":"n1","t":"2026-10-30T01:02:03.4Z","ev":"claim","id":"sb_1","template":"rt:24.04","size":"xlarge","tenant":"acme"},` +
`{"cursor":9,"node":"n2","t":"2026-10-30T01:02:03.4Z","ev":"release","id":"sb_1"}],"next":9}`
if resp.StatusCode != http.StatusOK || string(body) != want {
t.Errorf("full page: %d %s, want %s", resp.StatusCode, body, want)
Expand Down
10 changes: 5 additions & 5 deletions sandboxd/usage/pg/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const (
// cellLock serializes a cell's lands, so cursors commit in order and a reader paging by cursor never skips a row.
cellLock = `SELECT pg_advisory_xact_lock(hashtextextended('sandboxd_usage/' || $1, 0))`

insertRow = `INSERT INTO sandboxd_usage (cell, node, line_sha256, t, ev, id, vm, key, tenant, volumes, volumes_rw, children, ref, upstream, secret, tx, rx)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
insertRow = `INSERT INTO sandboxd_usage (cell, node, line_sha256, t, ev, id, vm, key, template, size, tenant, volumes, volumes_rw, children, ref, upstream, secret, tx, rx)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
ON CONFLICT (cell, node, line_sha256) DO NOTHING`

listColumns = `cursor, node, t, ev, id, vm, key, tenant, volumes, volumes_rw, children, ref, upstream, secret, tx, rx`
listColumns = `cursor, node, t, ev, id, vm, key, template, size, tenant, volumes, volumes_rw, children, ref, upstream, secret, tx, rx`
)

//go:embed schema.sql
Expand Down Expand Up @@ -71,7 +71,7 @@ func (s *Store) Land(ctx context.Context, recs []usage.Record) error {
}
var batch pgx.Batch
for _, r := range recs {
batch.Queue(insertRow, s.cell, r.Node, r.Digest[:], r.Time, r.Event.Event, r.ID, r.VMName, r.KeyHash, r.Tenant, r.Volumes, r.VolumesRW, r.Children,
batch.Queue(insertRow, s.cell, r.Node, r.Digest[:], r.Time, r.Event.Event, r.ID, r.VMName, r.KeyHash, r.Template, r.Size, r.Tenant, r.Volumes, r.VolumesRW, r.Children,
strings.ReplaceAll(r.Reference, "\x00", "\uFFFD"), r.Upstream, r.Secret, r.Sent, r.Received)
}
return tx.SendBatch(qctx, &batch).Close()
Expand Down Expand Up @@ -112,7 +112,7 @@ func (s *Store) Close() {

func scan(row pgx.CollectableRow) (usage.Record, error) {
var rec usage.Record
err := row.Scan(&rec.Cursor, &rec.Node, &rec.Time, &rec.Event.Event, &rec.ID, &rec.VMName, &rec.KeyHash, &rec.Tenant,
err := row.Scan(&rec.Cursor, &rec.Node, &rec.Time, &rec.Event.Event, &rec.ID, &rec.VMName, &rec.KeyHash, &rec.Template, &rec.Size, &rec.Tenant,
&rec.Volumes, &rec.VolumesRW, &rec.Children, &rec.Reference, &rec.Upstream, &rec.Secret, &rec.Sent, &rec.Received)
if err != nil {
return usage.Record{}, fmt.Errorf("scan usage row: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/usage/pg/pg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestLandListsInCursorOrderAndResumesFromTheMark(t *testing.T) {
}
t0 := time.Date(2026, 10, 30, 0, 0, 0, 0, time.UTC)
full := usage.Event{
Time: t0.Add(time.Microsecond), Event: "claim", ID: "sb_1", VMName: "sbx-1", KeyHash: "k", Tenant: "acme",
Time: t0.Add(time.Microsecond), Event: "claim", ID: "sb_1", VMName: "sbx-1", KeyHash: "k", Template: "rt:24.04", Size: "xlarge", Tenant: "acme",
Volumes: []string{"data", "ro"}, VolumesRW: []string{"data"}, Reference: "ref", Upstream: "up:1", Secret: "gh", Sent: 7, Received: 9,
}
land(t, s, record("n1", 1, full), record("n2", 1, usage.Event{Time: t0, Event: "claim", ID: "sb_2"}))
Expand Down
2 changes: 2 additions & 0 deletions sandboxd/usage/pg/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS sandboxd_usage (
id text NOT NULL,
vm text NOT NULL DEFAULT '',
key text NOT NULL DEFAULT '',
template text NOT NULL DEFAULT '',
size text NOT NULL DEFAULT '',
tenant text NOT NULL DEFAULT '',
volumes text[],
volumes_rw text[],
Expand Down
2 changes: 2 additions & 0 deletions sandboxd/usage/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Event struct {
ID string `json:"id"`
VMName string `json:"vm,omitempty"`
KeyHash string `json:"key,omitempty"`
Template string `json:"template,omitempty"`
Size string `json:"size,omitempty"`
Tenant string `json:"tenant,omitempty"`
Volumes []string `json:"volumes,omitempty"`
VolumesRW []string `json:"volumes_rw,omitempty"`
Expand Down
Loading