Skip to content

refactor(metering): split into subpackages + pluggable backend#62

Merged
CMGS merged 4 commits into
masterfrom
refactor/metering-subpackages
May 22, 2026
Merged

refactor(metering): split into subpackages + pluggable backend#62
CMGS merged 4 commits into
masterfrom
refactor/metering-subpackages

Conversation

@CMGS
Copy link
Copy Markdown
Contributor

@CMGS CMGS commented May 22, 2026

Summary

Reshape `metering/` to match cocoon's interface-in-main + backend-in-subpackage pattern (hypervisor / snapshot / images / network). Backend is now configurable via `config.MeteringConfig` instead of hard-coded in `cmd/core`.

New layout

```
config/
└── metering.go (MeteringConfig / FileMeteringConfig)
metering/
├── metering.go (Recorder interface, Entry/Shape/Kind/Reason, NopRecorder)
├── file/ (production: JSONL append on disk)
├── stderr/ (dev/debug: JSON line to stderr)
└── capture/ (testing-only: buffered with Entries/Reset)
```

Each subpackage exposes `Recorder` struct + `New` ctor + compile-time `var _ metering.Recorder = (*Recorder)(nil)`.

Config

```jsonc
// default — equivalent to v0.4.x file behavior, no config change needed
{}

// explicit
{"metering": {"backend": "file", "file": {"path": ""}}}

// disable
{"metering": {"backend": "nop"}}

// dev
{"metering": {"backend": "stderr"}}
```

Factory

`cmd/core/metering.go` switches over `conf.Metering.Backend`; unknown backend warns + falls back to NopRecorder. Adding a new backend (HTTP webhook, Pub/Sub, Kafka, …) is one new subpackage exposing a Recorder + adding one case to the switch.

Other notable changes

  • `file.New` no longer takes ctx (unused); returns `(*Recorder, error)` so the factory owns the NopRecorder fallback rather than the backend silently downgrading.
  • `capture` package doc explicitly flags itself as testing-only; `Entries` / `Reset` retained as testing helpers on top of the Recorder contract (same dual-access pattern as bytes.Buffer being io.Writer + .Bytes()).
  • Tests using `&metering.CaptureRecorder{}` migrated to `meteringcapture.New()` (3 test files, ~10 sites).

Backward compat

  • Zero config (`Metering: MeteringConfig{}`) keeps v0.4.x behavior — file at `/metering/ledger.jsonl`. Phase 2 logrotate/upload pipeline unaffected.
  • No external API consumers (cocoon is a binary, not a library).

Test plan

  • `make fmt-check && make lint && go test -race ./...` — 24/24 packages green, lint 0 (darwin + linux), fmt 0
  • AST layout audit — 0 violations (single const/var block, public-above-private, interface checks at top, struct methods grouped)
  • `cmd/core/metering_test.go` covers all 5 backend selection paths (default/file/nop/stderr/unknown)
  • Each subpackage carries its own round-trip + concurrent emit tests

CMGS added 4 commits May 23, 2026 02:20
…uggable backend

- metering/{file,stderr,capture}/ subpackages mirror hypervisor/snapshot
  pattern: interface lives in main metering package, backends are
  subpackage Recorder types exposing a New ctor.
- config.MeteringConfig selects the backend ("file" default, "nop",
  "stderr"); empty config keeps v0.4.x behavior.
- cmd/core/metering.go switches over conf.Metering.Backend instead of
  hard-coding NewFileRecorder.
- stderr/Recorder is a new debug backend; capture/Recorder keeps the
  Entries/Reset test helpers (package doc flags it as testing-only).
- file/Recorder.New no longer takes ctx (unused) and surfaces open
  errors so the cmd/core factory owns the NopRecorder fallback.
- config.MeteringBackend typed enum + constants (MeteringFile/Nop/Stderr)
  mirror the HypervisorType pattern; cmd/core switch now uses typed cases.
- MeteringConfig.Validate() rejects unknown backends at startup (hooked
  into config.Config.Validate) instead of warn-and-nop at first emit.
- stderr.Recorder.Emit uses Write with appended newline (matches
  file.Recorder.Emit shape, drops fmt.Fprintln + nolint:errcheck).
- Tighten godocs: stderr.Emit doc removed (impl is obvious), capture
  package doc collapsed to one sentence, MeteringRecorder godoc trimmed
  to the lazy-init contract.
Entry now implements io.WriterTo; file and stderr recorders share the
same encoding path instead of each calling json.Marshal + appending \n.
Wire-format changes (encoder swap, framing, compression) are now one
diff in metering.go rather than scattered across backends.

file.Emit collapses two log paths (marshal-error vs write-error) into
one — the wrapped error already carries the cause, and downstream cares
only that the entry failed.
- WriteTo: "writes one JSONL record" (was: wire-format + downstream tools).
- file.Recorder / file.Emit: drop the JSON-encoding narrative now that
  it lives on Entry.WriteTo; keep only the file-specific WHY (O_APPEND
  atomicity, swallow rationale).
- stderr.Recorder: routes to os.Stderr instead of restating the wire
  format.
- config: drop v0.4.x backstory; the default-behavior sentence carries
  the same information.
- metering_test: drop the comment that just paraphrased the test name.
@CMGS CMGS merged commit 49818f8 into master May 22, 2026
4 checks passed
@CMGS CMGS deleted the refactor/metering-subpackages branch May 22, 2026 18:53
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