Skip to content

FWS-9 — Move ops logger output from stderr to stdout (stream separation from audit) #100

@initializ-mk

Description

@initializ-mk

Part of the Forge backlog. Effort: XS (~0.5 engineer-day). Risk: low (single production call site, no API change). Depends on: nothing. Companion to FWS-7 (#95) — independent of it in code, but operational value is fully realized once the dedicated audit sink lands.

Scope

Route the structured ops logger (coreruntime.JSONLogger) output for forge run / forge serve to stdout instead of stderr, so stderr can carry audit NDJSON as a single-stream concern that container log collectors can route to a SIEM pipeline without parsing the payload.

One production call site changes — forge-cli/runtime/runner.go:123:

// Before
logger := coreruntime.NewJSONLogger(os.Stderr, cfg.Verbose)

// After
logger := coreruntime.NewJSONLogger(os.Stdout, cfg.Verbose)

That's the whole code change. The remainder of this issue is the rationale, the scoping guardrails, and the docs / acceptance criteria.

Why this matters

Today Forge emits both ops logs (request lines, startup banners, error logs from r.logger.Info / .Warn / .Error) and audit NDJSON on stderr. A SIEM pipeline that wants only audit records cannot split by stream alone — it has to filter every line by the presence of the event JSON field. That works but:

  • Adds parsing cost at the log collector.
  • Risks accidental rule drift if ops logs ever start carrying an event field.
  • Couples SIEM rule lifecycle to Forge's ops-log schema, which is meant to be free to evolve.

Moving ops logs to stdout makes the split clean at the stream level — exactly what stdout / stderr were designed for. After this change, the operational shape is:

Stream Carries Consumer
stdout Ops logs (forge run request lines, startup banners, errors) Container log collector / local debugging
stderr Audit NDJSON SIEM pipeline (today, until FWS-7 ships the dedicated sink)
UDS / HTTP sink (FWS-7) Audit NDJSON (primary, once FWS-7 lands) initializ platform sidecar / SIEM

This stays correct whether or not FWS-7 has shipped. Before FWS-7: stderr is the SIEM source. After FWS-7: dedicated sink is the SIEM source and stderr is the audit fallback. In both cases, ops logs are no longer mingled with audit, and that's the win.

Where this fits in the codebase

Inventory (already done):

Component Path Notes
JSONLogger definition forge-core/runtime/logger.go Takes io.Writer — no changes needed here, already writer-agnostic
Production construction site forge-cli/runtime/runner.go:123 The one line that needs changing
Test construction sites 8 sites in forge-cli/runtime/*_test.go All use bytes.Buffer{} / discardWriter{} / nil — unaffected by stream change
Audit logger forge-cli/runtime/runner.go Still goes to stderr today; FWS-7 will make this configurable. Do not change in this issue.

The construction-site change does not affect any other module — JSONLogger is already abstracted behind the Logger interface.

Out of scope

  • Interactive CLI commands (forge init, forge build, forge channel, forge serve start/stop). User-facing messages via fmt.Fprintf(os.Stderr, ...) for warnings / errors / banners are conventional CLI behavior and should stay on stderr. Only the long-lived forge run / forge serve ops logger is in scope.
  • Audit logger destination. Stays on stderr in this issue. FWS-7 (FWS-7 — Audit event export capability (Unix Domain Socket sink + HTTP fallback) #95) is the work that makes audit destination configurable. This issue does not block on FWS-7 nor require any change to the audit sink layer.
  • Log routing in tests. Tests pass bytes.Buffer{} or discardWriter{} to NewJSONLogger — they don't care about the production default.
  • fmt.Fprintf(os.Stderr, ...) calls in forge-cli/cmd/*.go for warnings/errors in interactive commands. These are CLI UX, not server ops logs.

Deliverables

  • Change forge-cli/runtime/runner.go:123 from os.Stderr to os.Stdout.
  • Add a one-paragraph note in docs/security/audit-logging.md under a new "Streams" subsection explaining the split: stdout = ops logs, stderr = audit (or the FWS-7 sink once it lands).
  • CHANGELOG entry under Changed: "forge run ops logger now writes to stdout (was stderr); audit NDJSON continues to write to stderr. Operators consuming ops logs via stderr should update their log collector configuration."
  • Update existing operator-facing docs (docs/deployment/*.md) if any of them currently say "ops logs go to stderr."

Risks

  • Operator migration cost. Anyone currently parsing forge run 2> ops.log for ops logs needs to switch to > ops.log. The CHANGELOG entry must call this out clearly, and the docs page should show the new shape. Risk is low because: (a) most production deployments use container log collectors that capture both streams already, (b) the audit-on-stderr behavior is unchanged, and (c) forge run is a server — not a tool whose stdout people pipe into another command.
  • Tests on the operator side that assert ops-log content via stderr capture. Out-of-tree concern; not Forge's problem. The CHANGELOG note is the migration aid.

Acceptance criteria

  1. forge-cli/runtime/runner.go:123 writes ops logs to stdout.
  2. forge run produces ops log entries on stdout and audit NDJSON entries on stderr — verifiable manually with forge run > ops.ndjson 2> audit.ndjson and confirming each file contains only the expected event kind.
  3. Existing forge-cli/runtime/*_test.go suite passes unchanged (tests use buffers, not OS streams).
  4. CHANGELOG entry is clear about the stream migration and the operator-facing impact.
  5. docs/security/audit-logging.md "Streams" note explains the post-change shape and references FWS-7 as the next step.

Anti-patterns to avoid

  • Do not also move audit to stdout. That defeats the stream split.
  • Do not add a flag to choose the destination. The whole point is to standardize on stream-based separation; a flag re-introduces operator confusion.
  • Do not change the JSONLogger API. It already takes io.Writer — the change is purely in the construction site.
  • Do not touch the interactive CLI command output (forge init wizard, forge build errors, etc.). Those are UX messages, not ops logs.

Relationship to FWS-7 (#95)

FWS-7 ships a dedicated audit export sink (UDS or localhost HTTP). Once that lands, the recommended operational shape becomes:

  • Audit NDJSON primary path → dedicated sink (consumed by initializ platform sidecar or customer SIEM).
  • Audit NDJSON fallback path → stderr (degraded-mode safety net).
  • Ops logs → stdout (this issue).

This issue is independent of FWS-7 in code — it can land before, after, or alongside. The operational value is realized in both states; FWS-7 just adds the dedicated-sink option on top.

Files expected to change

File Change
forge-cli/runtime/runner.go One-line: os.Stderros.Stdout in NewJSONLogger construction
docs/security/audit-logging.md New "Streams" subsection
CHANGELOG.md Changed entry under Unreleased
docs/deployment/*.md (if applicable) Update any text that says "ops logs go to stderr"

That's the entire surface. No new files, no API changes, no test changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions