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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,40 @@

All notable changes to this project will be documented in this file.

## Unreleased changes

### Breaking Changes

* Changed `monocle rib --sqlite-path` output schema from single `elems` table to two tables:
* `ribs` table: stores final reconstructed RIB states at each target timestamp
* `updates` table: stores filtered BGP updates used to build 2nd and later RIB snapshots
* Updates table is only populated for RIBs after the first/base RIB

### New Features

* Added `monocle rib` for reconstructing RIB state at arbitrary timestamps
* Selects the latest RIB before each requested `rib_ts` and replays updates to the exact timestamp
* Supports stdout output by default and SQLite output via `--sqlite-path`
* Repeated timestamp operands require `--sqlite-path` and are written to one merged SQLite file keyed by `rib_ts`
* Aborts when no RIB exists at or before a requested `rib_ts` for a selected collector
* Supports `--country`, `--origin-asn`, `--prefix`, `--as-path`, `--peer-asn`, `--collector`, `--project`, and `--full-feed-only`

### Performance Improvements

* Reduced string allocations in RIB reconstruction by using `Arc<str>` for collector and prefix fields
* Removed unnecessary per-snapshot sorting that was `O(n log n)` on all entries
* Reduced updates query window from +2 hours lookahead to exact target timestamp
* Results in 33% fewer update files downloaded for typical requests

### Code Improvements

* Added `StoredRibUpdate` struct to track filtered BGP updates during reconstruction
* Added a session-backed SQLite store for merged reconstructed RIB export
* Updated `monocle rib` reconstruction to keep the working RIB state in memory
* Removes SQLite lookups and writes from the replay hot path
* Keeps `path_id` only for internal route identity during add-path reconstruction
* Narrows reconstructed RIB entries and SQLite export rows to collector, timestamp, peer_ip, peer_asn, prefix, as_path, and origin_asns

## v1.2.0 - 2026-02-28

