Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ dpp --report-format json input.pcap output.csv > dpp-summary.json
| --- | --- |
| `-s, --silent` | Suppress info-level log output |
| `-f, --format <csv\|parquet\|pq>` | Select output format; stdout output is supported only for `csv` |
| `--report-format <text\|json>` | Select the final process report format; defaults to `text`; `json` cannot be combined with `output_filename = -` |
| `--report-format <text\|json>` | Select the final process report format; defaults to `text`; `json` cannot be combined with `output_filename = -` or `--silent` |
| `--match-timeout-ms <MS>` | Set the DNS query-response match timeout in milliseconds; allowed range is `1..=5000`, default is `1200` |
| `--monotonic-capture` | Assume globally monotonic packet timestamps, enable batched timeout eviction, and abort if a regression is detected |
| `-t, --threads <N>` | Cap the CPU execution budget at a positive `N`, never above available CPUs |
Expand All @@ -171,7 +171,7 @@ provide supported per-core thread pinning through the affinity backend.
| `DPP_FILENAME` | Input PCAP path. Use `-` to read a finite offline PCAP stream from `stdin` |
| `DPP_OUTPUT_FILENAME` | Optional output file path; use `-` for CSV stdout output |
| `DPP_FORMAT` | Output format: `csv`, `parquet`, or `pq` |
| `DPP_REPORT_FORMAT` | Final process report format: `text` or `json`; defaults to `text`; `json` cannot be combined with `DPP_OUTPUT_FILENAME=-` |
| `DPP_REPORT_FORMAT` | Final process report format: `text` or `json`; defaults to `text`; `json` cannot be combined with `DPP_OUTPUT_FILENAME=-` or `DPP_SILENT` |
| `DPP_MATCH_TIMEOUT_MS` | DNS query-response match timeout in milliseconds; allowed range is `1..=5000`, default is `1200` |
| `DPP_MONOTONIC_CAPTURE` | Assume globally monotonic packet timestamps, enable batched timeout eviction, and abort if a regression is detected |
| `DPP_THREADS` | Maximum CPU execution budget when `--threads` is not set; must be a positive integer |
Expand Down Expand Up @@ -353,6 +353,8 @@ type/code values rather than collapsing them to an `Unknown` label.

`--report-format json` or `DPP_REPORT_FORMAT=json` suppresses routine `info`/`warn` reporting and
writes one final summary object to stdout. CSV or Parquet records remain in their own output file.
JSON mode rejects `--silent` and `DPP_SILENT`: silent runs skip peak-RSS sampling, so
`metrics.max_memory_usage_kib` would not be a measured value.

The summary includes these processing and matching-quality metrics:

Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ authoritative processing counters. Oversized-QNAME rejections appear as

Final JSON reporting uses the CLI/config report-format contract. Runtime logging remains
human-readable. Stdout export is CSV-only, rejects JSON reports, and suppresses text reports.
Explicit silent mode (`--silent` or `DPP_SILENT`) also rejects JSON reports, because silent runs do
not start the RSS monitor that measures `metrics.max_memory_usage_kib`.
If stderr itself is a closed pipe, the logger suppresses broken-pipe noise.

