Skip to content

feat: CLI quick wins — help, version, colors, exit codes#68

Merged
strawgate merged 3 commits into
masterfrom
feat/cli-improvements
Mar 29, 2026
Merged

feat: CLI quick wins — help, version, colors, exit codes#68
strawgate merged 3 commits into
masterfrom
feat/cli-improvements

Conversation

@strawgate
Copy link
Copy Markdown
Owner

Summary

Polish the logfwd CLI with the basics every good CLI tool has.

New flags:

  • --help / -h — structured usage with USAGE, OPTIONS, EXIT CODES sections
  • --version / -V — prints version from Cargo.toml
  • -c shorthand for --config
  • --check alias for --validate

Colored output (ANSI, no dependencies):

  • Green for success (config ok, ready, logfwd running)
  • Red for errors (error: config I/O error: ...)
  • Yellow for warnings (warning: meter provider shutdown)
  • Dim for secondary info (version, diagnostics URL, SQL transform)
  • Respects NO_COLOR env var and non-TTY stderr

Consistent exit codes:

  • 0 = success
  • 1 = configuration error (bad YAML, missing file, invalid SQL)
  • 2 = runtime error

Startup summary — shows pipeline topology on boot:

logfwd v0.1.0
  pipeline default: 1 input(s) -> SELECT * FROM logs WHERE level_str = 'ERROR' -> 2 output(s)
  ready: default
  diagnostics: http://127.0.0.1:9090
logfwd running (1 pipeline(s))

Better error handling:

  • Config load errors print cleanly (no Rust panic backtraces)
  • Pipeline build errors caught at startup with context
  • --generate-json validates num_lines parse

Test plan

  • cargo clippy -p logfwd -- -D warnings clean
  • cargo fmt --check clean
  • logfwd --help shows usage, exits 0
  • logfwd --version shows logfwd 0.1.0, exits 0
  • logfwd --config bad.yaml prints error, exits 1
  • logfwd --config good.yaml --check prints config ok, exits 0
  • logfwd --config good.yaml --dry-run shows summary + dry run ok, exits 0
  • logfwd --bogus prints error + help hint, exits 1

🤖 Generated with Claude Code

…mary

- --help / -h: structured usage with sections (USAGE, OPTIONS, EXIT CODES)
- --version / -V: prints version from Cargo.toml
- -c shorthand for --config
- --check alias for --validate (matches common CLI conventions)
- Colored output: green for success, red for errors, yellow for warnings,
  dim for secondary info. Respects NO_COLOR env var and non-TTY stderr.
- Consistent exit codes: 0=success, 1=config error, 2=runtime error
- Config errors caught and printed cleanly (no Rust panic traces)
- Pipeline build errors caught at startup with colored context
- Startup summary shows pipeline topology: inputs -> SQL -> outputs
- Structured code: commands split into cmd_config/cmd_blackhole/cmd_generate_json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 29, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b388cc93-fb2e-4d24-9bbd-cf7ccbac5070

📥 Commits

Reviewing files that changed from the base of the PR and between f3efccb and 560d476.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • crates/logfwd-config/src/lib.rs
  • crates/logfwd-output/Cargo.toml
  • crates/logfwd-output/src/lib.rs
  • crates/logfwd-output/src/stdout.rs
  • crates/logfwd/src/main.rs

Walkthrough

Added libc dependencies to crates/logfwd and crates/logfwd-output. crates/logfwd/src/main.rs was refactored to a command-dispatch CLI (help, version, config, blackhole, generate-json, validate/dry-run), uses explicit exit codes, colored terminal helpers, and separates pipeline validation from execution; diagnostics server startup and threaded pipeline runtime/shutdown were reorganized. crates/logfwd-config gains a new Format::Console variant. crates/logfwd-output adds StdoutFormat::Console, a color field on StdoutSink, TTY/NO_COLOR detection, and a write_console path for colored human-readable output.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/logfwd/Cargo.toml (1)

10-22: 🛠️ Refactor suggestion | 🟠 Major

Add the net feature to Tokio.

crates/logfwd/src/main.rs calls tokio::runtime::Builder::enable_io() (line 418), which requires the net feature. Currently, this crate only declares rt-multi-thread and time, relying on transitive feature unification to compile.