### New Features
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ lib = [
# Database
"dep:oneio",
"dep:ipnet",
"dep:tempfile",
# Lenses
"dep:chrono-humanize",
"dep:dateparser",
Expand All @@ -98,6 +99,7 @@ lib = [
"dep:itertools",
"dep:radar-rs",
"dep:rayon",
"dep:regex",
# Display (always included with lib)
"dep:tabled",
"dep:json_to_table",
Expand Down Expand Up @@ -151,6 +153,7 @@ tracing = "0.1"
# Database
ipnet = { version = "2.10", features = ["json"], optional = true }
oneio = { version = "0.20.1", default-features = false, features = ["https", "gz", "bz", "json"], optional = true }
tempfile = { version = "3", optional = true }

# Lenses
chrono-humanize = { version = "0.2", optional = true }
Expand All @@ -162,6 +165,7 @@ bgpkit-commons = { version = "0.10.2", features = ["asinfo", "rpki", "countries"
itertools = { version = "0.14", optional = true }
radar-rs = { version = "0.1.0", optional = true }
rayon = { version = "1.8", optional = true }
regex = { version = "1.11", optional = true }

# Display
tabled = { version = "0.20", optional = true }
Expand Down
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ See through all Border Gateway Protocol (BGP) data with a monocle.
- [`monocle parse`](#monocle-parse)
- [Output Format](#output-format)
- [`monocle search`](#monocle-search)
- [`monocle rib`](#monocle-rib)
- [`monocle time`](#monocle-time)
- [`monocle inspect`](#monocle-inspect)
- [`monocle country`](#monocle-country)
Expand Down Expand Up @@ -229,6 +230,7 @@ Subcommands:

- `parse`: parse individual MRT files
- `search`: search for matching messages from all available public MRT files
- `rib`: reconstruct final RIB state at one or more arbitrary timestamps
- `server`: start a WebSocket server for programmatic access
- `inspect`: unified AS and prefix information lookup
- `country`: utility to look up country name and code
Expand Down Expand Up @@ -259,6 +261,7 @@ Usage: monocle [OPTIONS] <COMMAND>
Commands:
parse Parse individual MRT files given a file path, local or remote
search Search BGP messages from all available public MRT files
rib Reconstruct final RIB state at one or more arbitrary timestamps
server Start the WebSocket server (ws://<address>:<port>/ws, health: http://<address>:<port>/health)
inspect Unified AS and prefix information lookup
country Country name and code lookup utilities
Expand Down Expand Up @@ -701,6 +704,110 @@ Use `--broker-files` to see the list of MRT files that would be queried without
-c rrc00 --broker-files
```

### `monocle rib`

Reconstruct final RIB state at one or more arbitrary timestamps by loading the latest RIB at or before each requested `rib_ts` and replaying updates up to the exact timestamp.

```text
➜ monocle rib --help
Reconstruct final RIB state at one or more arbitrary timestamps

Usage: monocle rib [OPTIONS] <RIB_TS>...

Arguments:
<RIB_TS>... Target RIB timestamp operand. Repeat to request multiple snapshots

Options:
-o, --origin-asn <ORIGIN_ASN> Filter by origin AS Number(s), comma-separated. Prefix with ! to exclude
-C, --country <COUNTRY> Filter by origin ASN registration country
--debug Print debug information
--format <FORMAT> Output format: table, markdown, json, json-pretty, json-line, psv (default varies by command)
-p, --prefix <PREFIX> Filter by network prefix(es), comma-separated. Prefix with ! to exclude
--json Output as JSON objects (shortcut for --format json-pretty)
-s, --include-super Include super-prefixes when filtering
--no-update Disable automatic database updates (use existing cached data only)
-S, --include-sub Include sub-prefixes when filtering
-J, --peer-asn <PEER_ASN> Filter by peer ASN(s), comma-separated. Prefix with ! to exclude
-a, --as-path <AS_PATH> Filter by AS path regex string
-c, --collector <COLLECTOR> Filter by collector, e.g., rrc00 or route-views2
-P, --project <PROJECT> Filter by route collection project, i.e. riperis or routeviews
--full-feed-only Keep only full-feed peers based on broker peer metadata
--sqlite-path <SQLITE_PATH> SQLite output file path
-h, --help Print help
-V, --version Print version
```

Behavior:

- A single timestamp operand writes to stdout by default.
- Stdout output is the reconstructed final route set, not an MRT/table-dump export.
- Stdout follows the normal streaming formatter, so the default is `psv` unless `--format` or `--json` is provided.
- Repeated timestamp operands require `--sqlite-path` and are written to one merged SQLite file.
- Providing `--sqlite-path` writes the reconstructed results to that SQLite file instead of stdout.
- If any selected collector has no RIB at or before a requested `rib_ts`, the command aborts instead of producing a partial result.
- `--country` uses local ASInfo registration data, and `--full-feed-only` keeps only peers with at least 800k IPv4 prefixes or 100k IPv6 prefixes in broker peer metadata.

SQLite Output Schema:

When using `--sqlite-path`, the output contains two tables:

**`ribs` table** - Final reconstructed RIB states:
```sql
CREATE TABLE ribs (
rib_ts INTEGER NOT NULL, -- Target RIB timestamp (the time you requested)
timestamp REAL NOT NULL, -- Actual BGP message timestamp
collector TEXT NOT NULL, -- Route collector name (e.g., 'rrc00')
peer_ip TEXT NOT NULL, -- Peer IP address
peer_asn INTEGER NOT NULL, -- Peer AS number
prefix TEXT NOT NULL, -- Network prefix
path_id INTEGER, -- BGP path identifier (for add-path)
as_path TEXT, -- AS path string
origin_asns TEXT -- Origin AS numbers (space-separated)
);
```
- Contains one row per (rib_ts, route) showing the final routing table state at each requested timestamp.
- Query example: `SELECT * FROM ribs WHERE rib_ts = 1704067200 AND prefix = '1.1.1.0/24';`

**`updates` table** - Filtered BGP updates (2nd and later RIBs only):
```sql
CREATE TABLE updates (
rib_ts INTEGER NOT NULL, -- Target RIB timestamp this update contributed to
timestamp REAL NOT NULL, -- When the update message was received
collector TEXT NOT NULL, -- Route collector name
peer_ip TEXT NOT NULL, -- Peer IP address
peer_asn INTEGER NOT NULL, -- Peer AS number
prefix TEXT NOT NULL, -- Network prefix
path_id INTEGER, -- BGP path identifier
as_path TEXT, -- AS path string
origin_asns TEXT, -- Origin AS numbers
elem_type TEXT NOT NULL -- 'ANNOUNCE' or 'WITHDRAW'
);
```
- Contains filtered updates that were applied to build 2nd and later RIB snapshots.
- **Not populated for the first/base RIB** (loaded directly from RIB dump file).
- Shows the incremental changes between consecutive RIB states.
- Useful for understanding what changed between snapshots.
- Query example: `SELECT * FROM updates WHERE rib_ts = 1704090000 ORDER BY timestamp;`

Examples:

```bash
# Print the reconstructed RIB for a single timestamp to stdout
monocle rib 2025-09-01T12:00:00Z -c rrc00 -o 13335

# Write multiple timestamps to one merged SQLite file in the current directory
monocle rib \
2025-09-01T12:00:00Z \
2025-09-01T18:00:00Z \
--sqlite-path /tmp/rrc00-us.sqlite3 \
-c rrc00 \
--country US \
--full-feed-only

# Write a single reconstructed snapshot to SQLite
monocle rib 2025-09-01T12:00:00Z --sqlite-path /tmp/route-views2.sqlite3 -c route-views2
```

### `monocle time`

Parse and convert time strings between various formats.
Expand Down
25 changes: 25 additions & 0 deletions src/bin/commands/elem_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ pub const AVAILABLE_FIELDS: &[&str] = &[
"peer_ip",
"peer_asn",
"prefix",
"path_id",
"as_path",
"origin_asns",
"origin",
"next_hop",
"local_pref",
Expand Down Expand Up @@ -174,11 +176,26 @@ pub fn get_field_value_with_time_format(
"peer_ip" => elem.peer_ip.to_string(),
"peer_asn" => elem.peer_asn.to_string(),
"prefix" => elem.prefix.to_string(),
"path_id" => elem
.prefix
.path_id
.map(|path_id| path_id.to_string())
.unwrap_or_default(),
"as_path" => elem
.as_path
.as_ref()
.map(|p| p.to_string())
.unwrap_or_default(),
"origin_asns" => elem
.origin_asns
.as_ref()
.map(|asns| {
asns.iter()
.map(|asn| asn.to_string())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default(),
"origin" => elem
.origin
.as_ref()
Expand Down Expand Up @@ -288,10 +305,18 @@ pub fn build_json_object(
"peer_ip" => json!(elem.peer_ip.to_string()),
"peer_asn" => json!(elem.peer_asn),
"prefix" => json!(elem.prefix.to_string()),
"path_id" => match elem.prefix.path_id {
Some(path_id) => json!(path_id),
None => serde_json::Value::Null,
},
"as_path" => match &elem.as_path {
Some(p) => json!(p.to_string()),
None => serde_json::Value::Null,
},
"origin_asns" => match &elem.origin_asns {
Some(asns) => json!(asns.iter().map(|asn| asn.to_string()).collect::<Vec<_>>()),
None => serde_json::Value::Null,
},
"origin" => match &elem.origin {
Some(o) => json!(o.to_string()),
None => serde_json::Value::Null,
Expand Down
1 change: 1 addition & 0 deletions src/bin/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod inspect;
pub mod ip;
pub mod parse;
pub mod pfx2as;
pub mod rib;
pub mod rpki;
pub mod search;
pub mod time;
Loading
Loading