netstat_freebsd: add support for TCP state metrics

Metric added (gauge):
node_netstat_tcp_connections{state="CLOSED"}
node_netstat_tcp_connections{state="CLOSE_WAIT"}
node_netstat_tcp_connections{state="CLOSING"}
node_netstat_tcp_connections{state="ESTABLISHED"}
node_netstat_tcp_connections{state="FIN_WAIT_1"}
node_netstat_tcp_connections{state="FIN_WAIT_2"}
node_netstat_tcp_connections{state="LAST_ACK"}
node_netstat_tcp_connections{state="LISTEN"}
node_netstat_tcp_connections{state="SYN_RCVD"}
node_netstat_tcp_connections{state="SYN_SENT"}
node_netstat_tcp_connections{state="TIME_WAIT"}

Signed-off-by: Danilo Egea Gondolfo <danilo@FreeBSD.org>
This commit is contained in:
Danilo Egea Gondolfo 2025-04-27 17:22:00 +01:00
parent 2db27ff2fa
commit 4bb3f0315d
2 changed files with 80 additions and 4 deletions

View file

@ -17,6 +17,7 @@
package collector
import (
"encoding/binary"
"errors"
"fmt"
"log/slog"
@ -59,6 +60,16 @@ var (
ipv6ForwardTotal = "bsdNetstatIPv6ForwardTotal"
ipv6DeliveredTotal = "bsdNetstatIPv6DeliveredTotal"
tcpStates = []string{
"CLOSED", "LISTEN", "SYN_SENT", "SYN_RCVD",
"ESTABLISHED", "CLOSE_WAIT", "FIN_WAIT_1", "CLOSING",
"LAST_ACK", "FIN_WAIT_2", "TIME_WAIT",
}
tcpStatesMetric = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "netstat", "tcp_connections"),
"Number of TCP connections per state", []string{"state"}, nil)
counterMetrics = map[string]*prometheus.Desc{
// TCP stats
tcpSendTotal: prometheus.NewDesc(
@ -242,6 +253,30 @@ func getData(queryString string, expectedSize int) ([]byte, error) {
return data, nil
}
func getTCPStates() ([]uint64, error) {
// This sysctl returns an array of uint64
data, err := sysctlRaw("net.inet.tcp.states")
if err != nil {
return nil, err
}
if len(data)/8 != len(tcpStates) {
return nil, fmt.Errorf("invalid TCP states data: expected %d entries, found %d", len(tcpStates), len(data)/8)
}
states := make([]uint64, 0)
offset := 0
for range len(tcpStates) {
s := data[offset : offset+8]
offset += 8
states = append(states, binary.NativeEndian.Uint64(s))
}
return states, nil
}
type netStatCollector struct {
netStatMetric *prometheus.Desc
}
@ -309,6 +344,15 @@ func (c *netStatCollector) Update(ch chan<- prometheus.Metric) error {
)
}
tcpConnsPerStates, err := getTCPStates()
if err != nil {
return err
}
for i, value := range tcpConnsPerStates {
ch <- prometheus.MustNewConstMetric(tcpStatesMetric, prometheus.GaugeValue, float64(value), tcpStates[i])
}
return nil
}
@ -323,6 +367,16 @@ func getFreeBSDDataMock(sysctl string) []byte {
size := int(unsafe.Sizeof(C.struct_tcpstat{}))
return unsafe.Slice((*byte)(unsafe.Pointer(&tcpStats)), size)
} else if sysctl == "net.inet.tcp.states" {
tcpStatesSlice := make([]byte, 0, len(tcpStates)*8)
tcpStatesValues := []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
for _, value := range tcpStatesValues {
tcpStatesSlice = binary.NativeEndian.AppendUint64(tcpStatesSlice, value)
}
return tcpStatesSlice
} else if sysctl == "net.inet.udp.stats" {
udpStats := C.struct_udpstat{
udps_opackets: 1234,

View file

@ -62,6 +62,24 @@ func TestGetTCPMetrics(t *testing.T) {
}
}
func TestGetTCPStatesMetrics(t *testing.T) {
testSetup()
tcpData, err := getTCPStates()
if err != nil {
t.Fatal("unexpected error:", err)
}
expected := []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
for i, value := range tcpData {
if got, want := float64(value), float64(expected[i]); got != want {
t.Errorf("unexpected %s value: want %f, got %f", tcpStates[i], want, got)
}
}
}
func TestGetUDPMetrics(t *testing.T) {
testSetup()
@ -143,20 +161,24 @@ func TestGetIPv6Metrics(t *testing.T) {
}
func TestNetStatCollectorUpdate(t *testing.T) {
ch := make(chan prometheus.Metric, len(counterMetrics))
collector := &netStatCollector{
netStatMetric: prometheus.NewDesc("netstat_metric", "NetStat Metric", nil, nil),
}
totalMetrics := len(counterMetrics) + len(tcpStates)
ch := make(chan prometheus.Metric, totalMetrics)
err := collector.Update(ch)
if err != nil {
t.Fatal("unexpected error:", err)
}
if got, want := len(ch), len(counterMetrics); got != want {
t.Errorf("metric count mismatch: want %d, got %d", want, got)
if got, want := len(ch), totalMetrics; got != want {
t.Fatalf("metric count mismatch: want %d, got %d", want, got)
}
for range len(counterMetrics) {
for range totalMetrics {
<-ch
}
}