Building a BGP Monitoring Protocol (BMP) Lab: From Router to Dashboard
A hands-on guide to building a complete BMP pipeline with Huawei NE40e, GoBMP, Kafka, PostgreSQL, and Grafana seeing what your routing…
Building a BGP Monitoring Protocol (BMP) Lab: From Router to Dashboard
A hands-on guide to building a complete BMP pipeline with Huawei NE40e, GoBMP, Kafka, PostgreSQL, and Grafana seeing what your routing policy is hiding.
Why BMP? The Problem It Solves
If you’ve ever managed a BGP-heavy network, you know the pain. A customer calls: “I’m advertising 15.15.15.15/32 to you, but my traffic isn’t flowing.” Your first move? SSH into the router, run display bgp routing-table peer x.x.x.x accepted-routes, then not-accepted-routes, then check the policy, then check the next router… repeat across 60 POPs. That doesn’t scale.
BGP Monitoring Protocol (RFC 7854) solves this by having the router continuously stream its BGP state peer sessions, route announcements, and statistics — to an external collector over TCP. No polling. No CLI scraping. No SNMP traps that miss half the events.
The real power is pre-policy monitoring. BMP can show you every route a peer sends before your inbound filters act on it. This means you can see what got rejected and why, without logging into the router. For an ISP with hundreds of customers, this turns a 20-minute troubleshooting session into a dashboard lookup.
This post walks through building a complete BMP pipeline in a lab from router configuration to Grafana dashboards showing real filtered prefixes.
Lab Topology

