Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mssql: expose server version info #1741

Merged
merged 7 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
481 changes: 243 additions & 238 deletions docs/collector.mssql.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/prometheus-community/windows_exporter
go 1.23

require (
github.com/Microsoft/go-winio v0.6.2
github.com/Microsoft/hcsshim v0.12.9
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/bmatcuk/doublestar/v4 v4.7.1
Expand All @@ -19,7 +20,6 @@ require (
)

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down
104 changes: 103 additions & 1 deletion internal/collector/mssql/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import (
"fmt"
"log/slog"
"sort"
"strconv"
"strings"
"sync"
"time"
"unsafe"

"github.com/Microsoft/go-winio/pkg/process"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/perfdata"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)

Expand All @@ -38,6 +43,7 @@ const (

type Config struct {
CollectorsEnabled []string `yaml:"collectors_enabled"`
Port uint16 `yaml:"port"`
}

var ConfigDefaults = Config{
Expand All @@ -55,6 +61,7 @@ var ConfigDefaults = Config{
subCollectorTransactions,
subCollectorWaitStats,
},
Port: 1433,
}

// A Collector is a Prometheus Collector for various WMI Win32_PerfRawData_MSSQLSERVER_* metrics.
Expand All @@ -67,9 +74,13 @@ type Collector struct {
collectorFns []func(ch chan<- prometheus.Metric) error
closeFns []func()

fileVersion string
productVersion string

// meta
mssqlScrapeDurationDesc *prometheus.Desc
mssqlScrapeSuccessDesc *prometheus.Desc
mssqlInfoDesc *prometheus.Desc

collectorAccessMethods
collectorAvailabilityReplica
Expand All @@ -96,6 +107,10 @@ func New(config *Config) *Collector {
config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled
}

if config.Port == 0 {
config.Port = ConfigDefaults.Port
}

c := &Collector{
config: *config,
}
Expand All @@ -115,6 +130,11 @@ func NewWithFlags(app *kingpin.Application) *Collector {
"Comma-separated list of collectors to use.",
).Default(strings.Join(c.config.CollectorsEnabled, ",")).StringVar(&collectorsEnabled)

app.Flag(
"collector.mssql.port",
"Port of MSSQL server used for windows_mssql_info metric.",
).Default(strconv.FormatUint(uint64(c.config.Port), 10)).Uint16Var(&c.config.Port)

app.Action(func(*kingpin.ParseContext) error {
c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",")

Expand All @@ -140,6 +160,17 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
c.mssqlInstances = c.getMSSQLInstances()

fileVersion, productVersion, err := c.getMSSQLServerVersion(c.config.Port)
if err != nil {
logger.Warn("Failed to get MSSQL server version",
slog.Any("err", err),
slog.String("collector", Name),
)
}

c.fileVersion = fileVersion
c.productVersion = productVersion

subCollectors := map[string]struct {
build func() error
collect func(ch chan<- prometheus.Metric) error
Expand Down Expand Up @@ -209,7 +240,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {

c.collectorFns = make([]func(ch chan<- prometheus.Metric) error, 0, len(c.config.CollectorsEnabled))
c.closeFns = make([]func(), 0, len(c.config.CollectorsEnabled))

// Result must order, to prevent test failures.
sort.Strings(c.config.CollectorsEnabled)

Expand All @@ -227,6 +257,13 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}

// meta
c.mssqlInfoDesc = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "info"),
"mssql server information",
[]string{"file_version", "version"},
nil,
)

c.mssqlScrapeDurationDesc = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "collector_duration_seconds"),
"windows_exporter: Duration of an mssql child collection.",
Expand Down Expand Up @@ -379,3 +416,68 @@ func (c *Collector) collect(

return errors.Join(errs...)
}

// getMSSQLServerVersion get the version of the SQL Server instance by
// reading the version information from the process running the SQL Server instance port.
func (c *Collector) getMSSQLServerVersion(port uint16) (string, string, error) {
pid, err := iphlpapi.GetOwnerPIDOfTCPPort(windows.AF_INET, port)
if err != nil {
return "", "", fmt.Errorf("failed to get the PID of the process running on port 1433: %w", err)
}

hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", "", fmt.Errorf("failed to open the process with PID %d: %w", pid, err)
}

defer windows.CloseHandle(hProcess) //nolint:errcheck

processFilePath, err := process.QueryFullProcessImageName(hProcess, process.ImageNameFormatWin32Path)
if err != nil {
return "", "", fmt.Errorf("failed to query the full path of the process with PID %d: %w", pid, err)
}

// Load the file version information
size, err := windows.GetFileVersionInfoSize(processFilePath, nil)
if err != nil {
return "", "", fmt.Errorf("failed to get the size of the file version information: %w", err)
}

fileVersionInfo := make([]byte, size)

err = windows.GetFileVersionInfo(processFilePath, 0, size, unsafe.Pointer(&fileVersionInfo[0]))
if err != nil {
return "", "", fmt.Errorf("failed to get the file version information: %w", err)
}

var (
verData *byte
verSize uint32
)

err = windows.VerQueryValue(
unsafe.Pointer(&fileVersionInfo[0]),
`\StringFileInfo\040904b0\ProductVersion`,
unsafe.Pointer(&verData),
&verSize,
)
if err != nil {
return "", "", fmt.Errorf("failed to query the product version: %w", err)
}

productVersion := windows.UTF16ToString((*[1 << 16]uint16)(unsafe.Pointer(verData))[:verSize])

err = windows.VerQueryValue(
unsafe.Pointer(&fileVersionInfo[0]),
`\StringFileInfo\040904b0\FileVersion`,
unsafe.Pointer(&verData),
&verSize,
)
if err != nil {
return "", "", fmt.Errorf("failed to query the file version: %w", err)
}

fileVersion := windows.UTF16ToString((*[1 << 16]uint16)(unsafe.Pointer(verData))[:verSize])

return fileVersion, productVersion, nil
}
4 changes: 2 additions & 2 deletions internal/headers/iphlpapi/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
package iphlpapi

const (
TCPTableClass uint32 = 5
TCP6TableClass uint32 = 5
TCPTableOwnerPIDAll uint32 = 5
TCPTableOwnerPIDListener uint32 = 3
)
113 changes: 74 additions & 39 deletions internal/headers/iphlpapi/iphlpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package iphlpapi

import (
"encoding/binary"
"fmt"
"unsafe"

Expand All @@ -15,65 +16,99 @@ var (
)

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
}
switch family {
case windows.AF_INET:
table, err := getExtendedTcpTable[MIB_TCPROW_OWNER_PID](family, TCPTableOwnerPIDAll)
if err != nil {
return nil, err
}

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)
}
for _, row := range table {
stateCounts[row.dwState]++
}

buf := make([]byte, size)
return stateCounts, nil
case windows.AF_INET6:
table, err := getExtendedTcpTable[MIB_TCP6ROW_OWNER_PID](family, TCPTableOwnerPIDAll)
if err != nil {
return nil, err
}

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)
for _, row := range table {
stateCounts[row.dwState]++
}

