Skip to content

Commit 2cbc4a4

Browse files
committed
l2: add bridge and FDB statistics
Expose per-bridge forwarding, drop and learning counters aggregated across all datapath cores. Each counter category is maintained in per-core arrays to avoid cache contention in the datapath, then summed on demand when the control plane queries them. The bridge_stats structure tracks unicast, broadcast, multicast and flood forwarding, various drop reasons (no FDB entry, hairpin, interface down), and learning outcomes (success, update, failure, limit exceeded). The fdb_stats structure tracks lookup hits, misses and aged entries. Statistics are cleared when a bridge is destroyed. A reset API allows clearing counters without destroying the bridge. CLI commands are exposed under "bridge stats show/reset". Add the "bridge" CLI command group to generated man pages. Signed-off-by: Fabien Dupont <fdupont@redhat.com>
1 parent 9398ec0 commit 2cbc4a4

9 files changed

Lines changed: 308 additions & 4 deletions

File tree

docs/meson.build

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ custom_target(
7474
# Individual command man pages
7575
# The list is hardcoded since we can't run grcli during meson configuration.
7676
grcli_commands = [
77-
'address', 'affinity', 'conntrack', 'dnat44', 'events', 'fdb', 'flood',
78-
'graph', 'interface', 'logging', 'nexthop', 'ping', 'ping6', 'route',
79-
'router-advert', 'snat44', 'stats', 'trace', 'traceroute', 'traceroute6',
80-
'tunsrc',
77+
'address', 'affinity', 'bridge', 'conntrack', 'dnat44', 'events', 'fdb',
78+
'flood', 'graph', 'interface', 'logging', 'nexthop', 'ping', 'ping6',
79+
'route', 'router-advert', 'snat44', 'stats', 'trace', 'traceroute',
80+
'traceroute6', 'tunsrc',
8181
]
8282

8383
foreach cmd : grcli_commands

modules/l2/api/gr_l2.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,38 @@ struct gr_flood_list_req {
205205
};
206206

207207
STREAM_RESP(struct gr_flood_entry);
208+
209+
// Bridge statistics //////////////////////////////////////////////////////////
210+
211+
#define GR_L2_STATS_GET REQUEST_TYPE(GR_L2_MODULE, 0x0020)
212+
213+
struct gr_l2_stats_get_req {
214+
uint16_t bridge_id;
215+
};
216+
217+
struct gr_l2_bridge_stats {
218+
uint16_t bridge_id;
219+
uint64_t unicast_fwd;
220+
uint64_t broadcast_fwd;
221+
uint64_t multicast_fwd;
222+
uint64_t flood_fwd;
223+
uint64_t no_fdb_drop;
224+
uint64_t hairpin_drop;
225+
uint64_t iface_down_drop;
226+
uint64_t learn_ok;
227+
uint64_t learn_update;
228+
uint64_t learn_fail;
229+
uint64_t learn_skip;
230+
uint64_t learn_limit_bridge;
231+
uint64_t learn_limit_iface;
232+
uint64_t learn_shutdown;
233+
uint64_t fdb_lookup_hit;
234+
uint64_t fdb_lookup_miss;
235+
uint64_t fdb_entries_aged;
236+
};
237+
238+
#define GR_L2_STATS_RESET REQUEST_TYPE(GR_L2_MODULE, 0x0021)
239+
240+
struct gr_l2_stats_reset_req {
241+
uint16_t bridge_id;
242+
};

modules/l2/cli/l2_stats.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
// Copyright (c) 2026 Fabien Dupont
3+
4+
#include <gr_api.h>
5+
#include <gr_cli.h>
6+
#include <gr_cli_iface.h>
7+
#include <gr_l2.h>
8+
9+
#include <ecoli.h>
10+
11+
#include <errno.h>
12+
#include <stdio.h>
13+
14+
static cmd_status_t bridge_stats_show(struct gr_api_client *c, const struct ec_pnode *p) {
15+
const struct gr_l2_bridge_stats *stats;
16+
struct gr_l2_stats_get_req req;
17+
void *resp_ptr = NULL;
18+
struct gr_iface *iface;
19+
20+
iface = iface_from_name(c, arg_str(p, "NAME"));
21+
if (iface == NULL)
22+
return CMD_ERROR;
23+
24+
req.bridge_id = iface->id;
25+
free(iface);
26+
27+
if (gr_api_client_send_recv(c, GR_L2_STATS_GET, sizeof(req), &req, &resp_ptr) < 0)
28+
return CMD_ERROR;
29+
30+
stats = resp_ptr;
31+
32+
printf("forwarding:\n");
33+
printf(" unicast: %lu\n", stats->unicast_fwd);
34+
printf(" broadcast: %lu\n", stats->broadcast_fwd);
35+
printf(" multicast: %lu\n", stats->multicast_fwd);
36+
printf(" flood: %lu\n", stats->flood_fwd);
37+
printf("drops:\n");
38+
printf(" no_fdb: %lu\n", stats->no_fdb_drop);
39+
printf(" hairpin: %lu\n", stats->hairpin_drop);
40+
printf(" iface_down: %lu\n", stats->iface_down_drop);
41+
printf("learning:\n");
42+
printf(" learned: %lu\n", stats->learn_ok);
43+
printf(" updated: %lu\n", stats->learn_update);
44+
printf(" failed: %lu\n", stats->learn_fail);
45+
printf(" skipped: %lu\n", stats->learn_skip);
46+
printf(" limit_bridge: %lu\n", stats->learn_limit_bridge);
47+
printf(" limit_iface: %lu\n", stats->learn_limit_iface);
48+
printf(" shutdown: %lu\n", stats->learn_shutdown);
49+
printf("fdb:\n");
50+
printf(" lookup_hit: %lu\n", stats->fdb_lookup_hit);
51+
printf(" lookup_miss: %lu\n", stats->fdb_lookup_miss);
52+
printf(" entries_aged: %lu\n", stats->fdb_entries_aged);
53+
54+
free(resp_ptr);
55+
return CMD_SUCCESS;
56+
}
57+
58+
static cmd_status_t bridge_stats_reset(struct gr_api_client *c, const struct ec_pnode *p) {
59+
struct gr_l2_stats_reset_req req;
60+
struct gr_iface *iface;
61+
62+
iface = iface_from_name(c, arg_str(p, "NAME"));
63+
if (iface == NULL)
64+
return CMD_ERROR;
65+
66+
req.bridge_id = iface->id;
67+
free(iface);
68+
69+
if (gr_api_client_send_recv(c, GR_L2_STATS_RESET, sizeof(req), &req, NULL) < 0)
70+
return CMD_ERROR;
71+
72+
return CMD_SUCCESS;
73+
}
74+
75+
#define BRIDGE_STATS_CTX(root) \
76+
CLI_CONTEXT(root, CTX_ARG("bridge", "Bridge management."), CTX_ARG("stats", "Statistics."))
77+
78+
static int ctx_init(struct ec_node *root) {
79+
int ret;
80+
81+
ret = CLI_COMMAND(
82+
BRIDGE_STATS_CTX(root),
83+
"reset NAME",
84+
bridge_stats_reset,
85+
"Reset bridge statistics.",
86+
with_help(
87+
"Bridge interface name.",
88+
ec_node_dyn("NAME", complete_iface_names, INT2PTR(GR_IFACE_TYPE_BRIDGE))
89+
)
90+
);
91+
if (ret < 0)
92+
return ret;
93+
94+
ret = CLI_COMMAND(
95+
BRIDGE_STATS_CTX(root),
96+
"[show] NAME",
97+
bridge_stats_show,
98+
"Show bridge statistics.",
99+
with_help(
100+
"Bridge interface name.",
101+
ec_node_dyn("NAME", complete_iface_names, INT2PTR(GR_IFACE_TYPE_BRIDGE))
102+
)
103+
);
104+
if (ret < 0)
105+
return ret;
106+
107+
return 0;
108+
}
109+
110+
static struct cli_context ctx = {
111+
.name = "bridge stats",
112+
.init = ctx_init,
113+
};
114+
115+
static void __attribute__((constructor, used)) init(void) {
116+
cli_context_register(&ctx);
117+
}

modules/l2/cli/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ cli_src += files(
55
'bridge.c',
66
'flood.c',
77
'fdb.c',
8+
'l2_stats.c',
89
'vxlan.c',
910
)

modules/l2/control/bridge.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
#include <rte_ether.h>
1010
#include <rte_hash.h>
1111

12+
#include <string.h>
13+
14+
// Global statistics arrays.
15+
struct bridge_stats l2_bridge_stats[L2_MAX_BRIDGES][RTE_MAX_LCORE];
16+
struct fdb_stats l2_fdb_stats[L2_MAX_BRIDGES][RTE_MAX_LCORE];
17+
1218
static int bridge_reconfig(
1319
struct iface *iface,
1420
uint64_t set_attrs,
@@ -89,6 +95,12 @@ static int bridge_fini(struct iface *iface) {
8995
gr_event_push(GR_EVENT_IFACE_POST_RECONFIG, member);
9096
}
9197

98+
// Clear bridge statistics.
99+
if (iface->id < L2_MAX_BRIDGES) {
100+
memset(l2_bridge_stats[iface->id], 0, sizeof(l2_bridge_stats[0]));
101+
memset(l2_fdb_stats[iface->id], 0, sizeof(l2_fdb_stats[0]));
102+
}
103+
92104
fdb_purge_bridge(iface->id);
93105

94106
return 0;

modules/l2/control/gr_l2_control.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,55 @@
99
#include <gr_net_types.h>
1010

1111
#include <rte_ip.h>
12+
#include <rte_lcore.h>
1213
#include <rte_mbuf.h>
1314
#include <rte_udp.h>
1415
#include <rte_vxlan.h>
1516

1617
#include <stdint.h>
1718

19+
// Per-core bridge statistics, indexed by [bridge_id][lcore_id].
20+
struct bridge_stats {
21+
uint64_t unicast_fwd;
22+
uint64_t broadcast_fwd;
23+
uint64_t multicast_fwd;
24+
uint64_t flood_fwd;
25+
uint64_t no_fdb_drop;
26+
uint64_t hairpin_drop;
27+
uint64_t iface_down_drop;
28+
uint64_t learn_ok;
29+
uint64_t learn_update;
30+
uint64_t learn_fail;
31+
uint64_t learn_skip;
32+
uint64_t learn_limit_bridge;
33+
uint64_t learn_limit_iface;
34+
uint64_t learn_shutdown;
35+
};
36+
37+
// Per-core FDB statistics, indexed by [bridge_id][lcore_id].
38+
struct fdb_stats {
39+
uint64_t lookup_hit;
40+
uint64_t lookup_miss;
41+
uint64_t entries_aged;
42+
};
43+
44+
#define L2_MAX_BRIDGES 256
45+
46+
extern struct bridge_stats l2_bridge_stats[L2_MAX_BRIDGES][RTE_MAX_LCORE];
47+
extern struct fdb_stats l2_fdb_stats[L2_MAX_BRIDGES][RTE_MAX_LCORE];
48+
49+
static inline struct bridge_stats *bridge_get_stats(uint16_t bridge_id, uint16_t lcore_id) {
50+
if (bridge_id >= L2_MAX_BRIDGES)
51+
return NULL;
52+
return &l2_bridge_stats[bridge_id][lcore_id];
53+
}
54+
55+
static inline struct fdb_stats *fdb_get_stats(uint16_t bridge_id, uint16_t lcore_id) {
56+
if (bridge_id >= L2_MAX_BRIDGES)
57+
return NULL;
58+
return &l2_fdb_stats[bridge_id][lcore_id];
59+
}
60+
1861
// Internal bridge info structure.
1962
GR_IFACE_INFO(GR_IFACE_TYPE_BRIDGE, iface_info_bridge, {
2063
BASE(__gr_iface_info_bridge_base);

modules/l2/control/l2_stats.c

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
// Copyright (c) 2026 Fabien Dupont
3+
4+
#include <gr_api.h>
5+
#include <gr_iface.h>
6+
#include <gr_l2.h>
7+
#include <gr_l2_control.h>
8+
#include <gr_module.h>
9+
10+
#include <rte_lcore.h>
11+
12+
#include <stdlib.h>
13+
#include <string.h>
14+
15+
static struct api_out l2_stats_get(const void *request, struct api_ctx *) {
16+
const struct gr_l2_stats_get_req *req = request;
17+
const struct iface *iface;
18+
struct gr_l2_bridge_stats *resp;
19+
20+
iface = iface_from_id(req->bridge_id);
21+
if (iface == NULL || iface->type != GR_IFACE_TYPE_BRIDGE)
22+
return api_out(ENOENT, 0, NULL);
23+
24+
resp = calloc(1, sizeof(*resp));
25+
if (resp == NULL)
26+
return api_out(ENOMEM, 0, NULL);
27+
28+
resp->bridge_id = req->bridge_id;
29+
30+
for (unsigned lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
31+
struct bridge_stats *bs = bridge_get_stats(req->bridge_id, lcore_id);
32+
struct fdb_stats *fs = fdb_get_stats(req->bridge_id, lcore_id);
33+
34+
if (bs == NULL || fs == NULL)
35+
continue;
36+
37+
resp->unicast_fwd += bs->unicast_fwd;
38+
resp->broadcast_fwd += bs->broadcast_fwd;
39+
resp->multicast_fwd += bs->multicast_fwd;
40+
resp->flood_fwd += bs->flood_fwd;
41+
resp->no_fdb_drop += bs->no_fdb_drop;
42+
resp->hairpin_drop += bs->hairpin_drop;
43+
resp->iface_down_drop += bs->iface_down_drop;
44+
resp->learn_ok += bs->learn_ok;
45+
resp->learn_update += bs->learn_update;
46+
resp->learn_fail += bs->learn_fail;
47+
resp->learn_skip += bs->learn_skip;
48+
resp->learn_limit_bridge += bs->learn_limit_bridge;
49+
resp->learn_limit_iface += bs->learn_limit_iface;
50+
resp->learn_shutdown += bs->learn_shutdown;
51+
resp->fdb_lookup_hit += fs->lookup_hit;
52+
resp->fdb_lookup_miss += fs->lookup_miss;
53+
resp->fdb_entries_aged += fs->entries_aged;
54+
}
55+
56+
return api_out(0, sizeof(*resp), resp);
57+
}
58+
59+
static struct api_out l2_stats_reset(const void *request, struct api_ctx *) {
60+
const struct gr_l2_stats_reset_req *req = request;
61+
const struct iface *iface;
62+
63+
iface = iface_from_id(req->bridge_id);
64+
if (iface == NULL || iface->type != GR_IFACE_TYPE_BRIDGE)
65+
return api_out(ENOENT, 0, NULL);
66+
67+
if (req->bridge_id < L2_MAX_BRIDGES) {
68+
memset(l2_bridge_stats[req->bridge_id], 0, sizeof(l2_bridge_stats[0]));
69+
memset(l2_fdb_stats[req->bridge_id], 0, sizeof(l2_fdb_stats[0]));
70+
}
71+
72+
return api_out(0, 0, NULL);
73+
}
74+
75+
static struct gr_api_handler l2_stats_get_handler = {
76+
.name = "l2 stats get",
77+
.request_type = GR_L2_STATS_GET,
78+
.callback = l2_stats_get,
79+
};
80+
81+
static struct gr_api_handler l2_stats_reset_handler = {
82+
.name = "l2 stats reset",
83+
.request_type = GR_L2_STATS_RESET,
84+
.callback = l2_stats_reset,
85+
};
86+
87+
RTE_INIT(l2_stats_constructor) {
88+
gr_register_api_handler(&l2_stats_get_handler);
89+
gr_register_api_handler(&l2_stats_reset_handler);
90+
}

modules/l2/control/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ src += files(
55
'bridge.c',
66
'fdb.c',
77
'flood.c',
8+
'l2_stats.c',
89
'vxlan.c',
910
)
1011

smoke/bridge_test.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ if grcli fdb show iface p1 | grep .; then
4848
fail "fdb still contains entries for removed interface"
4949
fi
5050

51+
# verify bridge stats show non-zero forwarding counters
52+
grcli bridge stats br0 | grep -q 'unicast:' || fail "stats missing unicast field"
53+
grcli bridge stats reset br0
54+
grcli bridge stats br0 | grep -q 'unicast: 0' || fail "stats not reset"
55+
5156
grcli interface del br0
5257
if grcli fdb show | grep .; then
5358
fail "fdb still contains entries"

0 commit comments

Comments
 (0)