### Completion and failure handling
Expand Down
55 changes: 46 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ pub(crate) fn parse_args() -> Result<AppConfig> {
validate_distinct_input_output(&input_source, &output_filename)?;
validate_distinct_key_output(anonymize.as_deref(), &output_filename)?;
validate_output_mode(output_target, format, report_format)?;
let silent_requested = matches.get_flag("silent") || env_silent;
validate_silent_report_format(silent_requested, report_format)?;

let zstd = matches.get_flag("zstd") || env_zstd;
let v2 = matches.get_flag("v2") || env_v2;
let silent = resolve_silent_mode(&matches, env_silent, output_target);
let silent = resolve_silent_mode(silent_requested, output_target);
let affinity = matches.get_flag("affinity") || env_affinity;
let dns_wire_fast_path = matches.get_flag("dns_wire_fast_path") || env_dns_wire_fast_path;
let (allow_fragments, full_fragments) =
Expand Down Expand Up @@ -150,7 +152,7 @@ fn build_cli(version: &'static str) -> Command {
DPP_MONOTONIC_CAPTURE
Set to 'true' to assume globally monotonic packet timestamps, enable batched timeout eviction, and abort on timestamp regressions
DPP_THREADS Maximum CPU execution budget (positive integer, capped to available CPUs; used if --threads is not specified)
DPP_REPORT_FORMAT Final process report format: text or json (used if --report-format is not specified; json cannot be combined with stdout output)
DPP_REPORT_FORMAT Final process report format: text or json (used if --report-format is not specified; json cannot be combined with stdout output or silent mode)
DPP_MATCH_TIMEOUT_MS DNS match timeout in milliseconds; allowed range is 1..=5000, default is 1200
DPP_BONDED=N Set IO channel capacity in records; internally rounded up to batched messages of up to 1024 records; 0 uses the safe default bounded capacity
DPP_FORMAT Output format: csv, parquet, or pq (used if --format option is not specified)
Expand Down Expand Up @@ -194,7 +196,7 @@ LICENSE INFORMATION:
.arg(
Arg::new("report_format")
.long("report-format")
.help("Final process report format: text or json; json cannot be combined with stdout output")
.help("Final process report format: text or json; json cannot be combined with stdout output or --silent")
.value_name("text|json")
.num_args(1)
.value_parser(["text", "json"]),
Expand Down Expand Up @@ -440,12 +442,8 @@ fn resolve_max_dns_compression_jumps(
}
}

fn resolve_silent_mode(
matches: &ArgMatches,
env_silent: bool,
output_target: OutputTarget,
) -> bool {
matches.get_flag("silent") || env_silent || matches!(output_target, OutputTarget::Stdout)
fn resolve_silent_mode(silent_requested: bool, output_target: OutputTarget) -> bool {
silent_requested || matches!(output_target, OutputTarget::Stdout)
}

fn validate_parquet_only_flags(format: OutputFormat, zstd: bool, v2: bool) -> Result<()> {
Expand Down Expand Up @@ -559,6 +557,18 @@ fn validate_output_mode(
Ok(())
}

/// Silent runs skip peak-RSS sampling, so a JSON report could not carry a measured value.
fn validate_silent_report_format(
silent_requested: bool,
report_format: ReportFormat,
) -> Result<()> {
if silent_requested && matches!(report_format, ReportFormat::Json) {
bail!("'--report-format json' cannot be combined with '--silent' (or DPP_SILENT).");
}

Ok(())
}

fn validate_input_source(input_source: &InputSource) -> Result<()> {
match input_source {
InputSource::Stdin => validate_stdin_input(),
Expand Down Expand Up @@ -1280,4 +1290,31 @@ mod tests {
validate_output_mode(OutputTarget::Stdout, OutputFormat::Csv, ReportFormat::Text)
.expect("text report format remains valid for stdout output");
}

#[test]
fn json_report_format_is_rejected_in_silent_mode() {
let error = validate_silent_report_format(true, ReportFormat::Json)
.expect_err("json report format must be rejected in silent mode");

assert!(
error.to_string().contains(
"'--report-format json' cannot be combined with '--silent' (or DPP_SILENT)"
)
);
}

#[test]
fn silent_mode_restricts_only_json_reports() {
validate_silent_report_format(true, ReportFormat::Text)
.expect("text report format remains valid in silent mode");
validate_silent_report_format(false, ReportFormat::Json)
.expect("json report format remains valid without silent mode");
}

#[test]
fn stdout_output_implies_silent_mode() {
assert!(resolve_silent_mode(false, OutputTarget::Stdout));
assert!(resolve_silent_mode(true, OutputTarget::File));
assert!(!resolve_silent_mode(false, OutputTarget::File));
}
}
49 changes: 47 additions & 2 deletions tests/end_to_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fn run_fragment_reuse_capture(
fs::write(&input_path, capture).expect("fragment reuse capture written");
let mut command = Command::new(dpp_binary());
command
.args(["-s", "-f", "csv", "--full-fragments", "--threads"])
.args(["-f", "csv", "--full-fragments", "--threads"])
.arg(threads.to_string())
.args(["--report-format", "json"])
.args(extra_args)
Expand Down Expand Up @@ -577,7 +577,7 @@ fn full_ipv4_reassembly_ignores_first_fragment_duplicate_after_completion() {
.expect("fragmented duplicate test pcap written");

let result = Command::new(dpp_binary())
.args(["-s", "--full-fragments", "--report-format", "json"])
.args(["--full-fragments", "--report-format", "json"])
.arg(&input_path)
.arg(&output_path)
.output()
Expand Down Expand Up @@ -982,6 +982,51 @@ fn thread_cap_changes_reported_execution_budget() {
fs::remove_file(&output_path).expect("remove output csv");
}

#[test]
fn json_report_is_rejected_in_silent_mode() {
let input_path = temp_test_path("silent-json-report", "pcap");
let output_path = temp_test_path("silent-json-report", "csv");
fs::write(&input_path, classic_pcap_bytes(&[])).expect("empty pcap written");

for (args, env_silent) in [
(&["-s", "--report-format", "json"][..], None),
(&["--report-format", "json"][..], Some("true")),
] {
let mut command = Command::new(dpp_binary());
command
.args(args)
.env_remove("DPP_SILENT")
.env_remove("DPP_REPORT_FORMAT");
if let Some(value) = env_silent {
command.env("DPP_SILENT", value);
}
let output = command
.arg(&input_path)
.arg(&output_path)
.output()
.expect("dpp executed");

assert!(
!output.status.success(),
"silent JSON run must be rejected: {args:?}, DPP_SILENT={env_silent:?}"
);
assert!(
String::from_utf8_lossy(&output.stderr).contains(
"'--report-format json' cannot be combined with '--silent' (or DPP_SILENT)"
),
"unexpected stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stdout.is_empty(), "no JSON report may be emitted");
assert!(
!output_path.exists(),
"output must not be created when CLI validation fails"
);
}

fs::remove_file(&input_path).expect("remove input pcap");
}

#[test]
fn oversized_wire_qname_is_rejected_and_reported() {
let input_path = temp_test_path("oversized-wire-qname", "pcap");
Expand Down
Loading