diff --git a/docs/collector.tcp.md b/docs/collector.tcp.md index 0b1f1792..a3f46c36 100644 --- a/docs/collector.tcp.md +++ b/docs/collector.tcp.md @@ -26,6 +26,7 @@ Name | Description | Type | Labels `windows_tcp_segments_received_total` | Total segments received, including those received in error. This count includes segments received on currently established connections | counter | af `windows_tcp_segments_retransmitted_total` | Total segments retransmitted. That is, segments transmitted that contain one or more previously transmitted bytes | counter | af `windows_tcp_segments_sent_total` | Total segments sent, including those on current connections, but excluding those containing *only* retransmitted bytes | counter | af +`windows_tcp_connections_state_count` | Number of TCP connections by state among: CLOSED, LISTENING, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT1, FIN_WAIT2, CLOSE_WAIT, CLOSING, LAST_ACK, TIME_WAIT, DELETE_TCB | gauge | af ### Example metric _This collector does not yet have explained examples, we would appreciate your help adding them!_ diff --git a/pkg/collector/tcp/const.go b/pkg/collector/tcp/const.go index bffadfe9..a0a8c66a 100644 --- a/pkg/collector/tcp/const.go +++ b/pkg/collector/tcp/const.go @@ -3,6 +3,7 @@ package tcp // Win32_PerfRawData_Tcpip_TCPv4 docs // - https://msdn.microsoft.com/en-us/library/aa394341(v=vs.85).aspx // The TCPv6 performance object uses the same fields. +// https://learn.microsoft.com/en-us/dotnet/api/system.net.networkinformation.tcpstate?view=net-8.0. const ( connectionFailures = "Connection Failures" connectionsActive = "Connections Active" diff --git a/pkg/collector/tcp/tcp.go b/pkg/collector/tcp/tcp.go index 34fcfbf8..2892a7df 100644 --- a/pkg/collector/tcp/tcp.go +++ b/pkg/collector/tcp/tcp.go @@ -7,10 +7,12 @@ import ( "log/slog" "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/pkg/headers/iphlpapi" "github.com/prometheus-community/windows_exporter/pkg/perfdata" "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" + "golang.org/x/sys/windows" ) const Name = "tcp" @@ -35,6 +37,7 @@ type Collector struct { segmentsReceivedTotal *prometheus.Desc segmentsRetransmittedTotal *prometheus.Desc segmentsSentTotal *prometheus.Desc + connectionsStateCount *prometheus.Desc } func New(config *Config) *Collector { @@ -144,6 +147,11 @@ func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { []string{"af"}, nil, ) + c.connectionsStateCount = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "connections_state_count"), + "Number of TCP connections by state and address family", + []string{"af", "state"}, nil, + ) return nil } @@ -160,10 +168,36 @@ func (c *Collector) Collect(_ *types.ScrapeContext, logger *slog.Logger, ch chan return err } + if err := c.collectConnectionsState(ch); err != nil { + logger.Error("failed collecting tcp connection state metrics", + slog.Any("err", err), + ) + + return err + } + return nil } -func writeTCPCounters(metrics map[string]perfdata.CounterValues, labels []string, c *Collector, ch chan<- prometheus.Metric) { +func (c *Collector) collect(ch chan<- prometheus.Metric) error { + data, err := c.perfDataCollector4.Collect() + if err != nil { + return fmt.Errorf("failed to collect TCPv4 metrics: %w", err) + } + + c.writeTCPCounters(ch, data[perfdata.EmptyInstance], []string{"ipv4"}) + + data, err = c.perfDataCollector6.Collect() + if err != nil { + return fmt.Errorf("failed to collect TCPv6 metrics: %w", err) + } + + c.writeTCPCounters(ch, data[perfdata.EmptyInstance], []string{"ipv6"}) + + return nil +} + +func (c *Collector) writeTCPCounters(ch chan<- prometheus.Metric, metrics map[string]perfdata.CounterValues, labels []string) { ch <- prometheus.MustNewConstMetric( c.connectionFailures, prometheus.CounterValue, @@ -220,20 +254,32 @@ func writeTCPCounters(metrics map[string]perfdata.CounterValues, labels []string ) } -func (c *Collector) collect(ch chan<- prometheus.Metric) error { - data, err := c.perfDataCollector4.Collect() +func (c *Collector) collectConnectionsState(ch chan<- prometheus.Metric) error { + stateCounts, err := iphlpapi.GetTCPConnectionStates(windows.AF_INET) if err != nil { - return fmt.Errorf("failed to collect TCPv4 metrics: %w", err) + return fmt.Errorf("failed to collect TCP connection states for %s: %w", "ipv4", err) } - writeTCPCounters(data[perfdata.EmptyInstance], []string{"ipv4"}, c, ch) + c.sendTCPStateMetrics(ch, stateCounts, "ipv4") - data, err = c.perfDataCollector6.Collect() + stateCounts, err = iphlpapi.GetTCPConnectionStates(windows.AF_INET6) if err != nil { - return fmt.Errorf("failed to collect TCPv6 metrics: %w", err) + return fmt.Errorf("failed to collect TCP6 connection states for %s: %w", "ipv6", err) } - writeTCPCounters(data[perfdata.EmptyInstance], []string{"ipv6"}, c, ch) + c.sendTCPStateMetrics(ch, stateCounts, "ipv6") return nil } + +func (c *Collector) sendTCPStateMetrics(ch chan<- prometheus.Metric, stateCounts map[iphlpapi.MIB_TCP_STATE]uint32, af string) { + for state, count := range stateCounts { + ch <- prometheus.MustNewConstMetric( + c.connectionsStateCount, + prometheus.GaugeValue, + float64(count), + af, + state.String(), + ) + } +} diff --git a/pkg/headers/iphlpapi/const.go b/pkg/headers/iphlpapi/const.go new file mode 100644 index 00000000..60e986b8 --- /dev/null +++ b/pkg/headers/iphlpapi/const.go @@ -0,0 +1,6 @@ +package iphlpapi + +const ( + TCPTableClass uint32 = 5 + TCP6TableClass uint32 = 5 +) diff --git a/pkg/headers/iphlpapi/iphlpapi.go b/pkg/headers/iphlpapi/iphlpapi.go new file mode 100644 index 00000000..9f80c6a4 --- /dev/null +++ b/pkg/headers/iphlpapi/iphlpapi.go @@ -0,0 +1,77 @@ +package iphlpapi + +import ( + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable") +) + +func GetTCPConnectionStates(family uint32) (map[MIB_TCP_STATE]uint32, error) { + var size uint32 + + stateCounts := make(map[MIB_TCP_STATE]uint32) + rowSize := uint32(unsafe.Sizeof(MIB_TCPROW_OWNER_PID{})) + tableClass := TCPTableClass + + if family == windows.AF_INET6 { + rowSize = uint32(unsafe.Sizeof(MIB_TCP6ROW_OWNER_PID{})) + tableClass = TCP6TableClass + } + + ret := getExtendedTcpTable(0, &size, true, family, tableClass, 0) + if ret != 0 && ret != uintptr(windows.ERROR_INSUFFICIENT_BUFFER) { + return nil, fmt.Errorf("getExtendedTcpTable (size query) failed with code %d", ret) + } + + buf := make([]byte, size) + + ret = getExtendedTcpTable(uintptr(unsafe.Pointer(&buf[0])), &size, true, family, tableClass, 0) + if ret != 0 { + return nil, fmt.Errorf("getExtendedTcpTable (data query) failed with code %d", ret) + } + + numEntries := *(*uint32)(unsafe.Pointer(&buf[0])) + + for i := range numEntries { + var state MIB_TCP_STATE + + if family == windows.AF_INET6 { + row := (*MIB_TCP6ROW_OWNER_PID)(unsafe.Pointer(&buf[4+i*rowSize])) + state = row.dwState + } else { + row := (*MIB_TCPROW_OWNER_PID)(unsafe.Pointer(&buf[4+i*rowSize])) + state = row.dwState + } + + stateCounts[state]++ + } + + return stateCounts, nil +} + +func getExtendedTcpTable(pTCPTable uintptr, pdwSize *uint32, bOrder bool, ulAf uint32, tableClass uint32, reserved uint32) uintptr { + ret, _, _ := procGetExtendedTcpTable.Call( + pTCPTable, + uintptr(unsafe.Pointer(pdwSize)), + uintptr(boolToInt(bOrder)), + uintptr(ulAf), + uintptr(tableClass), + uintptr(reserved), + ) + + return ret +} + +func boolToInt(b bool) int { + if b { + return 1 + } + + return 0 +} diff --git a/pkg/headers/iphlpapi/types.go b/pkg/headers/iphlpapi/types.go new file mode 100644 index 00000000..5bad9af5 --- /dev/null +++ b/pkg/headers/iphlpapi/types.go @@ -0,0 +1,76 @@ +package iphlpapi + +import "fmt" + +// MIB_TCPROW_OWNER_PID structure for IPv4. +// https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcprow_owner_pid +type MIB_TCPROW_OWNER_PID struct { + dwState MIB_TCP_STATE + dwLocalAddr uint32 + dwLocalPort uint32 + dwRemoteAddr uint32 + dwRemotePort uint32 + dwOwningPid uint32 +} + +// MIB_TCP6ROW_OWNER_PID structure for IPv6. +// https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcp6row_owner_pid +type MIB_TCP6ROW_OWNER_PID struct { + ucLocalAddr [16]byte + dwLocalScopeId uint32 + dwLocalPort uint32 + ucRemoteAddr [16]byte + dwRemoteScopeId uint32 + dwRemotePort uint32 + dwState MIB_TCP_STATE + dwOwningPid uint32 +} + +type MIB_TCP_STATE uint32 + +const ( + _ MIB_TCP_STATE = iota + TCPStateClosed + TCPStateListening + TCPStateSynSent + TCPStateSynRcvd + TCPStateEstablished + TCPStateFinWait1 + TCPStateFinWait2 + TCPStateCloseWait + TCPStateClosing + TCPStateLastAck + TCPStateTimeWait + TCPStateDeleteTcb +) + +func (state MIB_TCP_STATE) String() string { + switch state { + case TCPStateClosed: + return "CLOSED" + case TCPStateListening: + return "LISTENING" + case TCPStateSynSent: + return "SYN_SENT" + case TCPStateSynRcvd: + return "SYN_RECEIVED" + case TCPStateEstablished: + return "ESTABLISHED" + case TCPStateFinWait1: + return "FIN_WAIT1" + case TCPStateFinWait2: + return "FIN_WAIT2" + case TCPStateCloseWait: + return "CLOSE_WAIT" + case TCPStateClosing: + return "CLOSING" + case TCPStateLastAck: + return "LAST_ACK" + case TCPStateTimeWait: + return "TIME_WAIT" + case TCPStateDeleteTcb: + return "DELETE_TCB" + default: + return fmt.Sprintf("UNKNOWN_%d", state) + } +}