container: fix network metrics (#2136)

This commit is contained in:
Jan-Otto Kröpke
2025-07-17 21:07:55 +02:00
committed by GitHub
parent ab7db07836
commit 255b01f610
7 changed files with 93 additions and 295 deletions

View File

@@ -29,10 +29,8 @@ import (
"unsafe"
"github.com/alecthomas/kingpin/v2"
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/hcn"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi"
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
@@ -523,7 +521,7 @@ func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDe
// collectNetworkMetrics collects network metrics for containers.
func (c *Collector) collectNetworkMetrics(ch chan<- prometheus.Metric) error {
endpoints, err := hcn.EnumerateEndpoints()
endpoints, err := hcn.ListEndpoints()
if err != nil {
return fmt.Errorf("error in fetching HCN endpoints: %w", err)
}
@@ -533,57 +531,24 @@ func (c *Collector) collectNetworkMetrics(ch chan<- prometheus.Metric) error {
}
for _, endpoint := range endpoints {
properties, err := hcn.GetEndpointProperties(endpoint)
if len(endpoint.SharedContainers) == 0 {
continue
}
endpointStats, err := hcn.GetHNSEndpointStats(endpoint.ID)
if err != nil {
c.logger.Warn("Failed to collect properties for interface "+endpoint.String(),
c.logger.Warn("Failed to collect network stats for interface "+endpoint.ID,
slog.Any("err", err),
)
continue
}
if len(properties.SharedContainers) == 0 {
continue
}
var nicGUID *ole.GUID
for _, allocator := range properties.Resources.Allocators {
if allocator.AdapterNetCfgInstanceId != nil {
nicGUID = allocator.AdapterNetCfgInstanceId
break
}
}
if nicGUID == nil {
c.logger.Warn("Failed to get nic GUID for endpoint " + endpoint.String())
continue
}
luid, err := iphlpapi.ConvertInterfaceGUIDToLUID(*nicGUID)
if err != nil {
return fmt.Errorf("error in converting interface GUID to LUID: %w", err)
}
var endpointStats iphlpapi.MIB_IF_ROW2
endpointStats.InterfaceLuid = luid
if err := iphlpapi.GetIfEntry2Ex(&endpointStats); err != nil {
c.logger.Warn("Failed to get interface entry for endpoint "+endpoint.String(),
slog.Any("err", err),
)
continue
}
for _, containerId := range properties.SharedContainers {
for _, containerId := range endpoint.SharedContainers {
containerInfo, ok := c.annotationsCacheHCS[containerId]
if !ok {
c.logger.Debug("Unknown container " + containerId + " for endpoint " + endpoint.String())
c.logger.Debug("Unknown container " + containerId + " for endpoint " + endpoint.ID)
continue
}
@@ -593,47 +558,47 @@ func (c *Collector) collectNetworkMetrics(ch chan<- prometheus.Metric) error {
continue
}
endpointId := strings.ToUpper(endpoint.String())
endpointId := strings.ToUpper(endpoint.ID)
ch <- prometheus.MustNewConstMetric(
c.bytesReceived,
prometheus.CounterValue,
float64(endpointStats.InOctets),
float64(endpointStats.BytesReceived),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
ch <- prometheus.MustNewConstMetric(
c.bytesSent,
prometheus.CounterValue,
float64(endpointStats.OutOctets),
float64(endpointStats.BytesSent),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
ch <- prometheus.MustNewConstMetric(
c.packetsReceived,
prometheus.CounterValue,
float64(endpointStats.InUcastPkts+endpointStats.InNUcastPkts),
float64(endpointStats.PacketsReceived),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
ch <- prometheus.MustNewConstMetric(
c.packetsSent,
prometheus.CounterValue,
float64(endpointStats.OutUcastPkts+endpointStats.OutNUcastPkts),
float64(endpointStats.PacketsSent),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
ch <- prometheus.MustNewConstMetric(
c.droppedPacketsIncoming,
prometheus.CounterValue,
float64(endpointStats.InDiscards+endpointStats.InErrors),
float64(endpointStats.DroppedPacketsIncoming),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
ch <- prometheus.MustNewConstMetric(
c.droppedPacketsOutgoing,
prometheus.CounterValue,
float64(endpointStats.OutDiscards+endpointStats.OutErrors),
float64(endpointStats.DroppedPacketsOutgoing),
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container, endpointId,
)
}

View File

@@ -18,30 +18,88 @@
package hcn
import (
"encoding/json"
"fmt"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"github.com/prometheus-community/windows_exporter/internal/utils"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
defaultQuery = utils.Must(windows.UTF16PtrFromString(`{"SchemaVersion":{"Major": 2,"Minor": 0},"Flags":"None"}`))
modvmcompute = windows.NewLazySystemDLL("vmcompute.dll")
procHNSCall = modvmcompute.NewProc("HNSCall")
hcnBodyEmpty = utils.Must(windows.UTF16PtrFromString(""))
hcnMethodGet = utils.Must(windows.UTF16PtrFromString("GET"))
hcnPathEndpoints = utils.Must(windows.UTF16PtrFromString("/endpoints/"))
hcnPathEndpointStats = utils.Must(windows.UTF16FromString("/endpointstats/"))
)
func GetEndpointProperties(endpointID ole.GUID) (EndpointProperties, error) {
endpoint, err := OpenEndpoint(endpointID)
func ListEndpoints() ([]EndpointProperties, error) {
result, err := hnsCall(hcnMethodGet, hcnPathEndpoints, hcnBodyEmpty)
if err != nil {
return EndpointProperties{}, fmt.Errorf("failed to open endpoint: %w", err)
return nil, err
}
defer CloseEndpoint(endpoint)
result, err := QueryEndpointProperties(endpoint, defaultQuery)
if err != nil {
return EndpointProperties{}, fmt.Errorf("failed to query endpoint properties: %w", err)
var endpoints struct {
Success bool `json:"success"`
Error string `json:"error"`
Output []EndpointProperties `json:"output"`
}
return result, nil
if err := json.Unmarshal([]byte(result), &endpoints); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON %s: %w", result, err)
}
if !endpoints.Success {
return nil, fmt.Errorf("HNSCall failed: %s", endpoints.Error)
}
return endpoints.Output, nil
}
func GetHNSEndpointStats(endpointID string) (EndpointStats, error) {
endpointIDUTF16, err := windows.UTF16FromString(endpointID)
if err != nil {
return EndpointStats{}, fmt.Errorf("failed to convert endpoint ID to UTF16: %w", err)
}
path := hcnPathEndpointStats[:len(hcnPathEndpointStats)-1]
path = append(path, endpointIDUTF16...)
result, err := hnsCall(hcnMethodGet, &path[0], hcnBodyEmpty)
if err != nil {
return EndpointStats{}, err
}
var stats EndpointStats
if err := json.Unmarshal([]byte(result), &stats); err != nil {
return EndpointStats{}, fmt.Errorf("failed to unmarshal JSON %s: %w", result, err)
}
return stats, nil
}
func hnsCall(method, path, body *uint16) (string, error) {
var responseJSON *uint16
r1, _, _ := procHNSCall.Call(
uintptr(unsafe.Pointer(method)),
uintptr(unsafe.Pointer(path)),
uintptr(unsafe.Pointer(body)),
uintptr(unsafe.Pointer(&responseJSON)),
)
response := windows.UTF16PtrToString(responseJSON)
windows.CoTaskMemFree(unsafe.Pointer(responseJSON))
if r1 != 0 {
return "", fmt.Errorf("HNSCall failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1))
}
return response, nil
}

View File

@@ -1,47 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package hcn_test
import (
"testing"
"github.com/prometheus-community/windows_exporter/internal/headers/hcn"
"github.com/stretchr/testify/require"
)
func TestEnumerateEndpoints(t *testing.T) {
t.Parallel()
endpoints, err := hcn.EnumerateEndpoints()
require.NoError(t, err)
require.NotNil(t, endpoints)
}
func TestQueryEndpointProperties(t *testing.T) {
t.Parallel()
endpoints, err := hcn.EnumerateEndpoints()
require.NoError(t, err)
if len(endpoints) == 0 {
t.Skip("No endpoints found")
}
_, err = hcn.GetEndpointProperties(endpoints[0])
require.NoError(t, err)
}

View File

@@ -1,134 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package hcn
import (
"encoding/json"
"fmt"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/prometheus-community/windows_exporter/internal/headers/hcs"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
modComputeNetwork = windows.NewLazySystemDLL("computenetwork.dll")
procHcnEnumerateEndpoints = modComputeNetwork.NewProc("HcnEnumerateEndpoints")
procHcnOpenEndpoint = modComputeNetwork.NewProc("HcnOpenEndpoint")
procHcnQueryEndpointProperties = modComputeNetwork.NewProc("HcnQueryEndpointProperties")
procHcnCloseEndpoint = modComputeNetwork.NewProc("HcnCloseEndpoint")
)
// EnumerateEndpoints enumerates the endpoints.
//
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnenumerateendpoints
func EnumerateEndpoints() ([]ole.GUID, error) {
var (
endpointsJSON *uint16
errorRecord *uint16
)
r1, _, _ := procHcnEnumerateEndpoints.Call(
0,
uintptr(unsafe.Pointer(&endpointsJSON)),
uintptr(unsafe.Pointer(&errorRecord)),
)
result := windows.UTF16PtrToString(endpointsJSON)
windows.CoTaskMemFree(unsafe.Pointer(endpointsJSON))
windows.CoTaskMemFree(unsafe.Pointer(errorRecord))
if r1 != 0 {
return nil, fmt.Errorf("HcnEnumerateEndpoints failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1))
}
var endpoints []ole.GUID
if err := json.Unmarshal([]byte(result), &endpoints); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON %s: %w", result, err)
}
return endpoints, nil
}
// OpenEndpoint opens an endpoint.
//
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnopenendpoint
func OpenEndpoint(id ole.GUID) (Endpoint, error) {
var (
endpoint Endpoint
errorRecord *uint16
)
r1, _, _ := procHcnOpenEndpoint.Call(
uintptr(unsafe.Pointer(&id)),
uintptr(unsafe.Pointer(&endpoint)),
uintptr(unsafe.Pointer(&errorRecord)),
)
windows.CoTaskMemFree(unsafe.Pointer(errorRecord))
if r1 != 0 {
return 0, fmt.Errorf("HcnOpenEndpoint failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1))
}
return endpoint, nil
}
// QueryEndpointProperties queries the properties of an endpoint.
//
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcnqueryendpointproperties
func QueryEndpointProperties(endpoint Endpoint, propertyQuery *uint16) (EndpointProperties, error) {
var (
resultDocument *uint16
errorRecord *uint16
)
r1, _, _ := procHcnQueryEndpointProperties.Call(
uintptr(endpoint),
uintptr(unsafe.Pointer(&propertyQuery)),
uintptr(unsafe.Pointer(&resultDocument)),
uintptr(unsafe.Pointer(&errorRecord)),
)
result := windows.UTF16PtrToString(resultDocument)
windows.CoTaskMemFree(unsafe.Pointer(resultDocument))
windows.CoTaskMemFree(unsafe.Pointer(errorRecord))
if r1 != 0 {
return EndpointProperties{}, fmt.Errorf("HcsGetComputeSystemProperties failed: HRESULT 0x%X: %w", r1, hcs.Win32FromHResult(r1))
}
var properties EndpointProperties
if err := json.Unmarshal([]byte(result), &properties); err != nil {
return EndpointProperties{}, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
return properties, nil
}
// CloseEndpoint close a handle to an Endpoint.
//
// https://learn.microsoft.com/en-us/virtualization/api/hcn/reference/hcncloseendpoint
func CloseEndpoint(endpoint Endpoint) {
_, _, _ = procHcnCloseEndpoint.Call(uintptr(endpoint))
}

View File

@@ -18,7 +18,6 @@
package hcn
import (
"github.com/go-ole/go-ole"
"golang.org/x/sys/windows"
)
@@ -28,17 +27,9 @@ type Endpoint = windows.Handle
//
// https://learn.microsoft.com/en-us/virtualization/api/hcn/hns_schema#HostComputeEndpoint
type EndpointProperties struct {
ID string `json:"ID"`
State int `json:"State"`
SharedContainers []string `json:"SharedContainers"`
Resources EndpointPropertiesResources `json:"Resources"`
}
type EndpointPropertiesResources struct {
Allocators []EndpointPropertiesAllocators `json:"Allocators"`
}
type EndpointPropertiesAllocators struct {
AdapterNetCfgInstanceId *ole.GUID `json:"AdapterNetCfgInstanceId"`
ID string `json:"ID"`
State int `json:"State"`
SharedContainers []string `json:"SharedContainers"`
}
type EndpointStats struct {

View File

@@ -22,16 +22,13 @@ import (
"fmt"
"unsafe"
"github.com/go-ole/go-ole"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
procGetIfEntry2Ex = modiphlpapi.NewProc("GetIfEntry2Ex")
procConvertInterfaceGuidToLuid = modiphlpapi.NewProc("ConvertInterfaceGuidToLuid")
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
)
func GetTCPConnectionStates(family uint32) (map[MIB_TCP_STATE]uint32, error) {
@@ -131,38 +128,3 @@ func getExtendedTcpTable[T any](ulAf uint32, tableClass uint32) ([]T, error) {
return unsafe.Slice((*T)(unsafe.Pointer(&buf[4])), binary.LittleEndian.Uint32(buf)), nil
}
// GetIfEntry2Ex function retrieves the specified level of information for the specified interface on the local computer.
//
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex
func GetIfEntry2Ex(row *MIB_IF_ROW2) error {
ret, _, _ := procGetIfEntry2Ex.Call(
uintptr(0),
uintptr(unsafe.Pointer(row)),
)
if ret != 0 {
return fmt.Errorf("GetIfEntry2Ex failed with code %d: %w", ret, windows.Errno(ret))
}
return nil
}
// ConvertInterfaceGUIDToLUID function converts a globally unique identifier (GUID) for a network interface to the
// locally unique identifier (LUID) for the interface.
//
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfaceguidtoluid
func ConvertInterfaceGUIDToLUID(guid ole.GUID) (uint64, error) {
var luid uint64
ret, _, _ := procConvertInterfaceGuidToLuid.Call(
uintptr(unsafe.Pointer(&guid)),
uintptr(unsafe.Pointer(&luid)),
)
if ret != 0 {
return 0, fmt.Errorf("ConvertInterfaceGUIDToLUID failed with code %d: %w", ret, windows.Errno(ret))
}
return luid, nil
}