Adding BGP Prefix Length Statistics to BIRD Exporter

Adding BGP Prefix Length Statistics to BIRD Exporter

When you're running BGP full tables (like the ~220k IPv6 routes I receive), understanding prefix length distribution becomes interesting for network analysis. The question is simple: how many /48s, /32s, /44s am I actually receiving?

The popular bird_exporter for Prometheus monitoring didn't support this out of the box. Time to fix that.

The Problem

BIRD exporter gives you great metrics about BGP session states, route counts, and protocol health. But it doesn't break down prefix lengths - essentially treating all routes equally. For understanding your BGP table composition, this granular data is interesting.

Looking at my setup receiving IPv6 full tables, I wanted to see something like:

  • /48 prefixes: 99,344 routes (customer allocations)
  • /32 prefixes: 25,763 routes (ISP allocations)
  • /40 prefixes: 23,172 routes
  • /44 prefixes: 20,789 routes
  • And so on...

The Naive Approach (That Doesn't Work)

My first instinct was straightforward: query all routes and parse prefix lengths.

show route table master6

This works fine for small routing tables. But with 220k routes, each generating ~4 lines of BIRD output (primary route, alternatives, interface info), you're looking at 880k lines of data.

Result: Unix socket buffer overflow. Truncated data. Only ~3,300 routes parsed instead of 220k.

Socket Buffer Reality Check

The core issue became clear quickly. BIRD's Unix socket has buffer limits, and massive route dumps exceed them. Even chunking by prefix length didn't help - a single /48 query returned 200k+ lines, still too large.

The Solution: Count Commands

Instead of parsing full route details, BIRD v2 supports efficient count queries:

show route table master6 where net ~ [::/0{48,48}] count

This returns just:

1007-197991 of 220785 routes for 220785 networks in table master6

Perfect! The number after the dash (197991) is exactly what we need - the count of /48 prefixes.

Implementation Details

The implementation queries each prefix length individually using BIRD's count command. Rather than checking all 128 possible IPv6 prefix lengths (which takes forever), I analyzed what actually exists in real BGP tables and built an optimized list. It's not bulletproof - new prefix lengths could appear - but otherwise parsing time becomes crazy.

for _, prefixLen := range prefixLengths {
    cmd := fmt.Sprintf("show route table %s where net ~ [::/0{%d,%d}] primary count", 
                      tableName, prefixLen, prefixLen)
    // Query and parse count...
}

The primary filter is crucial - it counts unique prefixes instead of duplicate advertisements from multiple peers. Without it, you get double-counting from overlapping route announcements.

Parsing BIRD v2 Output

BIRD v2's count format needed special handling:

1007-197991 of 220785 routes for 220785 networks in table master6

Where:

  • 1007 = response code
  • 197991 = filtered route count (what we want)
  • 220785 = total routes in table

A simple regex extracts the middle number:

countRegex := regexp.MustCompile(`^(\d+)-(\d+)\s+of\s+\d+\s+routes`)

Results

The implementation now exports clean Prometheus metrics:

# HELP bird_table_prefix_length_count Number of unique prefixes by prefix length in routing table
  # TYPE bird_table_prefix_length_count gauge
  bird_table_prefix_length_count{ip_version="6",prefix_length="48",table="master6"} 99344
  bird_table_prefix_length_count{ip_version="6",prefix_length="32",table="master6"} 25763
  bird_table_prefix_length_count{ip_version="6",prefix_length="40",table="master6"} 23172
  bird_table_prefix_length_count{ip_version="6",prefix_length="44",table="master6"} 20789
  bird_table_prefix_length_count{ip_version="6",prefix_length="36",table="master6"} 8633
  bird_table_prefix_length_count{ip_version="6",prefix_length="47",table="master6"} 6702
  bird_table_prefix_length_count{ip_version="6",prefix_length="29",table="master6"} 5184
  bird_table_prefix_length_count{ip_version="6",prefix_length="46",table="master6"} 4818
  bird_table_prefix_length_count{ip_version="6",prefix_length="33",table="master6"} 4812
  bird_table_prefix_length_count{ip_version="6",prefix_length="34",table="master6"} 4803
  bird_table_prefix_length_count{ip_version="6",prefix_length="45",table="master6"} 2754
  bird_table_prefix_length_count{ip_version="6",prefix_length="42",table="master6"} 2771
  bird_table_prefix_length_count{ip_version="6",prefix_length="38",table="master6"} 2282
  bird_table_prefix_length_count{ip_version="6",prefix_length="35",table="master6"} 1775
  bird_table_prefix_length_count{ip_version="6",prefix_length="39",table="master6"} 1751
  bird_table_prefix_length_count{ip_version="6",prefix_length="41",table="master6"} 1489
  bird_table_prefix_length_count{ip_version="6",prefix_length="37",table="master6"} 1299
  bird_table_prefix_length_count{ip_version="6",prefix_length="43",table="master6"} 1283
  bird_table_prefix_length_count{ip_version="6",prefix_length="30",table="master6"} 739
  bird_table_prefix_length_count{ip_version="6",prefix_length="31",table="master6"} 332
  bird_table_prefix_length_count{ip_version="6",prefix_length="28",table="master6"} 154
  bird_table_prefix_length_count{ip_version="6",prefix_length="24",table="master6"} 37
  bird_table_prefix_length_count{ip_version="6",prefix_length="26",table="master6"} 20
  bird_table_prefix_length_count{ip_version="6",prefix_length="27",table="master6"} 20
  bird_table_prefix_length_count{ip_version="6",prefix_length="20",table="master6"} 12
  bird_table_prefix_length_count{ip_version="6",prefix_length="25",table="master6"} 11
  bird_table_prefix_length_count{ip_version="6",prefix_length="64",table="master6"} 9
  bird_table_prefix_length_count{ip_version="6",prefix_length="23",table="master6"} 6
  bird_table_prefix_length_count{ip_version="6",prefix_length="22",table="master6"} 5
  bird_table_prefix_length_count{ip_version="6",prefix_length="128",table="master6"} 3
  bird_table_prefix_length_count{ip_version="6",prefix_length="126",table="master6"} 3
  bird_table_prefix_length_count{ip_version="6",prefix_length="21",table="master6"} 2
  bird_table_prefix_length_count{ip_version="6",prefix_length="16",table="master6"} 1
  bird_table_prefix_length_count{ip_version="6",prefix_length="19",table="master6"} 1
  bird_table_prefix_length_count{ip_version="6",prefix_length="3",table="master6"} 1
  bird_table_prefix_length_count{ip_version="6",prefix_length="127",table="master6"} 1


Perfect for Grafana dashboards showing prefix length distribution over time.

Usage

The feature is enabled with a simple flag:

bird_exporter -bird.v2 -bird.socket /var/run/bird/bird.ctl -prefix.size.table=true

It automatically detects both IPv4 and IPv6 BGP tables, generating separate metrics for each IP version.

Technical Lessons

  1. Don't parse what you can count - BIRD's count commands are far more efficient than full route parsing
  2. Socket buffers have limits - Large datasets require different approaches than small ones
  3. Read the manual - BIRD v2's filter syntax and count capabilities weren't immediately obvious
  4. Test with real data - Small test datasets hide scalability issues

The Code

The implementation is available on my GitHub fork with the complete table-wide prefix statistics feature.