diff --git a/collector/netstat_freebsd.go b/collector/netstat_freebsd.go index 903e97d2..39ac46b8 100644 --- a/collector/netstat_freebsd.go +++ b/collector/netstat_freebsd.go @@ -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, diff --git a/collector/netstat_freebsd_test.go b/collector/netstat_freebsd_test.go index 3d00f3de..a5e42663 100644 --- a/collector/netstat_freebsd_test.go +++ b/collector/netstat_freebsd_test.go @@ -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 } }