mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-26 21:49:03 +01:00
service: add delayed_start and trigger_start labels to windows_service_info (#2768)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
+55
-11
@@ -30,22 +30,24 @@ Recommended to keep down number of returned metrics. Default: `auto,boot,manual,
|
|||||||
|
|
||||||
## Configuration cache
|
## Configuration cache
|
||||||
|
|
||||||
The service configuration (display name, start mode, path and account, used by
|
The service configuration (display name, start mode, delayed start, triggers, path and
|
||||||
`windows_service_info` and `windows_service_start_mode`) is cached between scrapes.
|
account, used by `windows_service_info` and `windows_service_start_mode`) is cached between scrapes.
|
||||||
The collector watches the registry key `HKLM\SYSTEM\CurrentControlSet\Services`. When a
|
The collector watches the registry key `HKLM\SYSTEM\CurrentControlSet\Services`. When a
|
||||||
service's own key changes, for example after `sc config`, a change in the Services console
|
service's own key changes, for example after `sc config`, a change in the Services console
|
||||||
or a reinstall, its configuration is queried again on the next scrape. All configurations
|
or a reinstall, its configuration is queried again on the next scrape. All configurations
|
||||||
are queried again at the latest every 10 minutes. The service state and process are queried
|
are queried again at the latest every 10 minutes. The service state and process are queried
|
||||||
on every scrape.
|
on every scrape. Reading the delayed start flag and the triggers takes extra calls per service,
|
||||||
|
which run when a configuration is queried: on a cache miss, or on every scrape if the registry
|
||||||
|
key can't be watched and the cache is disabled.
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
| Name | Description | Type | Labels |
|
| Name | Description | Type | Labels |
|
||||||
|------------------------------|-----------------------------------------------------------------------------------------------|-------|---------------------------------------|
|
|------------------------------|-----------------------------------------------------------------------------------------------|-------|--------------------------------------------------------------------------|
|
||||||
| `windows_service_info` | Contains service information run as user in labels, constant 1 | gauge | service, display_name, run_as, path_name |
|
| `windows_service_info` | Contains service information run as user in labels, constant 1 | gauge | service, display_name, run_as, path_name, delayed_start, trigger_start |
|
||||||
| `windows_service_start_mode` | The start mode of the service, 1 if the current start mode, 0 otherwise | gauge | service, start_mode |
|
| `windows_service_start_mode` | The start mode of the service, 1 if the current start mode, 0 otherwise | gauge | service, start_mode |
|
||||||
| `windows_service_state` | The state of the service, 1 if the current state, 0 otherwise | gauge | service, state |
|
| `windows_service_state` | The state of the service, 1 if the current state, 0 otherwise | gauge | service, state |
|
||||||
| `windows_service_process` | Process of started service. The value is the creation time of the process as a Unix timestamp | gauge | service, process_id |
|
| `windows_service_process` | Process of started service. The value is the creation time of the process as a Unix timestamp | gauge | service, process_id |
|
||||||
|
|
||||||
### States
|
### States
|
||||||
|
|
||||||
@@ -69,6 +71,22 @@ A service can have the following start modes:
|
|||||||
|
|
||||||
Note that there is some overlap with service state.
|
Note that there is some overlap with service state.
|
||||||
|
|
||||||
|
### Delayed and trigger start
|
||||||
|
|
||||||
|
The Services console shows "Automatic (Delayed Start)" and "(Trigger Start)" in addition to
|
||||||
|
the start mode. `windows_service_start_mode` reports these services with their start mode
|
||||||
|
(`auto` or `manual`); the labels of `windows_service_info` tell them apart:
|
||||||
|
|
||||||
|
- `delayed_start` is `true` for an `auto` service that starts after the other automatic
|
||||||
|
services, `false` otherwise. Windows ignores the delayed start setting of services that
|
||||||
|
don't start automatically, so it is always `false` for them.
|
||||||
|
- `trigger_start` is `true` if the service has triggers, which start or stop it when an event
|
||||||
|
occurs, for example when a device arrives, the computer joins a domain or a network becomes
|
||||||
|
available. Such a service can stop by design while its start mode is `auto`.
|
||||||
|
|
||||||
|
Both labels are empty if the service configuration, or the delayed start flag and triggers,
|
||||||
|
can't be read. The other labels are still published in the second case.
|
||||||
|
|
||||||
### Run As
|
### Run As
|
||||||
|
|
||||||
Account name under which a service runs. Depending on the service type, the account name may be in the form of "DomainName\Username" or UPN format ("Username@DomainName").
|
Account name under which a service runs. Depending on the service type, the account name may be in the form of "DomainName\Username" or UPN format ("Username@DomainName").
|
||||||
@@ -78,8 +96,8 @@ Account name under which a service runs. Depending on the service type, the acco
|
|||||||
```
|
```
|
||||||
# HELP windows_service_info A metric with a constant '1' value labeled with service information
|
# HELP windows_service_info A metric with a constant '1' value labeled with service information
|
||||||
# TYPE windows_service_info gauge
|
# TYPE windows_service_info gauge
|
||||||
windows_service_info{display_name="Declared Configuration(DC) service",service="dcsvc",path_name="C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p",run_as="LocalSystem"} 1
|
windows_service_info{delayed_start="false",display_name="Declared Configuration(DC) service",service="dcsvc",path_name="C:\\WINDOWS\\system32\\svchost.exe -k netsvcs -p",run_as="LocalSystem",trigger_start="true"} 1
|
||||||
windows_service_info{display_name="Designs",service="Themes",path_name="C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p",run_as="LocalSystem"} 1
|
windows_service_info{delayed_start="false",display_name="Designs",service="Themes",path_name="C:\\WINDOWS\\System32\\svchost.exe -k netsvcs -p",run_as="LocalSystem",trigger_start="false"} 1
|
||||||
# HELP windows_service_process Process of started service. The value is the creation time of the process as a unix timestamp.
|
# HELP windows_service_process Process of started service. The value is the creation time of the process as a unix timestamp.
|
||||||
# TYPE windows_service_process gauge
|
# TYPE windows_service_process gauge
|
||||||
windows_service_process{service="Themes",process_id="2856"} 1.7244891e+09
|
windows_service_process{service="Themes",process_id="2856"} 1.7244891e+09
|
||||||
@@ -148,3 +166,29 @@ groups:
|
|||||||
description: "Service {{ $labels.service }} on instance {{ $labels.instance }} has been down for more than 3 minutes."
|
description: "Service {{ $labels.service }} on instance {{ $labels.instance }} has been down for more than 3 minutes."
|
||||||
```
|
```
|
||||||
In this example, `instance` is the target label of the host. So each alert will be processed per host, which is then used in the alert description.
|
In this example, `instance` is the target label of the host. So each alert will be processed per host, which is then used in the alert description.
|
||||||
|
|
||||||
|
The next rule alerts on every automatic service that is not running, except trigger-start
|
||||||
|
services, which stop by design:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
groups:
|
||||||
|
- name: Windows services
|
||||||
|
rules:
|
||||||
|
|
||||||
|
# Sends an alert when a service with start mode auto and without triggers is not running for 15 minutes.
|
||||||
|
- alert: WindowsAutoServiceNotRunning
|
||||||
|
expr: |
|
||||||
|
windows_service_state{state="running"} == 0
|
||||||
|
and on (instance, service) windows_service_start_mode{start_mode="auto"} == 1
|
||||||
|
unless on (instance, service) windows_service_info{trigger_start="true"}
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Service {{ $labels.service }} not running"
|
||||||
|
description: "Automatic service {{ $labels.service }} on instance {{ $labels.instance }} has not been running for more than 15 minutes."
|
||||||
|
```
|
||||||
|
|
||||||
|
Delayed-start services start about two minutes after the other automatic services, so the
|
||||||
|
`for` duration covers them after a reboot. To leave them out as well, add
|
||||||
|
`unless on (instance, service) windows_service_info{delayed_start="true"}`.
|
||||||
|
|||||||
@@ -900,6 +900,7 @@ windows_service_state{state="running"} == 0
|
|||||||
### Label changes
|
### Label changes
|
||||||
|
|
||||||
- `windows_os_info` has an `installation_type` label, for example `Server` or `Client`.
|
- `windows_os_info` has an `installation_type` label, for example `Server` or `Client`.
|
||||||
|
- `windows_service_info` has the labels `delayed_start` and `trigger_start` (`true` or `false`), which tell "Automatic (Delayed Start)" and trigger-start services apart. `windows_service_start_mode` is unchanged.
|
||||||
- The IIS HTTP request queue metrics (`windows_iis_http_requests_*`) have the label `app` instead of `site`. The value is unchanged: it always was the application pool that owns the HTTP.sys request queue, not a site. They are now filtered with `--collector.iis.app-include` and `--collector.iis.app-exclude` instead of the site filters.
|
- The IIS HTTP request queue metrics (`windows_iis_http_requests_*`) have the label `app` instead of `site`. The value is unchanged: it always was the application pool that owns the HTTP.sys request queue, not a site. They are now filtered with `--collector.iis.app-include` and `--collector.iis.app-exclude` instead of the site filters.
|
||||||
- All `windows_netframework_*` metrics have a `process_id` label, so processes with the same name, for example IIS worker processes (`w3wp`), can be joined with the `process` collector metrics. Only `.NET CLR Memory` reports the process ID; the other metrics take it from the `.NET CLR Memory` instance of the same sample and have an empty `process_id` if there is none. The new `--collector.netframework.process-include` and `--collector.netframework.process-exclude` flags filter the processes. The label changes the series identity: a restarted process, for example a recycled `w3wp`, starts a new series instead of continuing `process="w3wp"`. To keep the 0.31 series, aggregate with `without (process_id)` or `by (process)`.
|
- All `windows_netframework_*` metrics have a `process_id` label, so processes with the same name, for example IIS worker processes (`w3wp`), can be joined with the `process` collector metrics. Only `.NET CLR Memory` reports the process ID; the other metrics take it from the `.NET CLR Memory` instance of the same sample and have an empty `process_id` if there is none. The new `--collector.netframework.process-include` and `--collector.netframework.process-exclude` flags filter the processes. The label changes the series identity: a restarted process, for example a recycled `w3wp`, starts a new series instead of continuing `process="w3wp"`. To keep the 0.31 series, aggregate with `without (process_id)` or `by (process)`.
|
||||||
- In the `windows_hyperv_dynamic_memory_vm_*` metrics, unnamed VM instances have `vm="(unknown)"` (or `(unknown)#N`) instead of `vm="------"`.
|
- In the `windows_hyperv_dynamic_memory_vm_*` metrics, unnamed VM instances have `vm="(unknown)"` (or `(unknown)#N`) instead of `vm="------"`.
|
||||||
|
|||||||
@@ -9,6 +9,19 @@
|
|||||||
`RegNotifyChangeKeyValue` instead. Arm it with `REG_NOTIFY_THREAD_AGNOSTIC`:
|
`RegNotifyChangeKeyValue` instead. Arm it with `REG_NOTIFY_THREAD_AGNOSTIC`:
|
||||||
without it the registration ends when the arming OS thread exits, and Go
|
without it the registration ends when the arming OS thread exits, and Go
|
||||||
goroutines are not bound to one.
|
goroutines are not bound to one.
|
||||||
|
- The delayed auto-start flag and the triggers are queried with
|
||||||
|
`QueryServiceConfig2` on the same handle and cached with the configuration.
|
||||||
|
The SCM ignores the delayed flag unless the start type is auto, so it is only
|
||||||
|
queried for auto-start services. `x/sys/windows` has no
|
||||||
|
`SERVICE_TRIGGER_INFO`; the mirror in `service.go` only reads `cTriggers`.
|
||||||
|
- The delayed flag is the `DelayedAutostart` value of the service key, so a
|
||||||
|
change updates the key's last write time. Triggers are stored in the
|
||||||
|
`TriggerInfo` subkey. Adding the first trigger creates it, which updates the
|
||||||
|
last write time of the service key (`TestCollectorPicksUpConfigChanges`
|
||||||
|
checks this). That a later trigger change made with `sc triggerinfo` also
|
||||||
|
does is assumed, not verified: the SCM is believed to rewrite the whole
|
||||||
|
`TriggerInfo` subkey. If it only changes deeper keys, the cache picks the
|
||||||
|
change up after at most `configCacheMaxAge`.
|
||||||
- The subtree notification fires every few seconds on a normal machine (W32Time,
|
- The subtree notification fires every few seconds on a normal machine (W32Time,
|
||||||
Tcpip, VSS, driver `Enum` keys, Hyper-V). Treat it as a hint only and compare
|
Tcpip, VSS, driver `Enum` keys, Hyper-V). Treat it as a hint only and compare
|
||||||
the last write time of each service's own key, which changes only for its own
|
the last write time of each service's own key, which changes only for its own
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
"golang.org/x/sys/windows/svc/mgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -54,7 +53,7 @@ var errConfigCacheClosed = errors.New("service configuration cache is closed")
|
|||||||
|
|
||||||
// cachedServiceConfig is the result of queryServiceConfig.
|
// cachedServiceConfig is the result of queryServiceConfig.
|
||||||
type cachedServiceConfig struct {
|
type cachedServiceConfig struct {
|
||||||
config mgr.Config
|
config serviceConfig
|
||||||
available bool
|
available bool
|
||||||
|
|
||||||
// lastWrite is the last write time of the service's registry key, read
|
// lastWrite is the last write time of the service's registry key, read
|
||||||
@@ -280,7 +279,7 @@ func (cache *serviceConfigCache) close() error {
|
|||||||
|
|
||||||
// getCachedServiceConfig returns the configuration of a service from the cache,
|
// getCachedServiceConfig returns the configuration of a service from the cache,
|
||||||
// or queries and caches it on a miss.
|
// or queries and caches it on a miss.
|
||||||
func (c *Collector) getCachedServiceConfig(serviceName string) (mgr.Config, bool, error) {
|
func (c *Collector) getCachedServiceConfig(serviceName string) (serviceConfig, bool, error) {
|
||||||
entry, generation, ok := c.configCache.get(serviceName)
|
entry, generation, ok := c.configCache.get(serviceName)
|
||||||
if ok {
|
if ok {
|
||||||
return entry.config, entry.available, nil
|
return entry.config, entry.available, nil
|
||||||
@@ -296,7 +295,7 @@ func (c *Collector) getCachedServiceConfig(serviceName string) (mgr.Config, bool
|
|||||||
|
|
||||||
config, available, err := c.queryServiceConfig(serviceName)
|
config, available, err := c.queryServiceConfig(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mgr.Config{}, false, err
|
return serviceConfig{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -308,6 +307,8 @@ func (c *Collector) getCachedServiceConfig(serviceName string) (mgr.Config, bool
|
|||||||
case !available:
|
case !available:
|
||||||
// An unavailable configuration, for example a display name from a missing
|
// An unavailable configuration, for example a display name from a missing
|
||||||
// MUI file, can become available without a change to the service key.
|
// MUI file, can become available without a change to the service key.
|
||||||
|
case !config.extendedAvailable:
|
||||||
|
// The delayed auto-start flag and triggers are queried again on the next scrape.
|
||||||
case time.Since(lastWrite) < configCacheMinKeyAge:
|
case time.Since(lastWrite) < configCacheMinKeyAge:
|
||||||
// Cached on a later scrape, once a second write in the same tick is ruled out.
|
// Cached on a later scrape, once a second write in the same tick is ruled out.
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
"golang.org/x/sys/windows/svc/mgr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// newTestConfigCache returns a cache that watches a temporary key below
|
// newTestConfigCache returns a cache that watches a temporary key below
|
||||||
@@ -68,7 +67,7 @@ func newTestConfigCache(t *testing.T) (*serviceConfigCache, registry.Key) {
|
|||||||
|
|
||||||
_, generation, _ := cache.get(name)
|
_, generation, _ := cache.get(name)
|
||||||
cache.put(name, generation, cachedServiceConfig{
|
cache.put(name, generation, cachedServiceConfig{
|
||||||
config: mgr.Config{DisplayName: name},
|
config: serviceConfig{DisplayName: name},
|
||||||
available: true,
|
available: true,
|
||||||
lastWrite: lastWrite,
|
lastWrite: lastWrite,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -95,6 +95,32 @@ type Collector struct {
|
|||||||
metricCache metriccache.Cache[string, serviceValues]
|
metricCache metriccache.Cache[string, serviceValues]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serviceConfig is the configuration of a service that the collector publishes.
|
||||||
|
// Of the embedded [mgr.Config], only BinaryPathName, DisplayName, StartType,
|
||||||
|
// ServiceStartName and DelayedAutoStart are set. DelayedAutoStart is only
|
||||||
|
// queried for auto-start services and false for the others.
|
||||||
|
type serviceConfig struct {
|
||||||
|
mgr.Config
|
||||||
|
|
||||||
|
// triggerStart is true if the service has triggers, which start or stop it
|
||||||
|
// on events such as a device arrival or a network change.
|
||||||
|
triggerStart bool
|
||||||
|
|
||||||
|
// extendedAvailable is true if DelayedAutoStart and triggerStart were queried
|
||||||
|
// successfully. Otherwise both are false and their labels stay empty.
|
||||||
|
extendedAvailable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceTriggerInfo mirrors SERVICE_TRIGGER_INFO, which QueryServiceConfig2
|
||||||
|
// returns for SERVICE_CONFIG_TRIGGER_INFO. golang.org/x/sys/windows doesn't define it.
|
||||||
|
// Only cTriggers is read; the pointers point into the same buffer.
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger_info
|
||||||
|
type serviceTriggerInfo struct {
|
||||||
|
triggers uint32 // cTriggers
|
||||||
|
trigger uintptr // pTriggers
|
||||||
|
_ uintptr // pReserved
|
||||||
|
}
|
||||||
|
|
||||||
// serviceValues holds the inputs of the metrics of a service besides its name.
|
// serviceValues holds the inputs of the metrics of a service besides its name.
|
||||||
type serviceValues struct {
|
type serviceValues struct {
|
||||||
state uint32
|
state uint32
|
||||||
@@ -102,7 +128,10 @@ type serviceValues struct {
|
|||||||
displayName string
|
displayName string
|
||||||
runAs string
|
runAs string
|
||||||
pathName string
|
pathName string
|
||||||
processID uint32
|
// delayedStart and triggerStart are the label values "true" or "false".
|
||||||
|
delayedStart string
|
||||||
|
triggerStart string
|
||||||
|
processID uint32
|
||||||
// processStartTime is the creation time of the process in nanoseconds,
|
// processStartTime is the creation time of the process in nanoseconds,
|
||||||
// or 0 if it is unknown and the process metric is not published.
|
// or 0 if it is unknown and the process metric is not published.
|
||||||
processStartTime uint64
|
processStartTime uint64
|
||||||
@@ -187,7 +216,9 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
|||||||
|
|
||||||
c.serviceConfigPoolBytes = sync.Pool{
|
c.serviceConfigPoolBytes = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
return new([]byte)
|
buf := make([]byte, 1024)
|
||||||
|
|
||||||
|
return &buf
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +227,7 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
|||||||
c.info = prometheus.NewDesc(
|
c.info = prometheus.NewDesc(
|
||||||
prometheus.BuildFQName(types.Namespace, Name, "info"),
|
prometheus.BuildFQName(types.Namespace, Name, "info"),
|
||||||
"A metric with a constant '1' value labeled with service information",
|
"A metric with a constant '1' value labeled with service information",
|
||||||
[]string{"service", "display_name", "run_as", "path_name"},
|
[]string{"service", "display_name", "run_as", "path_name", "delayed_start", "trigger_start"},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
c.state = prometheus.NewDesc(
|
c.state = prometheus.NewDesc(
|
||||||
@@ -408,6 +439,13 @@ func (c *Collector) collectService(ch chan<- prometheus.Metric, scrape *metricca
|
|||||||
processID: service.status.ProcessId,
|
processID: service.status.ProcessId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The labels stay empty if the service configuration or the delayed
|
||||||
|
// auto-start flag and triggers are not available.
|
||||||
|
if configAvailable && serviceConfig.extendedAvailable {
|
||||||
|
values.delayedStart = strconv.FormatBool(serviceConfig.DelayedAutoStart)
|
||||||
|
values.triggerStart = strconv.FormatBool(serviceConfig.triggerStart)
|
||||||
|
}
|
||||||
|
|
||||||
if values.processID != 0 {
|
if values.processID != 0 {
|
||||||
values.processStartTime = c.processStartTime(serviceName, values.processID)
|
values.processStartTime = c.processStartTime(serviceName, values.processID)
|
||||||
}
|
}
|
||||||
@@ -435,6 +473,8 @@ func (c *Collector) serviceMetrics(serviceName string, values serviceValues) []p
|
|||||||
values.displayName,
|
values.displayName,
|
||||||
values.runAs,
|
values.runAs,
|
||||||
values.pathName,
|
values.pathName,
|
||||||
|
values.delayedStart,
|
||||||
|
values.triggerStart,
|
||||||
))
|
))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -588,16 +628,16 @@ func (c *Collector) queryAllServices() ([]serviceEntry, error) {
|
|||||||
|
|
||||||
// queryServiceConfig opens the service and queries its configuration.
|
// queryServiceConfig opens the service and queries its configuration.
|
||||||
// configAvailable is false if the service has no readable configuration.
|
// configAvailable is false if the service has no readable configuration.
|
||||||
func (c *Collector) queryServiceConfig(serviceName string) (mgr.Config, bool, error) {
|
func (c *Collector) queryServiceConfig(serviceName string) (serviceConfig, bool, error) {
|
||||||
serviceNamePtr, err := windows.UTF16PtrFromString(serviceName)
|
serviceNamePtr, err := windows.UTF16PtrFromString(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mgr.Config{}, false, fmt.Errorf("invalid service name: %w", err)
|
return serviceConfig{}, false, fmt.Errorf("invalid service name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open connection for service handler.
|
// Open connection for service handler.
|
||||||
serviceHandle, err := windows.OpenService(c.serviceManagerHandle.Handle, serviceNamePtr, windows.SERVICE_QUERY_CONFIG)
|
serviceHandle, err := windows.OpenService(c.serviceManagerHandle.Handle, serviceNamePtr, windows.SERVICE_QUERY_CONFIG)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mgr.Config{}, false, fmt.Errorf("failed to open service: %w", err)
|
return serviceConfig{}, false, fmt.Errorf("failed to open service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handle for each service.
|
// Create handle for each service.
|
||||||
@@ -612,10 +652,10 @@ func (c *Collector) queryServiceConfig(serviceName string) (mgr.Config, bool, er
|
|||||||
}(serviceManager)
|
}(serviceManager)
|
||||||
|
|
||||||
// Get Service Configuration.
|
// Get Service Configuration.
|
||||||
serviceConfig, err := c.getServiceConfig(serviceManager)
|
config, err := c.getServiceConfig(serviceManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && !errors.Is(err, windows.ERROR_MUI_FILE_NOT_FOUND) {
|
if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && !errors.Is(err, windows.ERROR_MUI_FILE_NOT_FOUND) {
|
||||||
return mgr.Config{}, false, fmt.Errorf("failed to get service configuration: %w", err)
|
return serviceConfig{}, false, fmt.Errorf("failed to get service configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Log(context.Background(), slog.LevelDebug, "failed collecting service config",
|
c.logger.Log(context.Background(), slog.LevelDebug, "failed collecting service config",
|
||||||
@@ -623,10 +663,10 @@ func (c *Collector) queryServiceConfig(serviceName string) (mgr.Config, bool, er
|
|||||||
slog.String("service", serviceName),
|
slog.String("service", serviceName),
|
||||||
)
|
)
|
||||||
|
|
||||||
return mgr.Config{}, false, nil
|
return serviceConfig{}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return serviceConfig, true, nil
|
return config, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Collector) getProcessStartTime(pid uint32) (uint64, error) {
|
func (c *Collector) getProcessStartTime(pid uint32) (uint64, error) {
|
||||||
@@ -657,45 +697,100 @@ func (c *Collector) getProcessStartTime(pid uint32) (uint64, error) {
|
|||||||
return uint64(creation.Nanoseconds()), nil
|
return uint64(creation.Nanoseconds()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getServiceConfig is an optimized variant of [mgr.Service] that only
|
// getServiceConfig is an optimized variant of [mgr.Service.Config] that only
|
||||||
// retrieves the necessary information.
|
// retrieves the necessary information.
|
||||||
func (c *Collector) getServiceConfig(service *mgr.Service) (mgr.Config, error) {
|
func (c *Collector) getServiceConfig(service *mgr.Service) (serviceConfig, error) {
|
||||||
var serviceConfig *windows.QUERY_SERVICE_CONFIG
|
buf := c.serviceConfigPoolBytes.Get().(*[]byte) //nolint:forcetypeassert // The pool only holds *[]byte.
|
||||||
|
defer c.serviceConfigPoolBytes.Put(buf)
|
||||||
|
|
||||||
bytesNeeded := uint32(1024)
|
err := queryServiceConfigBuffer(buf, func(data *byte, size uint32, bytesNeeded *uint32) error {
|
||||||
|
return windows.QueryServiceConfig(service.Handle, (*windows.QUERY_SERVICE_CONFIG)(unsafe.Pointer(data)), size, bytesNeeded)
|
||||||
buf, ok := c.serviceConfigPoolBytes.Get().(*[]byte)
|
})
|
||||||
if !ok || len(*buf) == 0 {
|
if err != nil {
|
||||||
*buf = make([]byte, bytesNeeded)
|
return serviceConfig{}, err
|
||||||
} else {
|
|
||||||
bytesNeeded = uint32(cap(*buf))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
queryServiceConfig := (*windows.QUERY_SERVICE_CONFIG)(unsafe.Pointer(&(*buf)[0]))
|
||||||
serviceConfig = (*windows.QUERY_SERVICE_CONFIG)(unsafe.Pointer(&(*buf)[0]))
|
|
||||||
|
|
||||||
err := windows.QueryServiceConfig(service.Handle, serviceConfig, bytesNeeded, &bytesNeeded)
|
config := serviceConfig{
|
||||||
|
BinaryPathName: windows.UTF16PtrToString(queryServiceConfig.BinaryPathName),
|
||||||
|
DisplayName: windows.UTF16PtrToString(queryServiceConfig.DisplayName),
|
||||||
|
StartType: queryServiceConfig.StartType,
|
||||||
|
ServiceStartName: windows.UTF16PtrToString(queryServiceConfig.ServiceStartName),
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failure only leaves the delayed_start and trigger_start labels empty.
|
||||||
|
// It must not hide the rest of the configuration.
|
||||||
|
if err := queryExtendedServiceConfig(service.Handle, buf, &config); err != nil {
|
||||||
|
c.logger.Log(context.Background(), slog.LevelDebug, "failed to query the delayed auto-start flag and triggers of the service",
|
||||||
|
slog.Any("err", err),
|
||||||
|
slog.String("service", service.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
config.DelayedAutoStart = false
|
||||||
|
config.triggerStart = false
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config.extendedAvailable = true
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryExtendedServiceConfig queries the delayed auto-start flag and the
|
||||||
|
// triggers of a service with QueryServiceConfig2 into config. buf must not be
|
||||||
|
// empty; the configuration strings must already be copied out of it.
|
||||||
|
func queryExtendedServiceConfig(handle windows.Handle, buf *[]byte, config *serviceConfig) error {
|
||||||
|
// The service control manager ignores the delayed auto-start flag of services
|
||||||
|
// that don't start automatically, so it is only queried for them.
|
||||||
|
if config.StartType == windows.SERVICE_AUTO_START {
|
||||||
|
var (
|
||||||
|
delayed windows.SERVICE_DELAYED_AUTO_START_INFO
|
||||||
|
bytesNeeded uint32
|
||||||
|
)
|
||||||
|
|
||||||
|
err := windows.QueryServiceConfig2(handle, windows.SERVICE_CONFIG_DELAYED_AUTO_START_INFO,
|
||||||
|
(*byte)(unsafe.Pointer(&delayed)), uint32(unsafe.Sizeof(delayed)), &bytesNeeded)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query the delayed auto-start flag: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.DelayedAutoStart = delayed.IsDelayedAutoStartUp != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
err := queryServiceConfigBuffer(buf, func(data *byte, size uint32, bytesNeeded *uint32) error {
|
||||||
|
return windows.QueryServiceConfig2(handle, windows.SERVICE_CONFIG_TRIGGER_INFO, data, size, bytesNeeded)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query the service triggers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.triggerStart = (*serviceTriggerInfo)(unsafe.Pointer(&(*buf)[0])).triggers > 0
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryServiceConfigBuffer calls query with the buffer and grows the buffer
|
||||||
|
// until the result fits. query wraps QueryServiceConfig or QueryServiceConfig2.
|
||||||
|
// The buffer must not be empty.
|
||||||
|
func queryServiceConfigBuffer(buf *[]byte, query func(data *byte, size uint32, bytesNeeded *uint32) error) error {
|
||||||
|
for {
|
||||||
|
var bytesNeeded uint32
|
||||||
|
|
||||||
|
err := query(&(*buf)[0], uint32(len(*buf)), &bytesNeeded)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) && !errors.Is(err, windows.ERROR_MORE_DATA) {
|
if !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) && !errors.Is(err, windows.ERROR_MORE_DATA) {
|
||||||
return mgr.Config{}, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytesNeeded <= uint32(len(*buf)) {
|
if bytesNeeded <= uint32(len(*buf)) {
|
||||||
return mgr.Config{}, fmt.Errorf("win32 reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(cap(*buf)), bytesNeeded, err)
|
return fmt.Errorf("win32 reports the buffer as too small (%d bytes needed, %d available): %w", bytesNeeded, len(*buf), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
*buf = make([]byte, bytesNeeded)
|
*buf = make([]byte, bytesNeeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer c.serviceConfigPoolBytes.Put(buf)
|
|
||||||
|
|
||||||
return mgr.Config{
|
|
||||||
BinaryPathName: windows.UTF16PtrToString(serviceConfig.BinaryPathName),
|
|
||||||
DisplayName: windows.UTF16PtrToString(serviceConfig.DisplayName),
|
|
||||||
StartType: serviceConfig.StartType,
|
|
||||||
ServiceStartName: windows.UTF16PtrToString(serviceConfig.ServiceStartName),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
|
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@@ -98,6 +99,40 @@ func serviceMetrics(t assert.TestingT, c *Collector, name string) map[string]flo
|
|||||||
return metrics
|
return metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setStartTrigger gives a service a trigger that starts it once the first IP
|
||||||
|
// address becomes available. golang.org/x/sys/windows/svc/mgr can't set triggers.
|
||||||
|
func setStartTrigger(t *testing.T, service *mgr.Service) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID.
|
||||||
|
subtype := windows.GUID{Data1: 0x4f27f2de, Data2: 0x14e2, Data3: 0x430b, Data4: [8]byte{0xa5, 0x49, 0x7c, 0xd4, 0x8c, 0xbc, 0x82, 0x45}}
|
||||||
|
|
||||||
|
// SERVICE_TRIGGER.
|
||||||
|
trigger := struct {
|
||||||
|
triggerType uint32
|
||||||
|
action uint32
|
||||||
|
subtype *windows.GUID
|
||||||
|
dataItems uint32
|
||||||
|
data uintptr
|
||||||
|
}{
|
||||||
|
triggerType: 3, // SERVICE_TRIGGER_TYPE_IP_ADDRESS_AVAILABILITY
|
||||||
|
action: 1, // SERVICE_TRIGGER_ACTION_SERVICE_START
|
||||||
|
subtype: &subtype,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVICE_TRIGGER_INFO.
|
||||||
|
info := struct {
|
||||||
|
triggers uint32
|
||||||
|
trigger unsafe.Pointer
|
||||||
|
reserved uintptr
|
||||||
|
}{
|
||||||
|
triggers: 1,
|
||||||
|
trigger: unsafe.Pointer(&trigger),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, windows.ChangeServiceConfig2(service.Handle, windows.SERVICE_CONFIG_TRIGGER_INFO, (*byte)(unsafe.Pointer(&info))))
|
||||||
|
}
|
||||||
|
|
||||||
// TestCollectorPicksUpConfigChanges creates a service, changes its configuration
|
// TestCollectorPicksUpConfigChanges creates a service, changes its configuration
|
||||||
// through the service control manager and checks that the cached configuration
|
// through the service control manager and checks that the cached configuration
|
||||||
// is replaced. It needs an elevated process, which the Windows CI runners are.
|
// is replaced. It needs an elevated process, which the Windows CI runners are.
|
||||||
@@ -148,7 +183,7 @@ func TestCollectorPicksUpConfigChanges(t *testing.T) {
|
|||||||
|
|
||||||
metrics := serviceMetrics(t, c, name)
|
metrics := serviceMetrics(t, c, name)
|
||||||
assert.InDelta(t, 1.0, metrics["windows_service_start_mode{start_mode=manual}"], 0)
|
assert.InDelta(t, 1.0, metrics["windows_service_start_mode{start_mode=manual}"], 0)
|
||||||
assert.Contains(t, metrics, `windows_service_info{display_name=before,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem}`)
|
assert.Contains(t, metrics, `windows_service_info{delayed_start=false,display_name=before,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem,trigger_start=false}`)
|
||||||
|
|
||||||
_, _, ok := c.configCache.get(name)
|
_, _, ok := c.configCache.get(name)
|
||||||
require.True(t, ok, "the configuration must be cached after a scrape")
|
require.True(t, ok, "the configuration must be cached after a scrape")
|
||||||
@@ -156,16 +191,45 @@ func TestCollectorPicksUpConfigChanges(t *testing.T) {
|
|||||||
config, err := service.Config()
|
config, err := service.Config()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
config.StartType = mgr.StartDisabled
|
// The service is never started. It is switched back to manual start below,
|
||||||
|
// so that a killed test doesn't leave an auto-start service behind.
|
||||||
|
config.StartType = mgr.StartAutomatic
|
||||||
|
config.DelayedAutoStart = true
|
||||||
config.DisplayName = "after"
|
config.DisplayName = "after"
|
||||||
require.NoError(t, service.UpdateConfig(config))
|
require.NoError(t, service.UpdateConfig(config))
|
||||||
|
|
||||||
// The change notification is delivered asynchronously.
|
// The change notification is delivered asynchronously.
|
||||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||||
metrics := serviceMetrics(collect, c, name)
|
metrics := serviceMetrics(collect, c, name)
|
||||||
assert.InDelta(collect, 1.0, metrics["windows_service_start_mode{start_mode=disabled}"], 0)
|
assert.InDelta(collect, 1.0, metrics["windows_service_start_mode{start_mode=auto}"], 0)
|
||||||
assert.InDelta(collect, 0.0, metrics["windows_service_start_mode{start_mode=manual}"], 0)
|
assert.InDelta(collect, 0.0, metrics["windows_service_start_mode{start_mode=manual}"], 0)
|
||||||
assert.Contains(collect, metrics, `windows_service_info{display_name=after,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem}`)
|
assert.Contains(collect, metrics, `windows_service_info{delayed_start=true,display_name=after,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem,trigger_start=false}`)
|
||||||
|
}, 5*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
|
// Windows ignores the delayed auto-start flag of a manual service, and so does the label.
|
||||||
|
config.StartType = mgr.StartManual
|
||||||
|
require.NoError(t, service.UpdateConfig(config))
|
||||||
|
|
||||||
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||||
|
metrics := serviceMetrics(collect, c, name)
|
||||||
|
assert.InDelta(collect, 1.0, metrics["windows_service_start_mode{start_mode=manual}"], 0)
|
||||||
|
assert.Contains(collect, metrics, `windows_service_info{delayed_start=false,display_name=after,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem,trigger_start=false}`)
|
||||||
|
}, 5*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
|
// Cache the changed configuration, so that the trigger must invalidate it.
|
||||||
|
time.Sleep(configCacheMinKeyAge + 100*time.Millisecond)
|
||||||
|
serviceMetrics(t, c, name)
|
||||||
|
|
||||||
|
_, _, ok = c.configCache.get(name)
|
||||||
|
require.True(t, ok, "the changed configuration must be cached after a scrape")
|
||||||
|
|
||||||
|
// Triggers are stored in the TriggerInfo subkey of the service key. Creating
|
||||||
|
// it changes the last write time of the service key.
|
||||||
|
setStartTrigger(t, service)
|
||||||
|
|
||||||
|
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||||
|
metrics := serviceMetrics(collect, c, name)
|
||||||
|
assert.Contains(collect, metrics, `windows_service_info{delayed_start=false,display_name=after,path_name=C:\Windows\System32\cmd.exe,run_as=LocalSystem,trigger_start=true}`)
|
||||||
}, 5*time.Second, 50*time.Millisecond)
|
}, 5*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
// The service control manager removes the service once the last handle is closed.
|
// The service control manager removes the service once the last handle is closed.
|
||||||
@@ -182,3 +246,18 @@ func TestCollectorPicksUpConfigChanges(t *testing.T) {
|
|||||||
assert.False(collect, ok, "the configuration of a deleted service must be dropped")
|
assert.False(collect, ok, "the configuration of a deleted service must be dropped")
|
||||||
}, 5*time.Second, 50*time.Millisecond)
|
}, 5*time.Second, 50*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServiceTriggerInfoLayout checks the mirror of SERVICE_TRIGGER_INFO
|
||||||
|
// (winsvc.h): DWORD cTriggers, PSERVICE_TRIGGER pTriggers and PBYTE pReserved.
|
||||||
|
// The pointers are pointer-aligned, so cTriggers is padded to a pointer word.
|
||||||
|
func TestServiceTriggerInfoLayout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var info serviceTriggerInfo
|
||||||
|
|
||||||
|
pointerSize := unsafe.Sizeof(uintptr(0))
|
||||||
|
|
||||||
|
assert.Equal(t, uintptr(0), unsafe.Offsetof(info.triggers))
|
||||||
|
assert.Equal(t, pointerSize, unsafe.Offsetof(info.trigger))
|
||||||
|
assert.Equal(t, 3*pointerSize, unsafe.Sizeof(info))
|
||||||
|
}
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ windows_scheduled_task_state{state="running",task="/Microsoft/Windows/PLA/GAEven
|
|||||||
windows_scheduled_task_state{state="unknown",task="/Microsoft/Windows/PLA/GAEvents"} 0
|
windows_scheduled_task_state{state="unknown",task="/Microsoft/Windows/PLA/GAEvents"} 0
|
||||||
# HELP windows_service_info A metric with a constant '1' value labeled with service information
|
# HELP windows_service_info A metric with a constant '1' value labeled with service information
|
||||||
# TYPE windows_service_info gauge
|
# TYPE windows_service_info gauge
|
||||||
windows_service_info{display_name="Themes",path_name="C:\\Windows\\System32\\svchost.exe -k netsvcs -p",run_as="LocalSystem",service="Themes"} 1
|
windows_service_info{delayed_start="false",display_name="Themes",path_name="C:\\Windows\\System32\\svchost.exe -k netsvcs -p",run_as="LocalSystem",service="Themes",trigger_start="false"} 1
|
||||||
# HELP windows_service_process Process of started service. The value is the creation time of the process as a unix timestamp.
|
# HELP windows_service_process Process of started service. The value is the creation time of the process as a unix timestamp.
|
||||||
# TYPE windows_service_process gauge
|
# TYPE windows_service_process gauge
|
||||||
# HELP windows_service_start_mode The start mode of the service (StartMode)
|
# HELP windows_service_start_mode The start mode of the service (StartMode)
|
||||||
|
|||||||
Reference in New Issue
Block a user