return stateCounts, nil
default:
return nil, fmt.Errorf("unsupported address family %d", family)
}
}

func GetOwnerPIDOfTCPPort(family uint32, tcpPort uint16) (uint32, error) {
switch family {
case windows.AF_INET:
table, err := getExtendedTcpTable[MIB_TCPROW_OWNER_PID](family, TCPTableOwnerPIDListener)
if err != nil {
return 0, err
}

numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
for _, row := range table {
if row.dwLocalPort.uint16() == tcpPort {
return row.dwOwningPid, nil
}
}

for i := range numEntries {
var state MIB_TCP_STATE
return 0, fmt.Errorf("no process found for port %d", tcpPort)
case windows.AF_INET6:
table, err := getExtendedTcpTable[MIB_TCP6ROW_OWNER_PID](family, TCPTableOwnerPIDListener)
if err != nil {
return 0, err
}

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
for _, row := range table {
if row.dwLocalPort.uint16() == tcpPort {
return row.dwOwningPid, nil
}
}

stateCounts[state]++
return 0, fmt.Errorf("no process found for port %d", tcpPort)
default:
return 0, fmt.Errorf("unsupported address family %d", family)
}

return stateCounts, nil
}

func getExtendedTcpTable(pTCPTable uintptr, pdwSize *uint32, bOrder bool, ulAf uint32, tableClass uint32, reserved uint32) uintptr {
func getExtendedTcpTable[T any](ulAf uint32, tableClass uint32) ([]T, error) {
var size uint32

ret, _, _ := procGetExtendedTcpTable.Call(
pTCPTable,
uintptr(unsafe.Pointer(pdwSize)),
uintptr(boolToInt(bOrder)),
uintptr(0),
uintptr(unsafe.Pointer(&size)),
uintptr(0),
uintptr(ulAf),
uintptr(tableClass),
uintptr(reserved),
uintptr(0),
)

return ret
}
if ret != uintptr(windows.ERROR_INSUFFICIENT_BUFFER) {
return nil, fmt.Errorf("getExtendedTcpTable (size query) failed with code %d", ret)
}

func boolToInt(b bool) int {
if b {
return 1
buf := make([]byte, size)

ret, _, _ = procGetExtendedTcpTable.Call(
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
uintptr(0),
uintptr(ulAf),
uintptr(tableClass),
uintptr(0),
)

if ret != 0 {
return nil, fmt.Errorf("getExtendedTcpTable (data query) failed with code %d", ret)
}

return 0
return unsafe.Slice((*T)(unsafe.Pointer(&buf[4])), binary.LittleEndian.Uint32(buf)), nil
}
34 changes: 34 additions & 0 deletions internal/headers/iphlpapi/iphlpapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package iphlpapi_test

import (
"net"
"os"
"testing"

"github.com/prometheus-community/windows_exporter/internal/headers/iphlpapi"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)

func TestGetTCPConnectionStates(t *testing.T) {
t.Parallel()

pid, err := iphlpapi.GetTCPConnectionStates(windows.AF_INET)
require.NoError(t, err)
require.NotEmpty(t, pid)
}

func TestGetOwnerPIDOfTCPPort(t *testing.T) {
t.Parallel()

lister, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)

t.Cleanup(func() {
require.NoError(t, lister.Close())
})

pid, err := iphlpapi.GetOwnerPIDOfTCPPort(windows.AF_INET, uint16(lister.Addr().(*net.TCPAddr).Port))
require.NoError(t, err)
require.EqualValues(t, os.Getpid(), pid)
}
Loading