mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-03-09 12:06:35 +00:00
chore: Move private packages to internal (#1664)
This commit is contained in:
416
internal/collector/service/service.go
Normal file
416
internal/collector/service/service.go
Normal file
@@ -0,0 +1,416 @@
|
||||
//go:build windows
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
types2 "github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/yusufpapurcu/wmi"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const Name = "service"
|
||||
|
||||
type Config struct {
|
||||
ServiceInclude *regexp.Regexp `yaml:"service_include"`
|
||||
ServiceExclude *regexp.Regexp `yaml:"service_exclude"`
|
||||
}
|
||||
|
||||
var ConfigDefaults = Config{
|
||||
ServiceInclude: types2.RegExpAny,
|
||||
ServiceExclude: types2.RegExpEmpty,
|
||||
}
|
||||
|
||||
// A Collector is a Prometheus Collector for service metrics.
|
||||
type Collector struct {
|
||||
config Config
|
||||
|
||||
state *prometheus.Desc
|
||||
processID *prometheus.Desc
|
||||
info *prometheus.Desc
|
||||
startMode *prometheus.Desc
|
||||
|
||||
serviceManagerHandle *mgr.Mgr
|
||||
}
|
||||
|
||||
func New(config *Config) *Collector {
|
||||
if config == nil {
|
||||
config = &ConfigDefaults
|
||||
}
|
||||
|
||||
if config.ServiceExclude == nil {
|
||||
config.ServiceExclude = ConfigDefaults.ServiceExclude
|
||||
}
|
||||
|
||||
if config.ServiceInclude == nil {
|
||||
config.ServiceInclude = ConfigDefaults.ServiceInclude
|
||||
}
|
||||
|
||||
c := &Collector{
|
||||
config: *config,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func NewWithFlags(app *kingpin.Application) *Collector {
|
||||
c := &Collector{
|
||||
config: ConfigDefaults,
|
||||
}
|
||||
|
||||
var serviceExclude, serviceInclude string
|
||||
|
||||
app.Flag(
|
||||
"collector.service.exclude",
|
||||
"Regexp of service to exclude. Service name (not the display name!) must both match include and not match exclude to be included.",
|
||||
).Default(c.config.ServiceExclude.String()).StringVar(&serviceExclude)
|
||||
|
||||
app.Flag(
|
||||
"collector.service.include",
|
||||
"Regexp of service to include. Process name (not the display name!) must both match include and not match exclude to be included.",
|
||||
).Default(c.config.ServiceInclude.String()).StringVar(&serviceInclude)
|
||||
|
||||
app.Action(func(*kingpin.ParseContext) error {
|
||||
var err error
|
||||
|
||||
c.config.ServiceExclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", serviceExclude))
|
||||
if err != nil {
|
||||
return fmt.Errorf("collector.process.exclude: %w", err)
|
||||
}
|
||||
|
||||
c.config.ServiceInclude, err = regexp.Compile(fmt.Sprintf("^(?:%s)$", serviceInclude))
|
||||
if err != nil {
|
||||
return fmt.Errorf("collector.process.include: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Collector) GetName() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (c *Collector) GetPerfCounter(_ *slog.Logger) ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (c *Collector) Build(logger *slog.Logger, _ *wmi.Client) error {
|
||||
logger = logger.With(slog.String("collector", Name))
|
||||
|
||||
if c.config.ServiceInclude.String() == "^(?:.*)$" && c.config.ServiceExclude.String() == "^(?:)$" {
|
||||
logger.Warn("No filters specified for service collector. This will generate a very large number of metrics!")
|
||||
}
|
||||
|
||||
c.info = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types2.Namespace, Name, "info"),
|
||||
"A metric with a constant '1' value labeled with service information",
|
||||
[]string{"name", "display_name", "run_as", "path_name"},
|
||||
nil,
|
||||
)
|
||||
c.state = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types2.Namespace, Name, "state"),
|
||||
"The state of the service (State)",
|
||||
[]string{"name", "state"},
|
||||
nil,
|
||||
)
|
||||
c.startMode = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types2.Namespace, Name, "start_mode"),
|
||||
"The start mode of the service (StartMode)",
|
||||
[]string{"name", "start_mode"},
|
||||
nil,
|
||||
)
|
||||
c.processID = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types2.Namespace, Name, "process"),
|
||||
"Process of started service. The value is the creation time of the process as a unix timestamp.",
|
||||
[]string{"name", "process_id"},
|
||||
nil,
|
||||
)
|
||||
|
||||
// EnumServiceStatusEx requires only SC_MANAGER_ENUM_SERVICE.
|
||||
handle, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_ENUMERATE_SERVICE)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open scm: %w", err)
|
||||
}
|
||||
|
||||
c.serviceManagerHandle = &mgr.Mgr{Handle: handle}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) Close(logger *slog.Logger) error {
|
||||
if err := c.serviceManagerHandle.Disconnect(); err != nil {
|
||||
logger.Warn("Failed to disconnect from scm",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect sends the metric values for each metric
|
||||
// to the provided prometheus Metric channel.
|
||||
func (c *Collector) Collect(_ *types2.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error {
|
||||
logger = logger.With(slog.String("collector", Name))
|
||||
|
||||
if err := c.collect(logger, ch); err != nil {
|
||||
logger.Error("failed collecting API service metrics:",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
return fmt.Errorf("failed collecting API service metrics: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) collect(logger *slog.Logger, ch chan<- prometheus.Metric) error {
|
||||
services, err := c.queryAllServices()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to query services",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if services == nil {
|
||||
logger.Warn("No services queried")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Iterate through the Services List.
|
||||
for _, service := range services {
|
||||
serviceName := windows.UTF16PtrToString(service.ServiceName)
|
||||
if c.config.ServiceExclude.MatchString(serviceName) ||
|
||||
!c.config.ServiceInclude.MatchString(serviceName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.collectService(ch, logger, service); err != nil {
|
||||
logger.Warn("failed collecting service info",
|
||||
slog.Any("err", err),
|
||||
slog.String("service", windows.UTF16PtrToString(service.ServiceName)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var apiStateValues = map[uint32]string{
|
||||
windows.SERVICE_CONTINUE_PENDING: "continue pending",
|
||||
windows.SERVICE_PAUSE_PENDING: "pause pending",
|
||||
windows.SERVICE_PAUSED: "paused",
|
||||
windows.SERVICE_RUNNING: "running",
|
||||
windows.SERVICE_START_PENDING: "start pending",
|
||||
windows.SERVICE_STOP_PENDING: "stop pending",
|
||||
windows.SERVICE_STOPPED: "stopped",
|
||||
}
|
||||
|
||||
var apiStartModeValues = map[uint32]string{
|
||||
windows.SERVICE_AUTO_START: "auto",
|
||||
windows.SERVICE_BOOT_START: "boot",
|
||||
windows.SERVICE_DEMAND_START: "manual",
|
||||
windows.SERVICE_DISABLED: "disabled",
|
||||
windows.SERVICE_SYSTEM_START: "system",
|
||||
}
|
||||
|
||||
func (c *Collector) collectService(ch chan<- prometheus.Metric, logger *slog.Logger, service windows.ENUM_SERVICE_STATUS_PROCESS) error {
|
||||
// Open connection for service handler.
|
||||
serviceHandle, err := windows.OpenService(c.serviceManagerHandle.Handle, service.ServiceName, windows.SERVICE_QUERY_CONFIG)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open service: %w", err)
|
||||
}
|
||||
|
||||
serviceNameString := windows.UTF16PtrToString(service.ServiceName)
|
||||
|
||||
// Create handle for each service.
|
||||
serviceManager := &mgr.Service{Name: serviceNameString, Handle: serviceHandle}
|
||||
defer func(serviceManager *mgr.Service) {
|
||||
if err := serviceManager.Close(); err != nil {
|
||||
logger.Warn("failed to close service handle",
|
||||
slog.Any("err", err),
|
||||
slog.String("service", serviceNameString),
|
||||
)
|
||||
}
|
||||
}(serviceManager)
|
||||
|
||||
// Get Service Configuration.
|
||||
serviceConfig, err := serviceManager.Config()
|
||||
if err != nil {
|
||||
if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && !errors.Is(err, windows.ERROR_MUI_FILE_NOT_FOUND) {
|
||||
return fmt.Errorf("failed to get service configuration: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("failed collecting service",
|
||||
slog.Any("err", err),
|
||||
slog.String("service", serviceNameString),
|
||||
)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.info,
|
||||
prometheus.GaugeValue,
|
||||
1.0,
|
||||
serviceNameString,
|
||||
serviceConfig.DisplayName,
|
||||
serviceConfig.ServiceStartName,
|
||||
serviceConfig.BinaryPathName,
|
||||
)
|
||||
|
||||
var (
|
||||
isCurrentStartMode float64
|
||||
isCurrentState float64
|
||||
)
|
||||
|
||||
for _, startMode := range apiStartModeValues {
|
||||
isCurrentStartMode = 0.0
|
||||
if startMode == apiStartModeValues[serviceConfig.StartType] {
|
||||
isCurrentStartMode = 1.0
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.startMode,
|
||||
prometheus.GaugeValue,
|
||||
isCurrentStartMode,
|
||||
serviceNameString,
|
||||
startMode,
|
||||
)
|
||||
}
|
||||
|
||||
for state, stateValue := range apiStateValues {
|
||||
isCurrentState = 0.0
|
||||
if state == service.ServiceStatusProcess.CurrentState {
|
||||
isCurrentState = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.state,
|
||||
prometheus.GaugeValue,
|
||||
isCurrentState,
|
||||
serviceNameString,
|
||||
stateValue,
|
||||
)
|
||||
}
|
||||
|
||||
processID := strconv.FormatUint(uint64(service.ServiceStatusProcess.ProcessId), 10)
|
||||
|
||||
if processID != "0" { //nolint: nestif
|
||||
processStartTime, err := getProcessStartTime(logger, service.ServiceStatusProcess.ProcessId)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
logger.Debug("failed to get process start time",
|
||||
slog.String("service", serviceNameString),
|
||||
slog.Any("err", err),
|
||||
)
|
||||
} else {
|
||||
logger.Warn("failed to get process start time",
|
||||
slog.String("service", serviceNameString),
|
||||
slog.Any("err", err),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.processID,
|
||||
prometheus.GaugeValue,
|
||||
float64(processStartTime/1_000_000_000),
|
||||
serviceNameString,
|
||||
processID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// queryAllServices returns all service states of the current Windows system
|
||||
// This is realized by ask Service Manager directly.
|
||||
func (c *Collector) queryAllServices() ([]windows.ENUM_SERVICE_STATUS_PROCESS, error) {
|
||||
var (
|
||||
bytesNeeded uint32
|
||||
servicesReturned uint32
|
||||
resumeHandle uint32
|
||||
)
|
||||
|
||||
if err := windows.EnumServicesStatusEx(
|
||||
c.serviceManagerHandle.Handle,
|
||||
windows.SC_STATUS_PROCESS_INFO,
|
||||
windows.SERVICE_WIN32,
|
||||
windows.SERVICE_STATE_ALL,
|
||||
nil,
|
||||
0,
|
||||
&bytesNeeded,
|
||||
&servicesReturned,
|
||||
&resumeHandle,
|
||||
nil,
|
||||
); !errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||
return nil, fmt.Errorf("could not fetch buffer size for EnumServicesStatusEx: %w", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, bytesNeeded)
|
||||
if err := windows.EnumServicesStatusEx(
|
||||
c.serviceManagerHandle.Handle,
|
||||
windows.SC_STATUS_PROCESS_INFO,
|
||||
windows.SERVICE_WIN32,
|
||||
windows.SERVICE_STATE_ALL,
|
||||
&buf[0],
|
||||
bytesNeeded,
|
||||
&bytesNeeded,
|
||||
&servicesReturned,
|
||||
&resumeHandle,
|
||||
nil,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("could not query windows service list: %w", err)
|
||||
}
|
||||
|
||||
if servicesReturned == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
services := unsafe.Slice((*windows.ENUM_SERVICE_STATUS_PROCESS)(unsafe.Pointer(&buf[0])), int(servicesReturned))
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func getProcessStartTime(logger *slog.Logger, pid uint32) (uint64, error) {
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open process %w", err)
|
||||
}
|
||||
|
||||
defer func(handle windows.Handle) {
|
||||
err := windows.CloseHandle(handle)
|
||||
if err != nil {
|
||||
logger.Warn("failed to close process handle",
|
||||
slog.Any("err", err),
|
||||
)
|
||||
}
|
||||
}(handle)
|
||||
|
||||
var creation windows.Filetime
|
||||
|
||||
var exit windows.Filetime
|
||||
|
||||
var krn windows.Filetime
|
||||
|
||||
var user windows.Filetime
|
||||
|
||||
err = windows.GetProcessTimes(handle, &creation, &exit, &krn, &user)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get process times %w", err)
|
||||
}
|
||||
|
||||
return uint64(creation.Nanoseconds()), nil
|
||||
}
|
||||
12
internal/collector/service/service_test.go
Normal file
12
internal/collector/service/service_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/service"
|
||||
"github.com/prometheus-community/windows_exporter/internal/testutils"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
testutils.FuncBenchmarkCollector(b, service.Name, service.NewWithFlags)
|
||||
}
|
||||
Reference in New Issue
Block a user