♻️ Proposed fix
-tokio = { version = "1", features = ["rt-multi-thread", "time"] }
+tokio = { version = "1", features = ["rt-multi-thread", "time", "net"] }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/logfwd/Cargo.toml` around lines 10 - 22, The Tokio dependency in
Cargo.toml is missing the "net" feature required by
tokio::runtime::Builder::enable_io() (used in crates/logfwd/src/main.rs), so
update the tokio entry in Cargo.toml to include "net" alongside
"rt-multi-thread" and "time" (i.e., add "net" to the features array) to ensure
compile-time availability without relying on transitive feature unification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/logfwd/src/main.rs`:
- Around line 153-160: The validate-only path currently returns immediately
after Config::load, skipping pipeline-level validation; instead, iterate over
config.pipelines and call Pipeline::from_config (or the existing pipeline
construction/validation function) for each pipeline to surface SQL/input/output
wiring errors, collect any errors and return a non-zero Result on failure, and
only print the "config ok" message after all Pipeline::from_config validations
succeed; update the validate_only branch in main/run to perform that loop and
error handling.
- Around line 141-143: The current extraction of validate_only and dry_run using
args.iter().any lets unknown tokens silently pass; change to explicitly parse
the tail slice (e.g., let extra = &args[3..]) and iterate matching allowed flags
only, setting validate_only and dry_run when you see "--validate"|"--check" and
"--dry-run" respectively, and return/exit with EXIT_CONFIG (or call the same
error path used elsewhere) if any unknown token is encountered; update
references to args, config_path, validate_only, dry_run to use this parsed
result so typos like "--dry-ru" produce an error instead of a real run.
- Around line 260-274: The sibling threads never see shutdown flipped and their
JoinHandle results are ignored; change the logic around pipelines/main_pipeline
so that after main_pipe.run(&shutdown) returns (on both Ok and Err) you set the
shared shutdown flag to true (e.g., shutdown.store(true, Ordering::SeqCst) or
shutdown.set(true) depending on the shutdown type) and then join all worker
threads, collecting each thread::JoinHandle result and the inner io::Result from
pipeline.run(&sd) and propagate any errors (map panics/join errors and
io::Result::Err into a returned Err) so non-main pipeline failures affect the
process exit code.
- Around line 242-248: DiagnosticsServer currently spawns the thread in
DiagnosticsServer::start() but defers the bind (which may panic) to the
background, so change DiagnosticsServer::start() to perform the bind
synchronously and return Result<JoinHandle<()>, E> (or similar) instead of
unconditionally spawning; then in main.rs replace Some(server.start()) with
matching on server.start(): on Ok(handle) keep Some(handle) and print the
diagnostics URL, on Err(e) log the bind error and abort/startup failure. Update
the DiagnosticsServer::start signature and callers (including the place that
previously expected a JoinHandle) to handle the Result so bind failures are
surfaced before readiness is advertised.
- Around line 200-203: The parse failure for args[2] should trigger the
configuration exit path instead of being wrapped as an io::Error and bubbling to
main (which causes EXIT_RUNTIME); change the `.parse().map_err(|e|
io::Error::other(...))?` handling for num_lines so that on parse error you
return or exit with the configuration error/exit code (use the existing
EXIT_CONFIG constant or the program's Config/Argument error variant) rather than
creating an io::Error — locate the parsing code that sets `num_lines` (the
`.parse()` call for args[2]) and adjust it to either call
`process::exit(EXIT_CONFIG)` on parse failure or return the appropriate
config-error result from main so the program returns the documented
config/argument exit code before calling `generate_json_log_file`.

---

Outside diff comments:
In `@crates/logfwd/Cargo.toml`:
- Around line 10-22: The Tokio dependency in Cargo.toml is missing the "net"
feature required by tokio::runtime::Builder::enable_io() (used in
crates/logfwd/src/main.rs), so update the tokio entry in Cargo.toml to include
"net" alongside "rt-multi-thread" and "time" (i.e., add "net" to the features
array) to ensure compile-time availability without relying on transitive feature
unification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf5372f3-0a75-49a7-8354-088ac312be4b

📥 Commits

Reviewing files that changed from the base of the PR and between 17df19c and f3efccb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • crates/logfwd/Cargo.toml
  • crates/logfwd/src/main.rs

Comment thread crates/logfwd/src/main.rs Outdated
Comment thread crates/logfwd/src/main.rs Outdated
Comment thread crates/logfwd/src/main.rs Outdated
Comment thread crates/logfwd/src/main.rs
Comment on lines +242 to +248
let _diag_handle = if let Some(ref addr) = config.server.diagnostics {
let mut server = DiagnosticsServer::new(addr);
for p in &pipelines {
server.add_pipeline(Arc::clone(p.metrics()));
}
eprintln!(" {}diagnostics{}: http://{addr}", dim(), reset());
Some(server.start())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface diagnostics bind failures before advertising readiness.

DiagnosticsServer::start() only spawns the thread; the actual bind still happens later in crates/logfwd-core/src/diagnostics.rs, where startup uses expect(...). If the port is busy, this path logs diagnostics: and keeps running while the background thread panics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/logfwd/src/main.rs` around lines 242 - 248, DiagnosticsServer
currently spawns the thread in DiagnosticsServer::start() but defers the bind
(which may panic) to the background, so change DiagnosticsServer::start() to
perform the bind synchronously and return Result<JoinHandle<()>, E> (or similar)
instead of unconditionally spawning; then in main.rs replace
Some(server.start()) with matching on server.start(): on Ok(handle) keep
Some(handle) and print the diagnostics URL, on Err(e) log the bind error and
abort/startup failure. Update the DiagnosticsServer::start signature and callers
(including the place that previously expected a JoinHandle) to handle the Result
so bind failures are surfaced before readiness is advertised.

Comment thread crates/logfwd/src/main.rs
Comment on lines +260 to +274
let mut handles = Vec::new();
let main_pipeline = pipelines.pop();

for mut pipeline in pipelines {
let sd = shutdown.clone();
handles.push(std::thread::spawn(move || pipeline.run(&sd)));
}

if let Some(mut main_pipe) = main_pipeline {
main_pipe.run(&shutdown)?;
}

for h in handles {
let _ = h.join();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Propagate shutdown and worker errors across pipelines.

shutdown is never flipped to true, so when the main pipeline returns normally the sibling threads keep running and join() can block forever. The worker JoinHandle<io::Result<()>> results are also discarded, so non-main pipeline failures never affect the process exit code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/logfwd/src/main.rs` around lines 260 - 274, The sibling threads never
see shutdown flipped and their JoinHandle results are ignored; change the logic
around pipelines/main_pipeline so that after main_pipe.run(&shutdown) returns
(on both Ok and Err) you set the shared shutdown flag to true (e.g.,
shutdown.store(true, Ordering::SeqCst) or shutdown.set(true) depending on the
shutdown type) and then join all worker threads, collecting each
thread::JoinHandle result and the inner io::Result from pipeline.run(&sd) and
propagate any errors (map panics/join errors and io::Result::Err into a returned
Err) so non-main pipeline failures affect the process exit code.

strawgate and others added 2 commits March 29, 2026 15:32
New `format: console` option for stdout output. Renders logs as:

  10:30:00.000Z  INFO   request handled GET /api/v1/users/10000  duration_ms=1 service=myapp
  10:30:00.001Z  DEBUG  request handled GET /api/v1/orders/10007  duration_ms=14 ...
  10:30:00.002Z  WARN   request handled GET /api/v2/products/10014  duration_ms=27 ...
  10:30:00.003Z  ERROR  request handled GET /health/10021  duration_ms=40 ...

- Timestamp shown as time-only (strips date prefix)
- Level colored: green=INFO, yellow=WARN, red=ERROR, dim=DEBUG
- Message displayed prominently
- Remaining fields as dim key=value pairs
- _raw field excluded (redundant with parsed fields)
- Respects NO_COLOR and non-TTY stdout

Config:
  output:
    type: stdout
    format: console

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…es, exit codes

- Reject unknown flags after --config (typos like --dry-ru now error)
- --validate and --check now build pipelines via Pipeline::from_config
  to catch SQL/wiring errors (not just YAML parsing)
- --generate-json parse error exits with EXIT_CONFIG (1) not EXIT_RUNTIME (2)
- Removed unused dry_run parameter from run_pipelines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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