Skip to content

feat: keep the service's hours for 90 days and serve them to the stats Worker - #51

Merged
frahlg merged 9 commits into
mainfrom
feat/internal-ops-stats
Oct 24, 2026
Merged

frahlg merged 9 commits into
mainfrom
feat/internal-ops-stats

Conversation

@frahlg

@frahlg frahlg commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Problem

Fredrik, 24 October 2026: Sourceful needs internal statistics to run FTW Cloud and understand the fleet, for internal use, in one place, without a new system. Today /metrics and the minute's log line count well, but nothing keeps them: the log dies at every deploy.

What changes

  • The hours are kept. internal/metrics rolls every series up into clock hours (Keeper): each counter's rise, each gauge's low, mean and high over the minutes sampled, each histogram's buckets, how much of the hour a process ran, how many processes started. The registry keeps one row an hour (ops_hours) for 90 days, pruned at each write and at upkeep. A deploy hands over its part of the hour after the drain; the next process adds the rest to the same row.
  • GET /v1/ops (internal/ops) serves up to a week of settled hours (?since=), as counts and p50/p95/p99 in ms, plus the exact totals of the anonymous fleet and energy reports for closed days. It answers only Authorization: Bearer <key>. The service holds the key's SHA-256 (-ops-key-sha256, FTW_CLOUD_OPS_KEY_SHA256 in compose), which is no secret; empty answers 404 to everyone and keeps the hours anyway. The key lives only as the stats Worker's secret.
  • New series for what the private view needs: open sockets by transport (transport_sockets_{box,app}_{quic,websocket}), bytes written to boxes and phones (bytes_out_{box,app}, against AWS's free 100 GB of data out), MCP requests and tool calls by outcome (mcp_*) and their time, browsers' push answers (webpush_*), and the drain (drains, drained_sockets, drain_left_open).
  • Docs: README "Internal operations", deploy/README.md "Watching it" (how to make the key and its hash), CLAUDE.md invariants, and privacy/internal-operations.md: what is kept, how long, who sees it, why none of it is about a household, and what remains.

Limits

  • A crash loses up to an hour. The keeper hands an hour to the registry when it ends, or on a clean drain. A process that dies without SIGTERM loses the counts of its current hour, up to 59 minutes. The next hour shows it: seconds below 3600 and starts 1.
  • A bad key hash leaves the feed off, not the service. -ops-key-sha256 is read by UseKey after New. Quotes and white space around it are dropped. A hash that is still not 64 hex digits is logged at start, /v1/ops answers 404, and boxes and phones are served as before (review fix, second commit).

Why none of it names a home

  • Every series sums over the whole process. TestCountsNameNobody holds the list of label names (kind, reason, path, dir, result, transport, outcome); each value is a fixed word.
  • TestAnHourOfCountsNamesNobody runs a box over QUIC and a phone through the tunnel, then checks that the stored hour holds the traffic, that its series are the ones that existed before the traffic, and that it holds no home id, device id, key or address.
  • Days are served only when closed: a fleet day once it has ended, an energy date once no report may name it (as /v1/energy/public). Two answers never differ by one report.
  • TestTheFeedHoldsOnlyItsFields pins the field set; the Worker refuses anything else.

The limit, plainly: while the fleet is a handful of homes, an hour's phone count or a closed day with one report can still describe one of a few people staff know. Only Access sees it. privacy/internal-operations.md names this and the options if Fredrik wants it smaller.

Size

Lines of Go without tests, against #50: internal/metrics +224 (hours.go), internal/ops +278 (new), internal/registry +68, internal/mcp +26, cmd/ftwcloudd +22, internal/webpush +15, internal/server +14, internal/appsession +10, internal/wire +10, internal/uplink +8. About +675 in all.

Pairs and order

Nothing is deployed. Turning it on needs the key in the Worker and its hash in /etc/ftw-cloud/compose.env.

Checks

go vet ./... && go test -race ./..., go test -tags ftwclouddev ./..., bash scripts/check-imports.sh: all pass locally. (TestARefusalOverQUICCarriesItsCode hit its 3 s deadline once on a loaded laptop and passed three runs in a row after.)

🤖 Generated with Claude Code

ftwcloudd is the gateway (RFC 9458) for the box's daily energy report,
which reaches it through an independent relay, so the service sees the
report and never the box's address.

- POST /v1/ohttp opens a request with the gateway key, takes only the one
  padded inner request with one valid report (energywire), adds its six
  sums to the date's row and answers a sealed 204 or 400. Nothing is kept
  per report: no row, logged body or address. One shared rate budget, and
  at most 100,000 reports a date.
- GET /.well-known/ohttp-gateway serves the key configuration.
- GET /v1/energy/public lists the closed dates of the last 30 days; a date
  with fewer than 5 reports shows null.
- The key comes from Parameter Store into tmpfs at boot, as the Noise key
  does (deploy/ohttp.md, deploy/ohttp.sh); keygen -ohttp prints the pin
  Core needs. Without a key the gateway routes answer 501.

Pairs with srcfl/ftw feat/anonymous-energy-ohttp; go/protocol is pinned at
its commit 28fddd802242.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T06:22:23.821838Z 4f2c862 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f2c86290f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/metrics/hours.go
Comment on lines +162 to +163
k.rise(k.set.state(), k.now())
k.put(k.hour)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll over the hour when closing the keeper

If SIGTERM arrives just before an hour boundary and the drain finishes after it, Keeper.Run stops with the signal, but Close adds the remaining duration and counters to the previous hour without checking the current hour. For example, beginning at 05:59 and closing at 06:00:03 after incrementing drains produces only a 05:00 row containing all 63 seconds and the drain; the 06:00 contribution is missing. This misattributes shutdown activity and can make a full hour exceed 3,600 seconds. Handle the hour boundary during close, splitting elapsed time and recording the drain in the hour in which it finishes.

Useful? React with 👍 / 👎.

Fredrik's change of scope, 24 October 2026: the fleet report moves onto
the same relay and gateway as the energy report, and the two stay apart.

- internal/energy becomes internal/gateway. POST /v1/ohttp reads the one
  padded inner request (go/protocol/reportwire), picks the decoder by
  schema, and adds a fleet report to the existing fleet_days totals that
  /v1/fleet/public reads and an energy report to energy_days. Nothing is
  kept per report.
- POST /v1/fleet stays for boxes on a Core release without a relay; it can
  go one stable Core release after the first that pins the relay.
- The request cap fits the 1,591-byte sealed request; README and
  deploy/ohttp.md describe both reports and the two-per-day traffic.
- go/protocol pinned at srcfl/ftw 4cfd65f7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from 4f2c862 to 999854e Compare October 24, 2026 06:43
A date with five reports and one car published that car's whole charge in
vehicle_charge_kwh, because the sum counted the other four reports' zeros.
energy_days now counts, for each sum, the reports that had any of it, and
/v1/energy/public shows a sum only when none or at least five had some.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from 999854e to d0fe105 Compare October 24, 2026 07:04
…t view

From review of #50:
- Replays: each inner request now carries a random report id and its send
  day (srcfl/ftw a7c025e8). The gateway refuses a request sent before
  yesterday and keeps a hash of each id per day for three days, in a WITHOUT
  ROWID table with no time, so a replay or a retry adds nothing and is still
  answered 204. Seven posts of one ciphertext now count once.
- /v1/fleet/public lists closed UTC days only, so the relay cannot poll it
  and match a new count to the report it just forwarded.
- Each report kind has its own budget, spent only by a report that opened
  and decoded; junk no longer silences real reports.
- A 10-second deadline on reading the body of /v1/ohttp and /v1/fleet; a
  failed read closes the connection.
- ohttp keys: data after the last whole PEM block, such as a second key cut
  short in a rotation, is refused instead of silently ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from d0fe105 to e755d0f Compare October 24, 2026 07:16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from e755d0f to f675fdc Compare October 24, 2026 07:50
frahlg and others added 3 commits October 24, 2026 10:10
The gateway now looks a report id up, read-only, before it spends the
kind's budget. A repeat gets a sealed 204 and costs nothing, so one
captured request sent many times cannot crowd out real reports.

The OHTTP key file must hold whole PKCS #8 blocks and nothing else. A
block cut short at either end, or text between blocks, stops the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s Worker

ftwcloudd now keeps an hourly summary of every series it counts in the
registry (ops_hours), for 90 days: each counter's rise, each gauge's low,
mean and high, each latency's buckets, how much of the hour a process ran
and how many started. A deploy hands over its part of the hour after the
drain; the next process adds the rest.

GET /v1/ops serves a week of settled hours at a time, with the exact
totals of the anonymous fleet and energy reports for closed days, only to
the key the project stats Worker holds. The service holds the key's
SHA-256 (-ops-key-sha256); without it the route answers 404 to everyone.

New series: sockets by transport (QUIC or WebSocket), bytes written to
boxes and phones, MCP requests and tool calls by outcome and their time,
web push answers, and the drain's sockets. Every label is a fixed word
(TestCountsNameNobody), and traffic makes no new series and leaves no home,
device, key or address in an hour (TestAnHourOfCountsNamesNobody).

Stacked on #50, whose energy day totals it reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed FTW_CLOUD_OPS_KEY_SHA256, such as a stray quote or line end
in compose.env, made ops.New fail, and with it the whole service. The
hash is now read by UseKey after New: quotes and white space around it
are dropped, and a hash that is still not 64 hex digits is logged and
leaves /v1/ops answering 404, while boxes and phones are served as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the feat/internal-ops-stats branch from f675fdc to fa879b0 Compare October 24, 2026 08:22
@frahlg
frahlg changed the base branch from feat/anonymous-energy-ohttp to main October 24, 2026 09:39
# Conflicts:
#	CLAUDE.md
#	README.md
#	cmd/ftwcloudd/main.go
#	go.mod
#	go.sum
#	internal/server/server.go
#	internal/server/server_test.go
@frahlg
frahlg merged commit f6e1c3e into main Oct 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant