Skip to content

feat: layered text output format (render::text + --format text) - #333

Merged
digizeph merged 4 commits into
mainfrom
feature/render-text-format
Aug 21, 2026
Merged

feat: layered text output format (render::text + --format text)#333
digizeph merged 4 commits into
mainfrom
feature/render-text-format

Conversation

@digizeph

Copy link
Copy Markdown
Member

Summary

Adds a layered, human-readable text output format for MRT records: a new render module in the library (render::text::format_record) and a --format text variant in the CLI. One indented block per record — session context, prefixes, every path attribute, and RFC 7606 findings. Inspired by bgpdump's human-readable output; the format itself is our own design, built from the crate's existing Display vocabulary rather than replicating bgpdump byte-for-byte.

  • render::text::format_record(&MrtRecord) -> String — a pure function of the record (no iterators, no I/O). Covers BGP4MP messages and state changes, legacy type-5 records, TABLE_DUMP / TABLE_DUMP_V2 RIB entries, and the peer index table. UPDATE blocks fold MP_REACH/MP_UNREACH prefixes into the ANNOUNCED:/WITHDRAWN: sections so routes are never silently dropped from the transcript, and validation findings render under WARNINGS:.
  • CLI: --format text — record-level; implies --level records. All other formats are elem-level, documented on the enum variants (and in --help).
  • Filter semantics made explicit: filters match on the elem projection, so records producing no elems (KEEPALIVE, OPEN, NOTIFICATION, state changes) never match and are dropped from record iteration while filters are active. This was previously implicit; it is now documented and each drop emits a debug! line. A matching UPDATE still passes as a whole record (attributes print once — the opposite of elem-level fan-out).

CLI output samples

Updates stream (real 1999 RIS rrc00 capture — legacy type-5 records rendered in full):

$ bgpkit-parser --format text updates.20000102.2014.gz

TIME: 946844090
TYPE: BGP/1
FROM: 195.211.222.254 AS5409
TO: 193.0.0.1 AS12654
UPDATE:
  ANNOUNCED:
    203.154.93.0/24
  ATTRIBUTES:
    ORIGIN: IGP
    AS_PATH: 5409 1280 6453 4618
    NEXT_HOP: 195.211.222.254

TIME: 946844090
TYPE: BGP/7
FROM: 193.0.0.56 AS3333
TO: 193.0.0.1 AS12654
KEEPALIVE:

TIME: 946844091
TYPE: BGP/1
FROM: 195.211.222.254 AS5409
TO: 193.0.0.1 AS12654
UPDATE:
  ANNOUNCED:
    206.224.32.0/19
  ATTRIBUTES:
    ORIGIN: IGP
    AS_PATH: 5409 6427 6461 568
    NEXT_HOP: 195.211.222.254
    ATOMIC_AGGREGATE
    AGGREGATOR: AS568 by 198.26.118.1

With a prefix filter (records carrying a matching elem pass as a whole):

$ bgpkit-parser --format text -p 206.224.32.0/19 updates.20000102.2014.gz

TIME: 946844091
TYPE: BGP/1
FROM: 195.211.222.254 AS5409
TO: 193.0.0.1 AS12654
UPDATE:
  ANNOUNCED:
    206.224.32.0/19
  ATTRIBUTES:
    ORIGIN: IGP
    AS_PATH: 5409 6427 6461 568
    NEXT_HOP: 195.211.222.254
    ATOMIC_AGGREGATE
    AGGREGATOR: AS568 by 198.26.118.1

RIB dump (bview file; legacy TABLE_DUMP batches render entry by entry):

$ bgpkit-parser --format text bview.20000111.0032.gz

TIME: 947550744
TYPE: TABLE_DUMP/1
TABLE_DUMP: 186 entries
RIB_ENTRY:
  PREFIX: 3.0.0.0/8
  PEER: 204.152.166.29 AS3549
  ORIGINATED: 947505332
  ATTRIBUTES:
    ORIGIN: IGP
    AS_PATH: 3549 701 80
    NEXT_HOP: 204.152.166.29
RIB_ENTRY:
  ...

Malformed record — RFC 7606 findings surface under WARNINGS: (crafted record: ORIGIN with wrong flags plus a duplicate ORIGIN):

TIME: 1700000000
TYPE: BGP4MP/MessageAs4
FROM: 192.0.2.1 AS64496
TO: 192.0.2.2 AS64497
UPDATE:
  ANNOUNCED:
    10.0.0.0/24
  ATTRIBUTES:
    ORIGIN: IGP
    ORIGIN: IGP
    AS_PATH:
    NEXT_HOP: 192.0.2.254
  WARNINGS:
    Attribute flags error for ORIGIN: expected 0x40, got 0x80
    Duplicate attribute: ORIGIN

Testing

  • 7 unit tests in render::text: golden block for a full UPDATE, warnings section ordering, MP_REACH prefix folding, state change / OPEN / KEEPALIVE, legacy type-5 message and state change, peer table + RIB entries.
  • 2 integration tests in tests/render_text.rs: record stream rendering and the filter semantics (KEEPALIVEs dropped from record iteration while filters are active, UPDATE survives).
  • cargo test --all-features: 950 passed, 0 failed; cargo clippy --all-targets --all-features -- -D warnings clean; cargo fmt --check clean; MSRV 1.87 verified.

Add a human-readable, record-level rendering of MRT records:

- render::text::format_record: pure function of &MrtRecord producing one
  indented block per record — session context, UPDATE withdrawn/announced
  sections (folding in MP_REACH/MP_UNREACH prefixes), every path attribute,
  OPEN capabilities, session states, RIB entries, peer tables, and full
  legacy type-5 records. RFC 7606 findings render under WARNINGS:.
  Format is our own design on the crate's Display vocabulary; inspired by
  bgpdump's human-readable output, not byte-compatible.
- CLI: --format text, record-level (implies --level records; other formats
  are elem-level, documented on the enum variants)
- Filter semantics made explicit: no-elem records (KEEPALIVE/OPEN/state
  changes) never match elem filters and drop from record iteration while
  filters are active, now noted with a debug! line; behavior covered by
  integration tests
Copilot AI balanced review requested due to automatic review settings August 21, 2026 19:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds layered, record-level human-readable MRT rendering to the library and CLI.

Changes:

  • Adds render::text::format_record.
  • Adds CLI --format text and documents record filter semantics.
  • Adds rendering and filtering tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/render/text.rs Implements layered text rendering and unit tests.
