Skip to content

Add the bitbang-metrics-dump command and a build/test CI workflow - #2

Merged
richlegrand merged 1 commit into
richlegrand:mainfrom
kodareef5:fix/metrics-dump-build
Aug 15, 2026
Merged

richlegrand merged 1 commit into
richlegrand:mainfrom
kodareef5:fix/metrics-dump-build

Conversation

@kodareef5

Copy link
Copy Markdown
Contributor

What & why

deploy/upload_production cross-compiles ./cmd/bitbang-metrics-dump and ships it beside signaling-go, and the README + production.env.example document it. But the command source was never committed, so the documented production deploy fails at build time:

stat .../cmd/bitbang-metrics-dump: directory not found

Two things kept this hidden: the test deploy (upload_test) only builds ./cmd/signaling, and there was no CI. The source was also swallowed by an unanchored bitbang-metrics-dump line in .gitignore, which matches the cmd/bitbang-metrics-dump/ directory as well as the intended root binary, so it could not be committed in the first place.

What's in this PR

The command the docs already describe:

  • no args: the latest snapshot, as a JSON object
  • -history N: the last N snapshots, as a JSON array, newest first
  • -db PATH: the database (defaults to $METRICS_PATH)

It reads the same SQLite database the recorder writes (internal/metrics) via the pure-Go modernc.org/sqlite driver already in the module, so it builds under CGO_ENABLED=0 like the deploy expects. No new dependencies. The row type embeds metrics.Snapshot, so the counter keys are the ones /status emits (ts and the two gauges are added on top; /status also carries version/protocol/active_codes, so this is a subset). It opens the database read-only (mode=ro plus PRAGMA query_only): it never creates the database and never modifies a recorded row.

Deploy realism: the binary ships to /opt/bitbang and METRICS_PATH is set only in the systemd unit, so a bare bitbang-metrics-dump on the box would hit "command not found" (or, with the full path, "no database path"). The README and production.env.example now show the explicit on-server invocation.

Tests run through the real recorder (so they fail if the schema drifts) and cover the command end to end: object vs. array vs. null/[] output, the $METRICS_PATH fallback, missing-path and negative-history errors, write rejection, special-character and relative database paths, and the JSON-key contract.

.gitignore anchors the two build-artifact patterns to the module root, so a source directory can no longer collide with an ignored binary.

CI (new): go vet, go test, and the two deploy cross-builds, so a missing build target fails in CI instead of at deploy time.

Verification

$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/bitbang-metrics-dump ./cmd/bitbang-metrics-dump   # OK
$ go vet ./...     # clean
$ go test ./...    # pass (14 tests in the new package)

Example output:

{
  "ts": "2026-07-31T00:05:00Z",
  "devices": 4,
  "clients": 6,
  "connection_requests_total": 200,
  "connections_direct_total": 120,
  "connections_relay_total": 60,
  "connections_tcp_relay_total": 10,
  "connections_failed_total": 10
}

One question: is bitbang-metrics-dump meant to exist (this PR assumes so, since the deploy and docs call for it), or was it dropped on purpose? If it was dropped, I can instead remove the references; the CI check stands either way.

deploy/upload_production cross-compiles ./cmd/bitbang-metrics-dump and
ships it next to signaling-go, and the README and production.env.example
document its use. But the command source was never committed, so the
production deploy fails at build time:

    stat .../cmd/bitbang-metrics-dump: directory not found

Two things hid this: the test deploy only builds ./cmd/signaling, and
there was no CI. An unanchored `bitbang-metrics-dump` line in .gitignore
also matched the cmd/bitbang-metrics-dump/ source directory, not just the
intended root binary, so the source could not be committed in the first
place.

The command reads the SQLite database the recorder writes
(internal/metrics), via the modernc.org/sqlite driver already in the
module (so CGO_ENABLED=0 builds work), and prints snapshot rows as JSON:

  - no args:    the latest snapshot, as a JSON object
  - -history N: the last N snapshots, as a JSON array, newest first
  - -db PATH:   the database (defaults to $METRICS_PATH)

