From 2155d3477995f8aec3be4712e44a708fda34aa2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Tue, 1 Oct 2024 23:44:53 +0200 Subject: [PATCH] net: expose network interfaces address (#1635) --- pkg/collector/net/const.go | 36 ++++ pkg/collector/net/net.go | 368 ++++++++++++++++++++++++++++------ pkg/collector/net/net_test.go | 21 -- pkg/collector/tcp/tcp.go | 64 ++++-- tools/e2e-output.txt | 2 + 5 files changed, 400 insertions(+), 91 deletions(-) create mode 100644 pkg/collector/net/const.go delete mode 100644 pkg/collector/net/net_test.go diff --git a/pkg/collector/net/const.go b/pkg/collector/net/const.go new file mode 100644 index 000000000..352866115 --- /dev/null +++ b/pkg/collector/net/const.go @@ -0,0 +1,36 @@ +package net + +const ( + BytesReceivedPerSec = "Bytes Received/sec" + BytesSentPerSec = "Bytes Sent/sec" + BytesTotalPerSec = "Bytes Total/sec" + OutputQueueLength = "Output Queue Length" + PacketsOutboundDiscarded = "Packets Outbound Discarded" + PacketsOutboundErrors = "Packets Outbound Errors" + PacketsPerSec = "Packets/sec" + PacketsReceivedDiscarded = "Packets Received Discarded" + PacketsReceivedErrors = "Packets Received Errors" + PacketsReceivedPerSec = "Packets Received/sec" + PacketsReceivedUnknown = "Packets Received Unknown" + PacketsSentPerSec = "Packets Sent/sec" + CurrentBandwidth = "Current Bandwidth" +) + +// Win32_PerfRawData_Tcpip_NetworkInterface docs: +// - https://technet.microsoft.com/en-us/security/aa394340(v=vs.80) +type perflibNetworkInterface struct { + BytesReceivedPerSec float64 `perflib:"Bytes Received/sec"` + BytesSentPerSec float64 `perflib:"Bytes Sent/sec"` + BytesTotalPerSec float64 `perflib:"Bytes Total/sec"` + Name string + OutputQueueLength float64 `perflib:"Output Queue Length"` + PacketsOutboundDiscarded float64 `perflib:"Packets Outbound Discarded"` + PacketsOutboundErrors float64 `perflib:"Packets Outbound Errors"` + PacketsPerSec float64 `perflib:"Packets/sec"` + PacketsReceivedDiscarded float64 `perflib:"Packets Received Discarded"` + PacketsReceivedErrors float64 `perflib:"Packets Received Errors"` + PacketsReceivedPerSec float64 `perflib:"Packets Received/sec"` + PacketsReceivedUnknown float64 `perflib:"Packets Received Unknown"` + PacketsSentPerSec float64 `perflib:"Packets Sent/sec"` + CurrentBandwidth float64 `perflib:"Current Bandwidth"` +} diff --git a/pkg/collector/net/net.go b/pkg/collector/net/net.go index 4bf449336..317b32386 100644 --- a/pkg/collector/net/net.go +++ b/pkg/collector/net/net.go @@ -3,35 +3,48 @@ package net import ( + "errors" "fmt" "log/slog" + "os" "regexp" + "slices" + "strings" + "unsafe" "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/pkg/perfdata" "github.com/prometheus-community/windows_exporter/pkg/perflib" "github.com/prometheus-community/windows_exporter/pkg/types" + "github.com/prometheus-community/windows_exporter/pkg/utils" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" + "golang.org/x/sys/windows" ) const Name = "net" type Config struct { - NicExclude *regexp.Regexp `yaml:"nic_exclude"` - NicInclude *regexp.Regexp `yaml:"nic_include"` + NicExclude *regexp.Regexp `yaml:"nic_exclude"` + NicInclude *regexp.Regexp `yaml:"nic_include"` + CollectorsEnabled []string `yaml:"collectors_enabled"` } var ConfigDefaults = Config{ NicExclude: types.RegExpEmpty, NicInclude: types.RegExpAny, + CollectorsEnabled: []string{ + "metrics", + "nic_addresses", + }, } -var nicNameToUnderscore = regexp.MustCompile("[^a-zA-Z0-9]") - // A Collector is a Prometheus Collector for Perflib Network Interface metrics. type Collector struct { config Config + perfDataCollector *perfdata.Collector + bytesReceivedTotal *prometheus.Desc bytesSentTotal *prometheus.Desc bytesTotal *prometheus.Desc @@ -45,6 +58,9 @@ type Collector struct { packetsReceivedUnknown *prometheus.Desc packetsSentTotal *prometheus.Desc currentBandwidth *prometheus.Desc + + nicAddressInfo *prometheus.Desc + routeInfo *prometheus.Desc } func New(config *Config) *Collector { @@ -60,6 +76,10 @@ func New(config *Config) *Collector { config.NicInclude = ConfigDefaults.NicInclude } + if config.CollectorsEnabled == nil { + config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled + } + c := &Collector{ config: *config, } @@ -71,9 +91,12 @@ func NewWithFlags(app *kingpin.Application) *Collector { c := &Collector{ config: ConfigDefaults, } + c.config.CollectorsEnabled = make([]string, 0) var nicExclude, nicInclude string + var collectorsEnabled string + app.Flag( "collector.net.nic-exclude", "Regexp of NIC:s to exclude. NIC name must both match include and not match exclude to be included.", @@ -84,7 +107,14 @@ func NewWithFlags(app *kingpin.Application) *Collector { "Regexp of NIC:s to include. NIC name must both match include and not match exclude to be included.", ).Default(c.config.NicInclude.String()).StringVar(&nicInclude) + app.Flag( + "collector.net.enabled", + "Comma-separated list of collectors to use. Defaults to all, if not specified.", + ).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled) + app.Action(func(*kingpin.ParseContext) error { + c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",") + var err error c.config.NicExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", nicExclude)) @@ -108,6 +138,10 @@ func (c *Collector) GetName() string { } func (c *Collector) GetPerfCounter(_ *slog.Logger) ([]string, error) { + if utils.PDHEnabled() { + return []string{}, nil + } + return []string{"Network Interface"}, nil } @@ -115,7 +149,38 @@ func (c *Collector) Close(_ *slog.Logger) error { return nil } -func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { +func (c *Collector) Build(logger *slog.Logger, _ *wmi.Client) error { + if utils.PDHEnabled() { + counters := []string{ + BytesReceivedPerSec, + BytesSentPerSec, + BytesTotalPerSec, + OutputQueueLength, + PacketsOutboundDiscarded, + PacketsOutboundErrors, + PacketsPerSec, + PacketsReceivedDiscarded, + PacketsReceivedErrors, + PacketsReceivedPerSec, + PacketsReceivedUnknown, + PacketsSentPerSec, + CurrentBandwidth, + } + + var err error + + c.perfDataCollector, err = perfdata.NewCollector("Network Interface", []string{"*"}, counters) + if err != nil { + return fmt.Errorf("failed to create Processor Information collector: %w", err) + } + } + + if slices.Contains(c.config.CollectorsEnabled, "addresses") { + logger.Info("nic/addresses collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.", + slog.String("collector", Name), + ) + } + c.bytesReceivedTotal = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "bytes_received_total"), "(Network.BytesReceivedPerSec)", @@ -194,6 +259,18 @@ func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { []string{"nic"}, nil, ) + c.nicAddressInfo = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "nic_address_info"), + "A metric with a constant '1' value labeled with the network interface's address information.", + []string{"nic", "friendly_name", "address", "family"}, + nil, + ) + c.routeInfo = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "route_info"), + "A metric with a constant '1' value labeled with the network interface's route information.", + []string{"nic", "src", "dest", "metric"}, + nil, + ) return nil } @@ -202,46 +279,32 @@ func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { // to the provided prometheus Metric channel. func (c *Collector) Collect(ctx *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { logger = logger.With(slog.String("collector", Name)) - if err := c.collect(ctx, logger, ch); err != nil { - logger.Error("failed collecting net metrics", - slog.Any("err", err), - ) - return err - } + if slices.Contains(c.config.CollectorsEnabled, "metrics") { + var err error - return nil -} + if utils.PDHEnabled() { + err = c.collectPDH(ch) + } else { + err = c.collect(ctx, logger, ch) + } -// mangleNetworkName mangles Network Adapter name (non-alphanumeric to _) -// that is used in networkInterface. -func mangleNetworkName(name string) string { - return nicNameToUnderscore.ReplaceAllString(name, "_") -} + if err != nil { + return fmt.Errorf("failed collecting net metrics: %w", err) + } + } -// Win32_PerfRawData_Tcpip_NetworkInterface docs: -// - https://technet.microsoft.com/en-us/security/aa394340(v=vs.80) -type networkInterface struct { - BytesReceivedPerSec float64 `perflib:"Bytes Received/sec"` - BytesSentPerSec float64 `perflib:"Bytes Sent/sec"` - BytesTotalPerSec float64 `perflib:"Bytes Total/sec"` - Name string - OutputQueueLength float64 `perflib:"Output Queue Length"` - PacketsOutboundDiscarded float64 `perflib:"Packets Outbound Discarded"` - PacketsOutboundErrors float64 `perflib:"Packets Outbound Errors"` - PacketsPerSec float64 `perflib:"Packets/sec"` - PacketsReceivedDiscarded float64 `perflib:"Packets Received Discarded"` - PacketsReceivedErrors float64 `perflib:"Packets Received Errors"` - PacketsReceivedPerSec float64 `perflib:"Packets Received/sec"` - PacketsReceivedUnknown float64 `perflib:"Packets Received Unknown"` - PacketsSentPerSec float64 `perflib:"Packets Sent/sec"` - CurrentBandwidth float64 `perflib:"Current Bandwidth"` + if slices.Contains(c.config.CollectorsEnabled, "nic_addresses") { + if err := c.collectNICAddresses(ch); err != nil { + return fmt.Errorf("failed collecting net addresses: %w", err) + } + } + + return nil } func (c *Collector) collect(ctx *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { - logger = logger.With(slog.String("collector", Name)) - - var dst []networkInterface + var dst []perflibNetworkInterface if err := perflib.UnmarshalObject(ctx.PerfObjects["Network Interface"], &dst, logger); err != nil { return err @@ -253,91 +316,284 @@ func (c *Collector) collect(ctx *types.ScrapeContext, logger *slog.Logger, ch ch continue } - name := mangleNetworkName(nic.Name) - if name == "" { - continue - } - // Counters ch <- prometheus.MustNewConstMetric( c.bytesReceivedTotal, prometheus.CounterValue, nic.BytesReceivedPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.bytesSentTotal, prometheus.CounterValue, nic.BytesSentPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.bytesTotal, prometheus.CounterValue, nic.BytesTotalPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.outputQueueLength, prometheus.GaugeValue, nic.OutputQueueLength, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsOutboundDiscarded, prometheus.CounterValue, nic.PacketsOutboundDiscarded, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsOutboundErrors, prometheus.CounterValue, nic.PacketsOutboundErrors, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsTotal, prometheus.CounterValue, nic.PacketsPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsReceivedDiscarded, prometheus.CounterValue, nic.PacketsReceivedDiscarded, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsReceivedErrors, prometheus.CounterValue, nic.PacketsReceivedErrors, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsReceivedTotal, prometheus.CounterValue, nic.PacketsReceivedPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsReceivedUnknown, prometheus.CounterValue, nic.PacketsReceivedUnknown, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.packetsSentTotal, prometheus.CounterValue, nic.PacketsSentPerSec, - name, + nic.Name, ) ch <- prometheus.MustNewConstMetric( c.currentBandwidth, prometheus.GaugeValue, nic.CurrentBandwidth/8, - name, + nic.Name, ) } return nil } + +func (c *Collector) collectPDH(ch chan<- prometheus.Metric) error { + data, err := c.perfDataCollector.Collect() + if err != nil { + return fmt.Errorf("failed to collect Network Information metrics: %w", err) + } + + for nicName, nicData := range data { + if c.config.NicExclude.MatchString(nicName) || + !c.config.NicInclude.MatchString(nicName) { + continue + } + + // Counters + ch <- prometheus.MustNewConstMetric( + c.bytesReceivedTotal, + prometheus.CounterValue, + nicData[BytesReceivedPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.bytesSentTotal, + prometheus.CounterValue, + nicData[BytesSentPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.bytesTotal, + prometheus.CounterValue, + nicData[BytesTotalPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.outputQueueLength, + prometheus.GaugeValue, + nicData[OutputQueueLength].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsOutboundDiscarded, + prometheus.CounterValue, + nicData[PacketsOutboundDiscarded].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsOutboundErrors, + prometheus.CounterValue, + nicData[PacketsOutboundErrors].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsTotal, + prometheus.CounterValue, + nicData[PacketsPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsReceivedDiscarded, + prometheus.CounterValue, + nicData[PacketsReceivedDiscarded].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsReceivedErrors, + prometheus.CounterValue, + nicData[PacketsReceivedErrors].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsReceivedTotal, + prometheus.CounterValue, + nicData[PacketsReceivedPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsReceivedUnknown, + prometheus.CounterValue, + nicData[PacketsReceivedUnknown].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.packetsSentTotal, + prometheus.CounterValue, + nicData[PacketsSentPerSec].FirstValue, + nicName, + ) + ch <- prometheus.MustNewConstMetric( + c.currentBandwidth, + prometheus.GaugeValue, + nicData[CurrentBandwidth].FirstValue/8, + nicName, + ) + } + + return nil +} + +var addressFamily = map[uint16]string{ + windows.AF_INET: "ipv4", + windows.AF_INET6: "ipv6", +} + +func (c *Collector) collectNICAddresses(ch chan<- prometheus.Metric) error { + nicAdapterAddresses, err := adapterAddresses() + if err != nil { + return err + } + + convertNicName := strings.NewReplacer("(", "[", ")", "]") + + for _, nicAdapterAddress := range nicAdapterAddresses { + friendlyName := windows.UTF16PtrToString(nicAdapterAddress.FriendlyName) + nicName := windows.UTF16PtrToString(nicAdapterAddress.Description) + + if c.config.NicExclude.MatchString(nicName) || + !c.config.NicInclude.MatchString(nicName) { + continue + } + + for address := nicAdapterAddress.FirstUnicastAddress; address != nil; address = address.Next { + ipAddr := address.Address.IP() + + if ipAddr == nil || !ipAddr.IsGlobalUnicast() { + continue + } + + ch <- prometheus.MustNewConstMetric( + c.nicAddressInfo, + prometheus.GaugeValue, + 1, + convertNicName.Replace(nicName), + friendlyName, + ipAddr.String(), + addressFamily[address.Address.Sockaddr.Addr.Family], + ) + } + + for address := nicAdapterAddress.FirstAnycastAddress; address != nil; address = address.Next { + ipAddr := address.Address.IP() + + if ipAddr == nil || !ipAddr.IsGlobalUnicast() { + continue + } + + ch <- prometheus.MustNewConstMetric( + c.nicAddressInfo, + prometheus.GaugeValue, + 1, + convertNicName.Replace(nicName), + friendlyName, + ipAddr.String(), + addressFamily[address.Address.Sockaddr.Addr.Family], + ) + } + } + + return nil +} + +// adapterAddresses returns a list of IP adapter and address +// structures. The structure contains an IP adapter and flattened +// multiple IP addresses including unicast, anycast and multicast +// addresses. +func adapterAddresses() ([]*windows.IpAdapterAddresses, error) { + var b []byte + + l := uint32(15000) // recommended initial size + + for { + b = make([]byte, l) + + const flags = windows.GAA_FLAG_SKIP_MULTICAST | windows.GAA_FLAG_SKIP_DNS_SERVER + + err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) + if err == nil { + if l == 0 { + return nil, nil + } + + break + } + + if !errors.Is(err, windows.ERROR_BUFFER_OVERFLOW) { + return nil, os.NewSyscallError("getadaptersaddresses", err) + } + + if l <= uint32(len(b)) { + return nil, os.NewSyscallError("getadaptersaddresses", err) + } + } + + var addresses []*windows.IpAdapterAddresses + for address := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); address != nil; address = address.Next { + addresses = append(addresses, address) + } + + return addresses, nil +} diff --git a/pkg/collector/net/net_test.go b/pkg/collector/net/net_test.go deleted file mode 100644 index 3dc170907..000000000 --- a/pkg/collector/net/net_test.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build windows - -package net - -import ( - "testing" -) - -func TestNetworkToInstanceName(t *testing.T) { - t.Parallel() - - data := map[string]string{ - "Intel[R] Dual Band Wireless-AC 8260": "Intel_R__Dual_Band_Wireless_AC_8260", - } - for in, out := range data { - got := mangleNetworkName(in) - if got != out { - t.Error("expected", out, "got", got) - } - } -} diff --git a/pkg/collector/tcp/tcp.go b/pkg/collector/tcp/tcp.go index 2892a7dfc..c062de99a 100644 --- a/pkg/collector/tcp/tcp.go +++ b/pkg/collector/tcp/tcp.go @@ -5,6 +5,8 @@ package tcp import ( "fmt" "log/slog" + "slices" + "strings" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/pkg/headers/iphlpapi" @@ -17,9 +19,16 @@ import ( const Name = "tcp" -type Config struct{} +type Config struct { + CollectorsEnabled []string `yaml:"collectors_enabled"` +} -var ConfigDefaults = Config{} +var ConfigDefaults = Config{ + CollectorsEnabled: []string{ + "metrics", + "connections_state", + }, +} // A Collector is a Prometheus Collector for WMI Win32_PerfRawData_Tcpip_TCPv{4,6} metrics. type Collector struct { @@ -45,6 +54,10 @@ func New(config *Config) *Collector { config = &ConfigDefaults } + if config.CollectorsEnabled == nil { + config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled + } + c := &Collector{ config: *config, } @@ -52,8 +65,26 @@ func New(config *Config) *Collector { return c } -func NewWithFlags(_ *kingpin.Application) *Collector { - return &Collector{} +func NewWithFlags(app *kingpin.Application) *Collector { + c := &Collector{ + config: ConfigDefaults, + } + c.config.CollectorsEnabled = make([]string, 0) + + var collectorsEnabled string + + app.Flag( + "collector.tcp.enabled", + "Comma-separated list of collectors to use. Defaults to all, if not specified.", + ).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled) + + app.Action(func(*kingpin.ParseContext) error { + c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",") + + return nil + }) + + return c } func (c *Collector) GetName() string { @@ -160,20 +191,25 @@ func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { // to the provided prometheus Metric channel. func (c *Collector) Collect(_ *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { logger = logger.With(slog.String("collector", Name)) - if err := c.collect(ch); err != nil { - logger.Error("failed collecting tcp metrics", - slog.Any("err", err), - ) - return err + if slices.Contains(c.config.CollectorsEnabled, "metrics") { + if err := c.collect(ch); err != nil { + logger.Error("failed collecting tcp metrics", + slog.Any("err", err), + ) + + return err + } } - if err := c.collectConnectionsState(ch); err != nil { - logger.Error("failed collecting tcp connection state metrics", - slog.Any("err", err), - ) + if slices.Contains(c.config.CollectorsEnabled, "connections_state") { + if err := c.collectConnectionsState(ch); err != nil { + logger.Error("failed collecting tcp connection state metrics", + slog.Any("err", err), + ) - return err + return err + } } return nil diff --git a/tools/e2e-output.txt b/tools/e2e-output.txt index 72a0dc057..fb1da2a44 100644 --- a/tools/e2e-output.txt +++ b/tools/e2e-output.txt @@ -29,6 +29,8 @@ test_alpha_total 42 # TYPE windows_cpu_interrupts_total counter # HELP windows_cpu_logical_processor Total number of logical processors # TYPE windows_cpu_logical_processor gauge +# HELP windows_net_nic_address_info A metric with a constant '1' value labeled with the network interface's address information. +# TYPE windows_net_nic_address_info gauge # HELP windows_cpu_parking_status Parking Status represents whether a processor is parked or not # TYPE windows_cpu_parking_status gauge # HELP windows_cpu_processor_mperf_total Processor MPerf is the number of TSC ticks incremented while executing instructions