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:
Jan-Otto Kröpke
2026-10-16 21:30:11 +02:00
committed by GitHub
parent 4f3152d84d
commit 4f8fc2baef
8 changed files with 289 additions and 57 deletions
+55 -11
View File
@@ -30,22 +30,24 @@ Recommended to keep down number of returned metrics. Default: `auto,boot,manual,
## Configuration cache
The service configuration (display name, start mode, path and account, used by
`windows_service_info` and `windows_service_start_mode`) is cached between scrapes.
The service configuration (display name, start mode, delayed start, triggers, path and
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
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
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
| 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_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_process` | Process of started service. The value is the creation time of the process as a Unix timestamp | gauge | service, process_id |
| 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, 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_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 |
### States
@@ -69,6 +71,22 @@ A service can have the following start modes:
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
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
# 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{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="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{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.
# TYPE windows_service_process gauge
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."
```
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"}`.
+1
View File
@@ -900,6 +900,7 @@ windows_service_state{state="running"} == 0
### Label changes
- `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.
- 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="------"`.
+13
View File
@@ -9,6 +9,19 @@
`RegNotifyChangeKeyValue` instead. Arm it with `REG_NOTIFY_THREAD_AGNOSTIC`:
without it the registration ends when the arming OS thread exits, and Go
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,
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
+5 -4
View File
@@ -26,7 +26,6 @@ import (
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc/mgr"
)
const (
@@ -54,7 +53,7 @@ var errConfigCacheClosed = errors.New("service configuration cache is closed")
// cachedServiceConfig is the result of queryServiceConfig.
type cachedServiceConfig struct {
config mgr.Config
config serviceConfig
available bool
// 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,
// 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)
if ok {
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)
if err != nil {
return mgr.Config{}, false, err
return serviceConfig{}, false, err
}
switch {
@@ -308,6 +307,8 @@ func (c *Collector) getCachedServiceConfig(serviceName string) (mgr.Config, bool
case !available:
// An unavailable configuration, for example a display name from a missing
// 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:
// Cached on a later scrape, once a second write in the same tick is ruled out.
default:
@@ -25,7 +25,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows/registry"
"golang.org/x/sys/windows/svc/mgr"
)
// 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)
cache.put(name, generation, cachedServiceConfig{
config: mgr.Config{DisplayName: name},
config: serviceConfig{DisplayName: name},
available: true,
lastWrite: lastWrite,
})
+130 -35
View File
@@ -95,6 +95,32 @@ type Collector struct {
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.
type serviceValues struct {
state uint32
@@ -102,7 +128,10 @@ type serviceValues struct {
displayName string
runAs 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,
// or 0 if it is unknown and the process metric is not published.
processStartTime uint64
@@ -187,7 +216,9 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.serviceConfigPoolBytes = sync.Pool{
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(
prometheus.BuildFQName(types.Namespace, Name, "info"),
"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,
)
c.state = prometheus.NewDesc(
@@ -408,6 +439,13 @@ func (c *Collector) collectService(ch chan<- prometheus.Metric, scrape *metricca
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 {
values.processStartTime = c.processStartTime(serviceName, values.processID)
}
@@ -435,6 +473,8 @@ func (c *Collector) serviceMetrics(serviceName string, values serviceValues) []p
values.displayName,
values.runAs,
values.pathName,
values.delayedStart,
values.triggerStart,
))
var (
@@ -588,16 +628,16 @@ func (c *Collector) queryAllServices() ([]serviceEntry, error) {
// queryServiceConfig opens the service and queries its 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)
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.
serviceHandle, err := windows.OpenService(c.serviceManagerHandle.Handle, serviceNamePtr, windows.SERVICE_QUERY_CONFIG)
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.
@@ -612,10 +652,10 @@ func (c *Collector) queryServiceConfig(serviceName string) (mgr.Config, bool, er
}(serviceManager)
// Get Service Configuration.
serviceConfig, err := c.getServiceConfig(serviceManager)
config, err := c.getServiceConfig(serviceManager)
if err != nil {
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",
@@ -623,10 +663,10 @@ func (c *Collector) queryServiceConfig(serviceName string) (mgr.Config, bool, er
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) {
@@ -657,45 +697,100 @@ func (c *Collector) getProcessStartTime(pid uint32) (uint64, error) {
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.
func (c *Collector) getServiceConfig(service *mgr.Service) (mgr.Config, error) {
var serviceConfig *windows.QUERY_SERVICE_CONFIG
func (c *Collector) getServiceConfig(service *mgr.Service) (serviceConfig, error) {
buf := c.serviceConfigPoolBytes.Get().(*[]byte) //nolint:forcetypeassert // The pool only holds *[]byte.
defer c.serviceConfigPoolBytes.Put(buf)
bytesNeeded := uint32(1024)
buf, ok := c.serviceConfigPoolBytes.Get().(*[]byte)
if !ok || len(*buf) == 0 {
*buf = make([]byte, bytesNeeded)
} else {
bytesNeeded = uint32(cap(*buf))
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)
})
if err != nil {
return serviceConfig{}, err
}
for {
serviceConfig = (*windows.QUERY_SERVICE_CONFIG)(unsafe.Pointer(&(*buf)[0]))
queryServiceConfig := (*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 {
break
return nil
}
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)) {
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)
}
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"
"testing"
"time"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
@@ -98,6 +99,40 @@ func serviceMetrics(t assert.TestingT, c *Collector, name string) map[string]flo
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
// through the service control manager and checks that the cached configuration
// 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)
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)
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()
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"
require.NoError(t, service.UpdateConfig(config))
// The change notification is delivered asynchronously.
require.EventuallyWithT(t, func(collect *assert.CollectT) {
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.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)
// 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")
}, 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))
}
+1 -1
View File
@@ -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
# HELP windows_service_info A metric with a constant '1' value labeled with service information
# 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.
# TYPE windows_service_process gauge
# HELP windows_service_start_mode The start mode of the service (StartMode)