Skip to content

Commit 3b5280f

Browse files
committed
feat: implement nfqueue collector
Signed-off-by: Denis Voytyuk <5462781+denisvmedia@users.noreply.github.com>
1 parent be92d50 commit 3b5280f

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
0 31621 0 2 65531 0 0 50 1
2+
1 31622 100 1 1024 150 10 200 1
3+
2 31623 25 0 512 20 5 300 1

collector/nfqueue_linux.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build !nonfqueue
15+
16+
package collector
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
"log/slog"
22+
"os"
23+
"strconv"
24+
25+
"github.com/prometheus/client_golang/prometheus"
26+
"github.com/prometheus/procfs"
27+
)
28+
29+
type nfqueueCollector struct {
30+
fs procfs.FS
31+
queueLength *prometheus.Desc
32+
packetsDropped *prometheus.Desc
33+
info *prometheus.Desc
34+
logger *slog.Logger
35+
}
36+
37+
func init() {
38+
registerCollector("nfqueue", defaultDisabled, NewNFQueueCollector)
39+
}
40+
41+
// NewNFQueueCollector returns a new Collector exposing netfilter queue stats
42+
// from /proc/net/netfilter/nfnetlink_queue.
43+
func NewNFQueueCollector(logger *slog.Logger) (Collector, error) {
44+
fs, err := procfs.NewFS(*procPath)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to open procfs: %w", err)
47+
}
48+
49+
return &nfqueueCollector{
50+
fs: fs,
51+
queueLength: prometheus.NewDesc(
52+
prometheus.BuildFQName(namespace, "nfqueue", "queue_length"),
53+
"Current number of packets waiting in the queue.",
54+
[]string{"queue"}, nil,
55+
),
56+
packetsDropped: prometheus.NewDesc(
57+
prometheus.BuildFQName(namespace, "nfqueue", "packets_dropped_total"),
58+
"Total number of packets dropped.",
59+
[]string{"queue", "reason"}, nil,
60+
),
61+
// nfqueue_info cardinality:
62+
//
63+
// queue | Queue ID (uint16): 0-65535 in theory, but in practice single digits to low tens.
64+
// peer_portid | PID of the userspace listener: one per queue, only changes on process restart.
65+
// copy_mode | Packet copy mode: only 3 possible values (none/meta/packet) - set per queue.
66+
// copy_range | Bytes copied to userspace: 0-65535, but typically a single fixed value per queue (e.g. 65535 or MTU size).
67+
//
68+
// The total number of time series = number of NFQUEUE queues. Even in an extreme case of 100 queues (unrealistically many),
69+
// that's just 100 series.
70+
info: prometheus.NewDesc(
71+
prometheus.BuildFQName(namespace, "nfqueue", "info"),
72+
"Non-numeric metadata about the queue (value is always 1).",
73+
[]string{"queue", "peer_portid", "copy_mode", "copy_range"}, nil,
74+
),
75+
logger: logger,
76+
}, nil
77+
}
78+
79+
func (c *nfqueueCollector) Update(ch chan<- prometheus.Metric) error {
80+
queues, err := c.fs.NFNetLinkQueue()
81+
if err != nil {
82+
if errors.Is(err, os.ErrNotExist) {
83+
c.logger.Debug("nfqueue: file not found, NFQUEUE probably not in use")
84+
return ErrNoData
85+
}
86+
return fmt.Errorf("failed to retrieve nfqueue stats: %w", err)
87+
}
88+
89+
for _, q := range queues {
90+
queueID := strconv.FormatUint(uint64(q.QueueID), 10)
91+
ch <- prometheus.MustNewConstMetric(
92+
c.queueLength, prometheus.GaugeValue,
93+
float64(q.QueueTotal), queueID,
94+
)
95+
ch <- prometheus.MustNewConstMetric(
96+
c.packetsDropped, prometheus.CounterValue,
97+
float64(q.QueueDropped), queueID, "queue_full",
98+
)
99+
ch <- prometheus.MustNewConstMetric(
100+
c.packetsDropped, prometheus.CounterValue,
101+
float64(q.QueueUserDropped), queueID, "user",
102+
)
103+
ch <- prometheus.MustNewConstMetric(
104+
c.info, prometheus.GaugeValue, 1,
105+
queueID,
106+
strconv.FormatUint(uint64(q.PeerPID), 10),
107+
nfqueueCopyModeString(q.CopyMode),
108+
strconv.FormatUint(uint64(q.CopyRange), 10),
109+
)
110+
}
111+
return nil
112+
}
113+
114+
func nfqueueCopyModeString(mode uint) string {
115+
switch mode {
116+
case 0:
117+
return "none"
118+
case 1:
119+
return "meta"
120+
case 2:
121+
return "packet"
122+
default:
123+
return strconv.FormatUint(uint64(mode), 10)
124+
}
125+
}