src/render/mod.rs Documents and exports rendering.
src/lib.rs Exposes the render module.
src/bin/main.rs Adds CLI text output.
src/parser/iters/mod.rs Logs filtered records without elems.
tests/render_text.rs Tests stream rendering and filters.
CHANGELOG.md Documents the feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/render/text.rs
Comment on lines +168 to +172
AttributeValue::MpUnreachNlri(nlri) => {
withdrawn.extend(nlri.prefixes.iter());
}
AttributeValue::MpReachNlri(nlri) => {
announced.extend(nlri.prefixes.iter());
Comment thread src/render/text.rs Outdated
Comment on lines +346 to +354
if iter.peek().is_none() {
return;
}
out.push_str(&format!("{}ATTRIBUTES:\n", INDENT.repeat(depth)));
for attr in iter {
if let Some(line) = render_attribute(attr) {
out.push_str(&format!("{pad}{line}\n"));
}
}
Comment thread src/render/text.rs
Comment thread src/render/text.rs Outdated
Comment on lines +210 to +211
/// One line per attribute; MP reachability is folded into the prefix lists
/// above and not repeated here.
Comment thread CHANGELOG.md Outdated

### Added

* **Layered text output format** (`render::text::format_record`, `--format text`): one human-readable, indented block per MRT record — session context (`TIME`/`TYPE`/`FROM`/`TO`), `UPDATE:` sections with withdrawn/announced prefixes (including those carried in MP_REACH/MP_UNREACH) and every path attribute, `OPEN:` capabilities, session states, RIB entries, the peer table, and full legacy type-5 records. RFC 7606 validation findings render under `WARNINGS:` when present. The format is designed around this crate's own models and `Display` vocabulary — inspired by bgpdump's human-readable output, not byte-compatible with it. Rendering is a pure function of the record. In the CLI, `--format text` is **record-level** (implies `--level records`; all other formats are elem-level).
Comment thread src/bin/main.rs Outdated
Comment on lines +23 to +24
/// Layered human-readable text, one block per MRT record (record-level;
/// implies `--level records`, all other formats are elem-level).
Comment thread tests/render_text.rs Outdated
Comment on lines +1 to +3
//! Behavior of `--format text` building blocks: record-level rendering and
//! the documented filter semantics (non-UPDATE records are dropped when
//! filters are active).
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.88235% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.82%. Comparing base (169ed50) to head (82534f4).

Files with missing lines Patch % Lines
src/render/text.rs 85.62% 96 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #333      +/-   ##
==========================================
- Coverage   90.96%   90.82%   -0.14%     
==========================================
  Files         100      102       +2     
  Lines       24891    25567     +676     
==========================================
+ Hits        22642    23222     +580     
- Misses       2249     2345      +96     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- fold labeled (MPLS) announcements into an ANNOUNCED (labeled) section
  and surface link-state / flowspec NLRI counts, so no MP routes vanish
  from the transcript
- render the warnings block even when the attribute list is empty (RIB
  entries can carry missing-attribute findings with no attributes)
- RIB_GENERIC header includes the route prefix
- fix the render_attribute doc comment: prefixes are folded, the
  attribute summary line is retained
- docs: text always uses record-level output; other formats follow
  --level (elem-level by default) — enum variant, changelog, and test
  docs corrected
- tests: labeled/flowspec/link-state sections (incl. no-sections for
  empty collections) and warnings-without-attributes rendering
@digizeph

Copy link
Copy Markdown
Member Author

All 7 review comments addressed in 4f7b5a2:

  1. Labeled / link-state / flowspec NLRI folding — MPLS-labeled announcements (RFC 3107/8277) now render under ANNOUNCED (labeled): with each prefix's label stack (192.0.2.0/24 labels=[24001, 16]); populated BGP-LS and FlowSpec collections render LINK_STATE_NLRI: N entries (BGP-LS) / FLOWSPEC_NLRI: N rules (RFC 8955) lines (labeled withdrawals carry no labels and arrive through the plain prefix lists, per the Nlri model). Covered by a new test including the no-sections-for-empty-collections case.
  2. Warnings with empty attributesrender_attributes_block no longer early-returns: the WARNINGS: block renders even when the attribute list is empty, which is exactly the RIB case (parsers call check_mandatory_attributes, so a bare entry carries missing-attribute findings). Test added: warnings render with no ATTRIBUTES: section present.
  3. RIB_GENERIC header — now includes the route prefix: RIB_GENERIC: <afi>/<safi> PREFIX: <prefix> (N entries).
  4. Comment accuracy — the render_attribute doc now says what the code does: MP prefixes are folded into the WITHDRAWN/ANNOUNCED sections, the attribute summary line is retained.
    5./6. Granularity docs — corrected in the changelog, the CLI enum variant docs, and --help: --format text always uses record-level output; the other formats follow --level (elems by default, --level records selects the record pipeline for them too).
  5. Test docs wording — the condition is now stated as "records with an empty elem projection" with RIB records explicitly noted as elem-producing and filterable.

Gates: cargo test --all-features 952 passed / 0 failed, clippy -D warnings clean, strict rustdoc clean, fmt clean, README in sync.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

Previously missed (5) — in code that hasn't changed since the last review.

src/bin/main.rs:266

  • --format text -e now takes the record pipeline even though -e is a count-only operation. With filters, run_records counts every elem in any record having one match, whereas the elem pipeline counts only matching elems; an UPDATE with two prefixes and a filter matching one therefore reports 2 instead of 1. Keep the format override limited to actual output runs.
    let use_elem_stream = ((opts.elems_count && !opts.records_count)
        || (!opts.elems_count && !opts.records_count && matches!(opts.level, OutputLevel::Elems)))
        && output_format != OutputFormat::Text;

src/render/text.rs:120

  • This tests whether any optional parameter exists, not whether any capability exists. An OPEN containing only ParamValue::Raw therefore prints an empty CAPABILITIES: section. Collect capability values first and emit the section only when that collection is nonempty.
            if open.opt_params.is_empty() {
                return;

src/render/text.rs:192

  • NetworkPrefix's Display implementation omits path_id, so ADD-PATH withdrawals lose the identifier that distinguishes multiple paths for the same prefix. Use its Debug representation (which includes #<path_id>) or render path_id explicitly.

This issue also appears on line 196 of the same file.

        for prefix in withdrawn {
            out.push_str(&format!("{INDENT}{INDENT}{prefix}\n"));

src/render/text.rs:356

  • For Rib*AddPath records, the parser stores the path identifier in RibEntry::path_id, but this RIB rendering drops it. Distinct paths from the same peer can consequently produce identical blocks; render PATH_ID when present.

This issue also appears on line 369 of the same file.

                out.push_str(&format!(
                    "{INDENT}RIB_ENTRY:\n{INDENT}{INDENT}PEER_INDEX: {}\n{INDENT}{INDENT}ORIGINATED: {}\n",
                    entry.peer_index, entry.originated_time
                ));

src/render/mod.rs:5

  • “Full-fidelity” overstates this API: notification payload bytes, raw attribute bytes, peer-table metadata, and geo-peer details are summarized or omitted. Describe this as a human-readable transcript unless those fields are also rendered.
The [`text`] module renders one [`MrtRecord`](crate::MrtRecord) as a layered, indented text
block — a full-fidelity transcript of the record: BGP4MP session context,
withdrawn and announced prefixes, every path attribute, and RFC 7606

src/render/text.rs:197

  • ADD-PATH announcement identifiers are silently discarded here because NetworkPrefix::fmt(Display) prints only the IP prefix. This can make two distinct advertised paths render identically; preserve the identifier in the text output.
        for prefix in announced {
            out.push_str(&format!("{INDENT}{INDENT}{prefix}\n"));

src/render/text.rs:372

  • The generic RIB entry has the same optional ADD-PATH identifier, but it is not represented in this block. Preserve entry.path_id when present so format_record does not collapse distinct generic paths.
                out.push_str(&format!(
                    "{INDENT}RIB_ENTRY:\n{INDENT}{INDENT}PEER_INDEX: {}\n{INDENT}{INDENT}ORIGINATED: {}\n",
                    entry.peer_index, entry.originated_time
                ));

Comment thread src/render/text.rs
Comment on lines +210 to +214
out.push_str(&format!(
"{INDENT}{INDENT}{} labels=[{}]\n",
labeled.prefix,
labels.join(", ")
));
@digizeph

Copy link
Copy Markdown
Member Author

Addressed in 82534f4: labeled prefixes now render their ADD-PATH identifier when present — 192.0.2.0/24 labels=[24001, 16] path-id 7 — and the labeled-section test covers the path-id case (absent ids stay absent, no trailing field). All gates green: 952 tests, clippy -D warnings, strict rustdoc, fmt.

@digizeph
digizeph merged commit fe3cd1b into main Aug 21, 2026
9 checks passed
@digizeph
digizeph deleted the feature/render-text-format branch August 21, 2026 21:35
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