The row type embeds metrics.Snapshot to reuse its JSON tags. It opens the
database read-only (mode=ro plus PRAGMA query_only): it never creates the
database and never modifies a recorded row. Tests run through the real
recorder, so they fail if the schema drifts, and exercise the command end
to end including its read-only guarantees.

The deploy ships the binary to /opt/bitbang and sets METRICS_PATH only in
the systemd unit, so the README and production.env.example show the
explicit invocation rather than a bare command that is neither on PATH nor
handed the database path.

CI runs go vet, go test, and the two deploy cross-builds, so a missing
build target fails in CI instead of at deploy time.
@richlegrand

Copy link
Copy Markdown
Owner

Hi @kodareef5, many thanks for this -- the metrics feature was incorporated mostly because I wanted to get some simple metrics of direct-vs-relay numbers. I'll give this a good look when I get back from travel.

@richlegrand

Copy link
Copy Markdown
Owner

Thanks @kodareef5, and sorry this sat -- you were right about all of it, including the part
that was hardest to see from outside.

I verified the diagnosis from a clean checkout:

$ git archive main | tar -x -C /tmp/clean
$ cd /tmp/clean/bitbang-server && go build ./cmd/bitbang-metrics-dump
stat .../cmd/bitbang-metrics-dump: directory not found

git check-ignore -v confirms .gitignore:11 was matching the directory, not just the
built binary. So the documented production deploy has been broken for anyone but me, and I
would not have found it on this machine -- a clean checkout is the one thing I never do
here.

One thing you could not have known, and it explains why the docs described the command --I had already written it. :)
It has been sitting untracked in my working tree the whole time, which is exactly why
git add -A never complained and why the deploy kept working locally. You reimplemented a file
you had no way to see.

Having compared them, I am taking yours, and the deciding reason is not the one I expected:

Mine opens the database read-write. A mistyped path makes SQLite create an empty
database rather than fail. Yours uses mode=ro plus PRAGMA query_only, so it structurally
cannot create or modify anything. On a production metrics database that is the difference
that matters, and I would not have noticed it if you had not written the alternative next to
mine.

The tests are the other half. Mine has none -- it could not, being untracked -- and yours run
through the real recorder, so schema drift fails the build instead of surfacing as confusing
output months later.

Merging as-is

Not holding this for anything. It has waited long enough, and it fixes a deploy that is
broken on main right now.

One thing I would like to change afterward, and I would rather ask than quietly rewrite your
code: could -history emit oldest-first? Mine sorts ts DESC and then reverses, so the
output reads chronologically, which is what I want when eyeballing a trend on the box.
Newest-first is the more defensible default in the abstract; chronological is what I actually
use this for. Say the word and I will make the change myself post-merge, or push it here if
you would rather it be yours -- and if you have a reason for the other order, I would rather
hear it and keep newest-first.

Nothing else. The -db flag and the object/array shape are better than my
positional-argument-and-NDJSON version, and I will adjust my habits rather than ask you to
adjust the code.

On the CI

Good instinct pointing the cross-build step at the same commands upload_production uses --
that is the step that would have caught this, and it is the reason to have it rather than a
generic build.

One addition: check-latest: true alongside go-version-file. Without it the runner's
cached Go satisfies the constraint and never updates, so a standard-library advisory turns
the build red until someone bumps it by hand. That happened to me in bitbang-cli last week,
and worse, the release workflow had the same gap -- it would have shipped binaries built
against a standard library with four reachable advisories.

Also a heads-up for whenever you rebase: @Brumbelow's browser flow-control work in #4 adds
node --test web_test/, which will want a step in this workflow once both land. No action
needed now; just so the two of you are not surprised by each other.

Thanks again. A broken deploy path plus the reason it was invisible plus a test that keeps it
from recurring is a properly complete piece of work.

@richlegrand richlegrand reopened this Aug 15, 2026
@richlegrand
richlegrand merged commit fcdf479 into richlegrand:main Aug 15, 2026
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.

2 participants