collector/nfqueue_linux_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build !nonfqueue
15+
16+
package collector
17+
18+
import (
19+
"errors"
20+
"io"
21+
"log/slog"
22+
"strings"
23+
"testing"
24+
25+
"github.com/prometheus/client_golang/prometheus"
26+
"github.com/prometheus/client_golang/prometheus/testutil"
27+
)
28+
29+
type testNFQueueCollector struct {
30+
nc Collector
31+
}
32+
33+
func (c testNFQueueCollector) Collect(ch chan<- prometheus.Metric) {
34+
c.nc.Update(ch)
35+
}
36+
37+
func (c testNFQueueCollector) Describe(ch chan<- *prometheus.Desc) {
38+
prometheus.DescribeByCollect(c, ch)
39+
}
40+
41+
func TestNFQueueStats(t *testing.T) {
42+
testcase := `# HELP node_nfqueue_packets_dropped_total Total number of packets dropped.
43+
# TYPE node_nfqueue_packets_dropped_total counter
44+
node_nfqueue_packets_dropped_total{queue="0",reason="queue_full"} 0
45+
node_nfqueue_packets_dropped_total{queue="0",reason="user"} 0
46+
node_nfqueue_packets_dropped_total{queue="1",reason="queue_full"} 150
47+
node_nfqueue_packets_dropped_total{queue="1",reason="user"} 10
48+
node_nfqueue_packets_dropped_total{queue="2",reason="queue_full"} 20
49+
node_nfqueue_packets_dropped_total{queue="2",reason="user"} 5
50+
# HELP node_nfqueue_info Non-numeric metadata about the queue (value is always 1).
51+
# TYPE node_nfqueue_info gauge
52+
node_nfqueue_info{copy_mode="packet",copy_range="65531",peer_portid="31621",queue="0"} 1
53+
node_nfqueue_info{copy_mode="meta",copy_range="1024",peer_portid="31622",queue="1"} 1
54+
node_nfqueue_info{copy_mode="none",copy_range="512",peer_portid="31623",queue="2"} 1
55+
# HELP node_nfqueue_queue_length Current number of packets waiting in the queue.
56+
# TYPE node_nfqueue_queue_length gauge
57+
node_nfqueue_queue_length{queue="0"} 0
58+
node_nfqueue_queue_length{queue="1"} 100
59+
node_nfqueue_queue_length{queue="2"} 25
60+
`
61+
*procPath = "fixtures/proc"
62+
63+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
64+
c, err := NewNFQueueCollector(logger)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
reg := prometheus.NewRegistry()
69+
reg.MustRegister(&testNFQueueCollector{nc: c})
70+
71+
err = testutil.GatherAndCompare(reg, strings.NewReader(testcase))
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
}
76+
77+
func TestNFQueueStatsErrNoData(t *testing.T) {
78+
*procPath = t.TempDir() // valid dir, but no nfnetlink_queue file inside
79+
80+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
81+
c, err := NewNFQueueCollector(logger)
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
ch := make(chan prometheus.Metric)
87+
err = c.Update(ch)
88+
if !errors.Is(err, ErrNoData) {
89+
t.Fatalf("expected ErrNoData, got: %v", err)
90+
}
91+
}

0 commit comments

Comments
 (0)