Four Huawei NE40e routers running in eNSP. All in AS 65001, using IS-IS as the IGP. The central router (9.9.9.9) acts as a route reflector and runs BMP, streaming data to a GoBMP collector on the management network.
The key detail: R2 advertises 15.15.15.15/32, but an inbound route-policy on NE40E blocks it. With BMP in pre-policy mode, we can see that rejected prefix in the monitoring data even though the router’s RIB never accepts it.
The Data Pipeline
NE40E ──BMP/TCP──▶ GoBMP ──JSON──▶ Kafka ──consume──▶ Python ──SQL──▶ PostgreSQL ──query──▶ Grafana
Each component has a specific job:
GoBMP receives raw BMP messages from the router and parses them into JSON. It’s a lightweight Go binary that understands the BMP wire format and publishes structured messages to Kafka topics, one topic per message type.
Kafka acts as a message buffer between GoBMP and the database. This matters because when a BGP session resets, the router dumps its entire routing table over BMP in seconds potentially thousands of messages. Kafka absorbs that burst while the consumer drains at a pace PostgreSQL can handle. For this lab with a handful of routes, Kafka isn’t strictly necessary, but it teaches the architecture pattern used at production scale.
Python Consumer bridges Kafka and PostgreSQL. It reads JSON messages from Kafka topics, extracts the relevant fields, and writes SQL INSERTs to the appropriate tables. This is the translator layer where we handle GoBMP’s JSON quirks like inconsistent field names between message types.
PostgreSQL provides permanent, queryable storage. Unlike Kafka (which retains messages temporarily), PostgreSQL lets us run time-range queries, aggregations, and JOINs that power the Grafana dashboards.
Grafana visualizes the data peer status timelines, route counts, and most importantly, the pre-policy statistics showing received vs. accepted vs. rejected prefix counts.
Router Configurations
NE40E BMP Router and Route Reflector
This is the central router that runs BMP. It peers with all three routers, reflects routes between them, and streams BMP data to the GoBMP collector.
sysname NE40E
isis 1
is-level level-2
cost-style wide
network-entity 49.0001.0000.0000.0009.00
is-name NE40E
interface Ethernet1/0/0
ip address 192.168.1.77 255.255.255.0
isis enable 1
interface Ethernet1/0/1
description TO-R1
ip address 10.0.1.1 255.255.255.252
isis enable 1
interface Ethernet1/0/2
description TO-R2
ip address 10.0.2.1 255.255.255.252
isis enable 1
interface Ethernet1/0/3
description TO-R3
ip address 10.0.3.1 255.255.255.252
isis enable 1
interface LoopBack0
ip address 9.9.9.9 255.255.255.255
isis enable 1
The route-policy that blocks 15.15.15.15/32 from R2:
ip ip-prefix BLOCK-15 index 10 deny 15.15.15.15 32
ip ip-prefix BLOCK-15 index 20 permit 0.0.0.0 0 less-equal 32
route-policy FILTER-PEER2 permit node 10
if-match ip-prefix BLOCK-15
BGP configuration with route reflection and per-peer keep-all-routes:
bgp 65001
router-id 9.9.9.9
group RR internal
peer 1.1.1.1 as-number 65001
peer 1.1.1.1 group RR
peer 1.1.1.1 connect-interface LoopBack0
peer 2.2.2.2 as-number 65001
peer 2.2.2.2 group RR
peer 2.2.2.2 connect-interface LoopBack0
peer 3.3.3.3 as-number 65001
peer 3.3.3.3 group RR
peer 3.3.3.3 connect-interface LoopBack0
ipv4-family unicast
undo synchronization
peer RR enable
peer RR reflect-client
peer 1.1.1.1 enable
peer 1.1.1.1 group RR
peer 1.1.1.1 keep-all-routes
peer 2.2.2.2 enable
peer 2.2.2.2 group RR
peer 2.2.2.2 route-policy FILTER-PEER2 import
peer 2.2.2.2 keep-all-routes
peer 3.3.3.3 enable
peer 3.3.3.3 group RR
peer 3.3.3.3 keep-all-routes
The keep-all-routes command is critical. Without it, the router discards rejected routes from memory immediately, so BMP would have nothing to report for pre-policy. With it, rejected routes are kept in a separate Adj-RIB-In table that BMP can read.
BMP Configuration
bmp
statistics-timer 15
route-mode pre-policy
session 192.168.1.180 alias GOBMP-VM
tcp connect port 11019
monitor address-family all disable
connect-interface Ethernet1/0/0
monitor ipv4-family unicast peer 1.1.1.1
monitor ipv4-family unicast peer 2.2.2.2
monitor ipv4-family unicast peer 3.3.3.3
A few things to note:
route-mode pre-policy tells BMP to send routes as received, before inbound policy filtering. On this NE40e version, it's a global setting you can't mix pre-policy and post-policy per peer. You get one or the other.
monitor address-family all disable followed by specific monitor statements is a deny-all-then-permit approach. Without the disable, BMP would stream data for every peer in every address family. We only want IPv4 unicast from our three peers.
statistics-timer 15 sends per-peer stats every 15 seconds prefix counts, rejected counts, and other counters.
R1 Config
sysname R1
isis 1
is-level level-2
cost-style wide
network-entity 49.0001.0000.0000.0001.00
interface Ethernet0/0/0
ip address 10.0.1.2 255.255.255.252
isis enable 1
interface LoopBack0
ip address 1.1.1.1 255.255.255.255
isis enable 1
interface LoopBack11
ip address 11.11.11.11 255.255.255.255
interface LoopBack12
ip address 12.12.12.12 255.255.255.255
bgp 65001
router-id 1.1.1.1
peer 9.9.9.9 as-number 65001
peer 9.9.9.9 connect-interface LoopBack0
ipv4-family unicast
undo synchronization
network 11.11.11.11 255.255.255.255
network 12.12.12.12 255.255.255.255
peer 9.9.9.9 enable
R2 Config
sysname R2
isis 1
is-level level-2
cost-style wide
network-entity 49.0001.0000.0000.0002.00
interface Ethernet0/0/0
ip address 10.0.2.2 255.255.255.252
isis enable 1
interface LoopBack0
ip address 2.2.2.2 255.255.255.255
isis enable 1
interface LoopBack15
ip address 15.15.15.15 255.255.255.255
interface LoopBack22
ip address 22.22.22.22 255.255.255.255
bgp 65001
router-id 2.2.2.2
peer 9.9.9.9 as-number 65001
peer 9.9.9.9 connect-interface LoopBack0
ipv4-family unicast
undo synchronization
network 15.15.15.15 255.255.255.255
network 22.22.22.22 255.255.255.255
peer 9.9.9.9 enable
R2 advertises both 22.22.22.22/32 and 15.15.15.15/32. NE40E’s inbound policy accepts the first and rejects the second.
R3 Config
sysname R3
isis 1
is-level level-2
cost-style wide
network-entity 49.0001.0000.0000.0003.00
interface Ethernet0/0/0
ip address 10.0.3.2 255.255.255.252
isis enable 1
interface LoopBack0
ip address 3.3.3.3 255.255.255.255
isis enable 1
interface LoopBack33
ip address 33.33.33.33 255.255.255.255
interface LoopBack10
ip address 192.168.1.1 255.255.255.255
interface LoopBack11
ip address 192.168.2.1 255.255.255.255
bgp 65001
router-id 3.3.3.3
peer 9.9.9.9 as-number 65001
peer 9.9.9.9 connect-interface LoopBack0
ipv4-family unicast
undo synchronization
network 33.33.33.33 255.255.255.255
network 192.168.1.1 255.255.255.255
network 192.168.2.1 255.255.255.255
peer 9.9.9.9 enable
Verifying the Setup
Before looking at BMP data, let’s confirm everything is working on the NE40E.
IS-IS adjacencies:
<NE40E>dis isis peer
System Id Interface Circuit Id State HoldTime Type PRI
--------------------------------------------------------------------------------
R1 Eth1/0/1 R1.01 Up 9s L2 64
R2 Eth1/0/2 R2.01 Up 8s L2 64
R3 Eth1/0/3 R3.01 Up 8s L2 64
BGP peers :
<NE40E>dis bgp peer
BGP local router ID : 9.9.9.9
Local AS number : 65001
Total number of peers : 3 Peers in established state : 3
Peer V AS MsgRcvd MsgSent OutQ Up/Down State PrefRcv
1.1.1.1 4 65001 82 117 0 01:18:47 Established 2
2.2.2.2 4 65001 62 102 0 01:00:01 Established 2
3.3.3.3 4 65001 214 315 0 03:28:04 Established 3
BGP routing table
<NE40E>dis bgp routing-table
Total Number of Routes: 7
Network NextHop MED LocPrf PrefVal Path/Ogn
*>i 11.11.11.11/32 1.1.1.1 0 100 0 i
*>i 12.12.12.12/32 1.1.1.1 0 100 0 i
i 15.15.15.15/32 2.2.2.2 0 100 0 i
*>i 22.22.22.22/32 2.2.2.2 0 100 0 i
i 33.33.33.33/32 3.3.3.3 0 100 0 i
i 192.168.1.1/32 3.3.3.3 0 100 0 i
i 192.168.2.1/32 3.3.3.3 0 100 0 i
See 15.15.15.15/32? It has i but no *> — the route is known (because of keep-all-routes) but not best, not valid, not installed. The route-policy rejected it. Without keep-all-routes, this line wouldn't appear at all.
BMP session
<NE40E>dis bmp session 192.168.1.180 alias GOBMP-VM verbose
BMP session 192.168.1.180, port 11019
Current state: Up, Age: 01h45m22s
Route Mode: pre-policy
Statistics timeout: 15(s)
Bmp monitor 3 Peer(s):
BGP ipv4-family unicast :
1.1.1.1
2.2.2.2
3.3.3.3
Docker Stack
The collector infrastructure runs on a Linux VM (192.168.1.180) reachable from the NE40E’s management interface.
docker-compose.yml
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:7.5.0
container_name: kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
healthcheck:
test: kafka-topics --bootstrap-server localhost:9092 --list
interval: 10s
timeout: 5s
retries: 5
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: kafka-ui
depends_on:
- kafka
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: bmp-lab
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
gobmp:
image: sbezverk/gobmp:latest
container_name: gobmp
depends_on:
kafka:
condition: service_healthy
ports:
- "11019:11019"
- "56767:56767"
command: >
--source-port=11019
--kafka-server=kafka:29092
--v=5
restart: unless-stopped
postgres:
image: postgres:16
container_name: postgres
environment:
POSTGRES_DB: bmpdata
POSTGRES_USER: bmpuser
POSTGRES_PASSWORD: bmppass123
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
grafana:
image: grafana/grafana:latest
container_name: grafana
depends_on:
- postgres
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana-storage:/var/lib/grafana
volumes:
pgdata:
grafana-storage:
Start everything:
docker compose up -d
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
gobmp sbezverk/gobmp:latest "/gobmp --source-por…" gobmp 3 hours ago Up 3 hours 0.0.0.0:11019->11019/tcp, [::]:11019->11019/tcp, 0.0.0.0:56767->56767/tcp, [::]:56767->56767/tcp
grafana grafana/grafana:latest "/run.sh" grafana 3 hours ago Up 3 hours 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
kafka confluentinc/cp-kafka:7.5.0 "/etc/confluent/dock…" kafka 3 hours ago Up 3 hours (healthy) 0.0.0.0:9092->9092/tcp, [::]:9092->9092/tcp
kafka-ui provectuslabs/kafka-ui:latest "/bin/sh -c 'java --…" kafka-ui 3 hours ago Up 3 hours 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp
postgres postgres:16 "docker-entrypoint.s…" postgres 3 hours ago Up 3 hours 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
zookeeper confluentinc/cp-zookeeper:7.5.0 "/etc/confluent/dock…" zookeeper 3 hours ago Up 3 hours 2888/tcp, 0.0.0.0:2181->2181/tcp, [::]:2181->2181/tcp, 3888/tcp
Understanding GoBMP’s Output
GoBMP publishes to separate Kafka topics per message type. The three that matter for this lab:
| Kafka Topic | Content |
| -------------------------------- | ----------------------------------------- |
| `gobmp.parsed.peer` | Peer Up/Down events |
| `gobmp.parsed.unicast_prefix_v4` | IPv4 route announcements and withdrawals |
| `gobmp.parsed.statistics` | Per-peer stats reports (every 15 seconds) |
You can inspect the raw messages using Kafka UI at http://localhost:8080, or from the command line:
docker exec kafka kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic gobmp.parsed.statistics \
--from-beginning --max-messages 3
Peer Up Messages
When a BGP session establishes, GoBMP publishes to gobmp.parsed.peer:
{
"action": "add",
"router_ip": "9.9.9.9",
"remote_ip": "2.2.2.2",
"remote_asn": 65001,
"local_asn": 65001,
"local_port": 179,
"remote_port": 61650,
"remote_holddown": 180,
"is_prepolicy": false,
"is_adj_rib_in_post_policy": false
}
Peer Down Messages
When a session drops, the same topic carries a "down" action with a reason code:
{
"action": "down",
"router_ip": "9.9.9.9",
"remote_ip": "3.3.3.3",
"bmp_reason": 3
}
The bmp_reason codes from RFC 7854: 1 = local closed, 2 = local sent NOTIFICATION, 3 = remote sent NOTIFICATION, 4 = remote closed without NOTIFICATION.
Route Announcements
Actual prefixes appear on gobmp.parsed.unicast_prefix_v4:
{
"action": "add",
"router_ip": "9.9.9.9",
"peer_ip": "2.2.2.2",
"peer_asn": 65001,
"prefix": "15.15.15.15",
"prefix_len": 32,
"nexthop": "2.2.2.2",
"is_adj_rib_in_post_policy": false,
"base_attrs": {
"origin": "igp",
"local_pref": 100
}
}
Notice that 15.15.15.15/32 appears here even though it’s blocked by the route-policy. This is pre-policy BMP in action the router reports what the peer sent before the filter runs.
Statistics Reports
Every 15 seconds, gobmp.parsed.statistics delivers per-peer counters:
{
"router_ip": "9.9.9.9",
"remote_ip": "2.2.2.2",
"remote_asn": 65001,
"ads_rib_in": 2,
"local_rib": 1,
"prefixes_rejected_inbound": 1
}
This is the proof: ads_rib_in: 2 (R2 sent 2 prefixes), local_rib: 1 (only 1 made it to the routing table), prefixes_rejected_inbound: 1 (1 was blocked by the inbound policy). The math checks out: 2 − 1 = 1.
Database Schema
The init-db.sql file creates three tables:
CREATE TABLE IF NOT EXISTS bmp_peers (
id SERIAL PRIMARY KEY,
router_ip VARCHAR(45) NOT NULL,
peer_ip VARCHAR(45) NOT NULL,
peer_asn BIGINT,
is_up BOOLEAN DEFAULT false,
bmp_reason INT,
remote_port INT,
timestamp TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS bmp_routes (
id SERIAL PRIMARY KEY,
router_ip VARCHAR(45) NOT NULL,
peer_ip VARCHAR(45) NOT NULL,
peer_asn BIGINT,
prefix VARCHAR(50) NOT NULL,
prefix_len INT,
nexthop VARCHAR(45),
origin VARCHAR(10),
local_pref INT,
policy_type VARCHAR(10) NOT NULL,
action VARCHAR(10) NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS bmp_stats (
id SERIAL PRIMARY KEY,
router_ip VARCHAR(45) NOT NULL,
peer_ip VARCHAR(45) NOT NULL,
peer_asn BIGINT,
ads_rib_in INT DEFAULT 0,
local_rib INT DEFAULT 0,
prefixes_rejected INT DEFAULT 0,
duplicate_prefix INT DEFAULT 0,
timestamp TIMESTAMPTZ DEFAULT NOW()
);
Python Consumer
The consumer reads from Kafka, detects message type by topic name, extracts fields, and writes to PostgreSQL.
import json
import psycopg2
from kafka import KafkaConsumer
from datetime import datetime
import os
KAFKA_BROKER = os.environ.get('KAFKA_BROKER', 'localhost:9092')
DB_CONFIG = {
'host': os.environ.get('DB_HOST', 'localhost'),
'database': os.environ.get('DB_NAME', 'bmpdata'),
'user': os.environ.get('DB_USER', 'bmpuser'),
'password': os.environ.get('DB_PASS', 'bmppass123')
}
TOPICS = [
'gobmp.parsed.peer',
'gobmp.parsed.unicast_prefix_v4',
'gobmp.parsed.unicast_prefix_v6',
'gobmp.parsed.statistics'
]
def get_db_connection():
conn = psycopg2.connect(**DB_CONFIG)
conn.autocommit = True
return conn
def process_peer_message(data, cursor):
action = data.get('action', '')
is_up = (action == 'add')
cursor.execute("""
INSERT INTO bmp_peers
(router_ip, peer_ip, peer_asn, is_up, bmp_reason, remote_port, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (
data.get('router_ip', ''),
data.get('remote_ip', ''), # NOTE: remote_ip for peer messages
data.get('remote_asn', 0),
is_up,
data.get('bmp_reason'),
data.get('remote_port'),
data.get('timestamp', datetime.utcnow().isoformat())
))
status = "UP" if is_up else f"DOWN (reason: {data.get('bmp_reason', '?')})"
print(f" [PEER] {data.get('remote_ip')} -> {status}")
def process_route_message(data, cursor):
if data.get('is_eor', False):
return
if not data.get('prefix'):
return
if data.get('is_adj_rib_in_post_policy', False):
policy_type = 'post'
else:
policy_type = 'pre'
base_attrs = data.get('base_attrs', {})
cursor.execute("""
INSERT INTO bmp_routes
(router_ip, peer_ip, peer_asn, prefix, prefix_len,
nexthop, origin, local_pref, policy_type, action, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
data.get('router_ip', ''),
data.get('peer_ip', ''), # NOTE: peer_ip for route messages
data.get('peer_asn', 0),
data.get('prefix', ''),
data.get('prefix_len', 0),
data.get('nexthop', ''),
base_attrs.get('origin', ''),
base_attrs.get('local_pref'),
policy_type,
data.get('action', 'add'),
data.get('timestamp', datetime.utcnow().isoformat())
))
print(f" [ROUTE] {data.get('peer_ip')} -> {data.get('prefix')}/{data.get('prefix_len')} "
f"[{policy_type}] nexthop={data.get('nexthop')}")
def process_stats_message(data, cursor):
cursor.execute("""
INSERT INTO bmp_stats
(router_ip, peer_ip, peer_asn, ads_rib_in, local_rib,
prefixes_rejected, duplicate_prefix, timestamp)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (
data.get('router_ip', ''),
data.get('remote_ip', ''), # NOTE: remote_ip for stats messages
data.get('remote_asn', 0),
data.get('ads_rib_in', 0),
data.get('local_rib', 0),
data.get('prefixes_rejected_inbound', 0),
data.get('duplicate_prefix', 0),
data.get('timestamp', datetime.utcnow().isoformat())
))
rejected = data.get('prefixes_rejected_inbound', 0)
reject_str = f" REJECTED={rejected}" if rejected > 0 else ""
print(f" [STATS] {data.get('remote_ip')} -> "
f"rib_in={data.get('ads_rib_in', 0)} "
f"local_rib={data.get('local_rib', 0)}{reject_str}")
def main():
print("BMP Kafka Consumer — NE40e + GoBMP Lab")
consumer = KafkaConsumer(
*TOPICS,
bootstrap_servers=KAFKA_BROKER,
auto_offset_reset='earliest',
group_id='bmp-lab-consumer',
)
conn = get_db_connection()
cursor = conn.cursor()
for msg in consumer:
try:
data = json.loads(msg.value.decode('utf-8'))
if msg.topic == 'gobmp.parsed.peer':
process_peer_message(data, cursor)
elif msg.topic in ('gobmp.parsed.unicast_prefix_v4',
'gobmp.parsed.unicast_prefix_v6'):
process_route_message(data, cursor)
elif msg.topic == 'gobmp.parsed.statistics':
process_stats_message(data, cursor)
except Exception as e:
print(f"[ERROR] {e}")
cursor.close()
conn.close()
if __name__ == '__main__':
main()
Install dependencies and run:
pip install kafka-python-ng psycopg2-binary
python consumer.py
Grafana Dashboards
Connect Grafana (http://localhost:3000, admin/admin) to PostgreSQL: Connections → Data sources → PostgreSQL → Host: postgres:5432, Database: bmpdata, User: bmpuser, Password: bmppass123, TLS disabled.
Peer Status Table
A table showing every peer state change with timestamps, reason codes, and TCP port numbers. When a peer flaps, you see the DOWN event (with bmp_reason) followed by the UP event with a new remote_portconfirming it's a new TCP session, not a keep-alive recovery.

SELECT
timestamp,
peer_ip,
peer_asn,
CASE WHEN is_up THEN 'UP'
ELSE 'DOWN (reason ' || COALESCE(bmp_reason::text, '?') || ')'
END AS status,
remote_port
FROM bmp_peers
ORDER BY timestamp DESC
LIMIT 50;
Routes Per Peer
A bar chart showing how many route announcements BMP captured per peer. R2 (2.2.2.2) leads with 6 because it has 2 prefixes that got re-announced across multiple BMP session resets.

SELECT
peer_ip,
COUNT(*) AS route_count
FROM bmp_routes
WHERE action = 'add'
GROUP BY peer_ip
ORDER BY route_count DESC;
All Peers Stats Comparison
This is the dashboard that demonstrates BMP’s core value. Three groups of bars, one per peer:
1.1.1.1: Received 2, Accepted 2, Rejected 0 — no filtering, everything passes 2.2.2.2: Received 2, Accepted 1, Rejected 1–15.15.15.15/32 is blocked 3.3.3.3: Received 3, Accepted 3, Rejected 0 — no filtering
That red bar on peer 2.2.2.2 is the entire point of this lab. Without BMP pre-policy monitoring, you’d have to SSH into the router and run display bgp routing-table peer 2.2.2.2 not-accepted-routes to discover that a prefix is being rejected. With BMP, it shows up on a dashboard automatically, in real time, across every peer on every router.

SELECT
peer_ip,
MAX(ads_rib_in) AS "Received",
MAX(local_rib) AS "Accepted",
MAX(prefixes_rejected) AS "Rejected"
FROM bmp_stats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
GROUP BY peer_ip
ORDER BY peer_ip;
What We Learned
BMP’s value is in what your router hides from you. That red “Rejected: 1” bar on peer 2.2.2.2 told us in a glance what would have taken CLI access and per-peer commands to find manually. Scale that to hundreds of peers and you see why ISPs invest in this.
Build the full pipeline, even for 7 routes. GoBMP → Kafka → Python → PostgreSQL → Grafana taught us how each component connects and fails. Learning that pattern in a lab is cheaper than learning it with 900,000 prefixes in production.
메타데이터
- post_id
- 3ec6a8b779d3
- slug
- building-a-bgp-monitoring-protocol-bmp-lab-from-router-to-dashboard-3ec6a8b779d3
- url
- https://medium.com/@netopsautomation/building-a-bgp-monitoring-protocol-bmp-lab-from-router-to-dashboard-3ec6a8b779d3
- canonical_url
- https://medium.com/@netopsautomation/building-a-bgp-monitoring-protocol-bmp-lab-from-router-to-dashboard-3ec6a8b779d3
- author_url
- https://medium.com/@netopsautomation
- status
- ok
- fetched_at
- 2026-06-10 15:53:41