Skip to content

Commit bfb16e0

Browse files
Add per_user services filter to windows_service (#24088) (#24204)
* Add per_user services filter criterion to windows_service Add a per_user (true/false) match criterion to the services filter, mirroring trigger_start, to select services by whether they are Windows per-user service instances. Configure per_user: false to exclude per-user services from collection. Warn when grouping is enabled alongside a per_user: false filter, since excluded services cannot be grouped. * Add changelog entry * Reuse ServiceAssertion in per_user tests Use the existing ServiceAssertion / assert_service_check_and_metrics helpers in the per_user tests for consistency with the rest of the suite; this also asserts the uptime/state/restarts metrics, not just the service check. * Read service_type from ServiceView instead of a match() parameter Pass the enumeration's ServiceType into ServiceView and have ServiceFilter.match read service_view.service_type, consistent with how it reads the other service properties, rather than threading service_type through match() as an argument. * Make ServiceView.service_type lazy with config fallback Read service_type from the service config when it was not provided to the constructor, caching like the other ServiceView properties, and include it in __str__. (cherry picked from commit 3ca0f7d) Co-authored-by: Branden Clark <branden.clark@datadoghq.com>
1 parent 36d461d commit bfb16e0

5 files changed

Lines changed: 115 additions & 5 deletions

File tree

windows_service/assets/configuration/spec.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ files:
2424
- true
2525
- false
2626
27+
Services can also be selectively monitored by whether they are Windows per-user services,
28+
which are instantiated per user logon session and named `<service name>_<LUID>`
29+
(for example `OneSyncSvc_443f50`). The possible values for `per_user` are:
30+
- true
31+
- false
32+
To exclude per-user services from collection, monitor only non-per-user services with
33+
`per_user: false`.
34+
2735
If any service is set to `ALL`, all services registered with the SCM will be monitored, and
2836
all other patterns provided in this instance will be ignored.
2937
@@ -45,6 +53,7 @@ files:
4553
- name: <SERVICE_NAME_2>
4654
- startup_type: automatic
4755
trigger_start: false
56+
- per_user: false
4857
- name: disable_legacy_service_tag
4958
description: |
5059
Whether or not to stop submitting the tag `service` that has been renamed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a `per_user` services filter criterion to select or exclude Windows per-user services (use `per_user: false` to exclude them from collection).

windows_service/datadog_checks/windows_service/data/conf.yaml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ instances:
2727
## - true
2828
## - false
2929
##
30+
## Services can also be selectively monitored by whether they are Windows per-user services,
31+
## which are instantiated per user logon session and named `<service name>_<LUID>`
32+
## (for example `OneSyncSvc_443f50`). The possible values for `per_user` are:
33+
## - true
34+
## - false
35+
## To exclude per-user services from collection, monitor only non-per-user services with
36+
## `per_user: false`.
37+
##
3038
## If any service is set to `ALL`, all services registered with the SCM will be monitored, and
3139
## all other patterns provided in this instance will be ignored.
3240
##
@@ -41,6 +49,7 @@ instances:
4149
- name: <SERVICE_NAME_2>
4250
- startup_type: automatic
4351
trigger_start: false
52+
- per_user: false
4453

4554
## @param disable_legacy_service_tag - boolean - optional - default: false
4655
## Whether or not to stop submitting the tag `service` that has been renamed

windows_service/datadog_checks/windows_service/windows_service.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
USER_SERVICE_LUID_SUFFIX_RE = re.compile(r'_[0-9A-Fa-f]+$')
2323

2424

25+
def _is_per_user_service(service_type: int) -> bool:
26+
"""True for per-user service instances (named <template>_<LUID>)."""
27+
return bool(service_type & SERVICE_USERSERVICE_INSTANCE)
28+
29+
2530
def _group_per_user_service_name(name: str, service_type: int) -> str:
2631
"""Strip the per-user LUID suffix so instances group under their template name."""
27-
if service_type & SERVICE_USERSERVICE_INSTANCE:
32+
if _is_per_user_service(service_type):
2833
return USER_SERVICE_LUID_SUFFIX_RE.sub('', name)
2934
return name
3035

@@ -42,10 +47,11 @@ class TriggerInfo(ctypes.Structure):
4247

4348

4449
class ServiceFilter(object):
45-
def __init__(self, name=None, startup_type=None, trigger_start=None):
50+
def __init__(self, name=None, startup_type=None, trigger_start=None, per_user=None):
4651
self.name = name
4752
self.startup_type = startup_type
4853
self.trigger_start = trigger_start
54+
self.per_user = per_user
4955

5056
self._init_patterns()
5157

@@ -61,6 +67,9 @@ def match(self, service_view):
6167
if self.name is not None:
6268
if not self._name_re.match(service_view.name):
6369
return False
70+
if self.per_user is not None:
71+
if self.per_user != _is_per_user_service(service_view.service_type):
72+
return False
6473
if self.startup_type is not None:
6574
if self.startup_type.lower() != service_view.startup_type_string().lower():
6675
return False
@@ -79,6 +88,8 @@ def __str__(self):
7988
vals.append('startup_type={}'.format(self.startup_type))
8089
if self.trigger_start is not None:
8190
vals.append('trigger_start={}'.format(self.trigger_start))
91+
if self.per_user is not None:
92+
vals.append('per_user={}'.format(self.per_user))
8293
# Example:
8394
# - ServiceFilter(name=EventLog)
8495
# - ServiceFilter(startup_type=automatic)
@@ -113,7 +124,8 @@ def from_config(cls, item, wmi_compat=False):
113124
name = cls._wmi_compat_name(name)
114125
startup_type = item.get('startup_type', None)
115126
trigger_start = item.get('trigger_start', None)
116-
obj = cls(name=name, startup_type=startup_type, trigger_start=trigger_start)
127+
per_user = item.get('per_user', None)
128+
obj = cls(name=name, startup_type=startup_type, trigger_start=trigger_start, per_user=per_user)
117129
else:
118130
raise Exception("Invalid type '{}' for service".format(type(item).__name__))
119131
return obj
@@ -130,7 +142,7 @@ class ServiceView(object):
130142
STARTUP_TYPE_UNKNOWN = "unknown"
131143
DISPLAY_NAME_UNKNOWN = "Not_Found"
132144

133-
def __init__(self, scm_handle, name):
145+
def __init__(self, scm_handle, name, service_type=None):
134146
self.scm_handle = scm_handle
135147
self.name = name
136148

@@ -139,11 +151,14 @@ def __init__(self, scm_handle, name):
139151
self._service_config = None
140152
self._is_delayed_auto = None
141153
self._trigger_count = None
154+
self._service_type = service_type
142155

143156
def __str__(self):
144157
vals = []
145158
if self.name is not None:
146159
vals.append('name={}'.format(self.name))
160+
if self._service_type is not None:
161+
vals.append('service_type=0x{:X}'.format(self._service_type))
147162
if self._startup_type is not None:
148163
vals.append('startup_type={}'.format(self.startup_type_string()))
149164
if self._trigger_count is not None:
@@ -167,6 +182,12 @@ def service_config(self):
167182
self._service_config = win32service.QueryServiceConfig(self.hSvc)
168183
return self._service_config
169184

185+
@property
186+
def service_type(self):
187+
if self._service_type is None:
188+
self._service_type = self.service_config[0]
189+
return self._service_type
190+
170191
@property
171192
def startup_type(self):
172193
if self._startup_type is None:
@@ -317,14 +338,22 @@ def check(self, instance):
317338

318339
group_per_user_services = instance.get('group_per_user_services', False)
319340

341+
# Exclusion (per_user: false) takes precedence over grouping; excluded services are never
342+
# collected, so they can't be grouped.
343+
if group_per_user_services and any(f.per_user is False for f in service_filters):
344+
self.warning(
345+
"group_per_user_services is enabled but a services filter excludes per-user services "
346+
"(per_user: false); excluded services are not collected and will not be grouped."
347+
)
348+
320349
for service_status_process_enum in service_status_process_enums:
321350
service_name = service_status_process_enum["ServiceName"]
322351
display_name = service_status_process_enum["DisplayName"]
323352
state = service_status_process_enum["CurrentState"]
324353
service_pid = service_status_process_enum["ProcessId"]
325354
service_type = service_status_process_enum.get("ServiceType", 0)
326355

327-
service_view = ServiceView(scm_handle, service_name)
356+
service_view = ServiceView(scm_handle, service_name, service_type)
328357

329358
# Names used for tags; for per-user services these collapse the per-session LUID suffix
330359
# so all instances report under their template name.

windows_service/tests/test_windows_service.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,68 @@ def test_group_per_user_services_with_name_filter(aggregator, check, instance_gr
505505
assert_service_check_and_metrics(aggregator, services)
506506

507507

508+
def test_per_user_false_excludes_per_user_services(aggregator, check):
509+
# `per_user: false` collects only non-per-user services, so per-user instances are dropped.
510+
instance = {'services': [{'per_user': False}], 'disable_legacy_service_tag': True}
511+
c = check(instance)
512+
513+
with patch('win32service.EnumServicesStatusEx', return_value=_per_user_mock_services()):
514+
c.check(instance)
515+
516+
services = [
517+
ServiceAssertion('Dnscache', win32service.SERVICE_RUNNING),
518+
ServiceAssertion('OneSyncSvc_443f50', win32service.SERVICE_RUNNING, count=0),
519+
ServiceAssertion('OneSyncSvc_18f113', win32service.SERVICE_RUNNING, count=0),
520+
]
521+
assert_service_check_and_metrics(aggregator, services)
522+
523+
524+
def test_per_user_true_collects_only_per_user_services(aggregator, check):
525+
instance = {'services': [{'per_user': True}], 'disable_legacy_service_tag': True}
526+
c = check(instance)
527+
528+
with patch('win32service.EnumServicesStatusEx', return_value=_per_user_mock_services()):
529+
c.check(instance)
530+
531+
services = [
532+
ServiceAssertion('OneSyncSvc_443f50', win32service.SERVICE_RUNNING),
533+
ServiceAssertion('OneSyncSvc_18f113', win32service.SERVICE_RUNNING),
534+
ServiceAssertion('Dnscache', win32service.SERVICE_RUNNING, count=0),
535+
]
536+
assert_service_check_and_metrics(aggregator, services)
537+
538+
539+
def test_per_user_composes_with_name_filter(aggregator, check):
540+
# A name filter that matches the per-user instances but is gated to non-per-user collects nothing.
541+
instance = {'services': [{'name': 'OneSyncSvc', 'per_user': False}], 'disable_legacy_service_tag': True}
542+
c = check(instance)
543+
544+
with patch('win32service.EnumServicesStatusEx', return_value=_per_user_mock_services()):
545+
c.check(instance)
546+
547+
services = [
548+
ServiceAssertion('OneSyncSvc_443f50', win32service.SERVICE_RUNNING, count=0),
549+
ServiceAssertion('OneSyncSvc_18f113', win32service.SERVICE_RUNNING, count=0),
550+
# The named filter still goes unmatched and reports UNKNOWN once.
551+
ServiceAssertion('OneSyncSvc', -1, count=1),
552+
]
553+
assert_service_check_and_metrics(aggregator, services)
554+
555+
556+
def test_per_user_false_with_grouping_warns(aggregator, check):
557+
instance = {
558+
'services': [{'per_user': False}],
559+
'group_per_user_services': True,
560+
'disable_legacy_service_tag': True,
561+
}
562+
c = check(instance)
563+
564+
with patch('win32service.EnumServicesStatusEx', return_value=_per_user_mock_services()):
565+
c.check(instance)
566+
567+
assert any('will not be grouped' in w for w in c.warnings)
568+
569+
508570
@pytest.mark.e2e
509571
def test_basic_e2e(dd_agent_check, check, instance_basic):
510572
aggregator = dd_agent_check(instance_basic)

0 commit comments

Comments
 (0)