Skip to content

Commit 1dc27b5

Browse files
authored
Add uptime metric to windows services (DataDog#21778)
* Add uptime metric to windows services * Update unit tests * Add docs * Update manifest.json * Fix metadata.csv
1 parent f25c304 commit 1dc27b5

8 files changed

Lines changed: 262 additions & 17 deletions

File tree

windows_service/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Beginning with Agent version 7.73, the check automatically adds a `windows_servi
9595

9696
### Metrics
9797

98-
The Windows Service check does not include any metrics.
98+
See [metadata.csv][19] for a list of metrics provided by this integration.
9999

100100
### Events
101101

@@ -141,3 +141,4 @@ If the service is present in the output, permissions are the issue. To give the
141141
[16]: https://learn.microsoft.com/en-US/troubleshoot/windows-server/group-policy/configure-group-policies-set-security
142142
[17]: https://learn.microsoft.com/en-us/windows/win32/services/service-trigger-events
143143
[18]: /integrations/windows-service?search=windows%20service
144+
[19]: https://github.com/DataDog/integrations-core/blob/master/windows_service/metadata.csv
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add uptime metric to windows services

windows_service/datadog_checks/windows_service/windows_service.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
import ctypes
55
import re
6+
import time
67

8+
import psutil
79
import pywintypes
810
import win32service
911
import winerror
@@ -142,6 +144,8 @@ def __str__(self):
142144
@property
143145
def hSvc(self):
144146
if self._hSvc is None:
147+
# Handle will automatically be closed by pywin32
148+
# https://mhammond.github.io/pywin32/PySC_HANDLE.html
145149
self._hSvc = win32service.OpenService(self.scm_handle, self.name, win32service.SERVICE_QUERY_CONFIG)
146150
return self._hSvc
147151

@@ -204,6 +208,21 @@ def startup_type_string(self):
204208
return startup_type_string
205209

206210

211+
def _build_process_cache() -> dict[int, "psutil.Process"]:
212+
process_cache_dict = {}
213+
for proc in psutil.process_iter(attrs=['pid', 'create_time']):
214+
process_cache_dict[proc.pid] = proc
215+
return process_cache_dict
216+
217+
218+
def _get_process_uptime_from_cache(pid: int, process_cache: dict[int, "psutil.Process"]) -> int:
219+
process = process_cache.get(pid)
220+
if process is None:
221+
return 0
222+
223+
return int(time.time() - process.create_time())
224+
225+
207226
class WindowsService(AgentCheck):
208227
SERVICE_CHECK_NAME = 'windows_service.state'
209228
# https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status_process
@@ -225,6 +244,7 @@ class WindowsService(AgentCheck):
225244
win32service.SERVICE_PAUSE_PENDING: "pause_pending",
226245
win32service.SERVICE_PAUSED: "paused",
227246
}
247+
UNKNOWN_LITERAL = "unknown"
228248

229249
def check(self, instance):
230250
services = instance.get('services', [])
@@ -243,14 +263,21 @@ def check(self, instance):
243263
services_unseen = {f.name for f in service_filters if f.name is not None}
244264

245265
try:
266+
# Handle will automatically be closed by pywin32
267+
# https://mhammond.github.io/pywin32/PySC_HANDLE.html
246268
scm_handle = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ENUMERATE_SERVICE)
247269
except Exception as e: # no cov
248270
raise Exception('Unable to open SCManager: {}'.format(e))
249271

250-
type_filter = win32service.SERVICE_WIN32
251-
state_filter = win32service.SERVICE_STATE_ALL
272+
service_status_process_enums = win32service.EnumServicesStatusEx(
273+
scm_handle,
274+
win32service.SERVICE_WIN32,
275+
win32service.SERVICE_STATE_ALL,
276+
None,
277+
win32service.SC_ENUM_PROCESS_INFO,
278+
)
252279

253-
service_statuses = win32service.EnumServicesStatus(scm_handle, type_filter, state_filter)
280+
process_cache = _build_process_cache()
254281

255282
# Sort service filters in reverse order on the regex pattern so more specific (longer)
256283
# regex patterns are tested first. This is to handle cases when a pattern is a prefix of
@@ -261,8 +288,13 @@ def check(self, instance):
261288
# See test_name_regex_order()
262289
service_filters = sorted(service_filters, reverse=True, key=lambda x: len(x.name or ""))
263290

264-
for short_name, display_name, service_status in service_statuses:
265-
service_view = ServiceView(scm_handle, short_name)
291+
for service_status_process_enum in service_status_process_enums:
292+
service_name = service_status_process_enum["ServiceName"]
293+
display_name = service_status_process_enum["DisplayName"]
294+
state = service_status_process_enum["CurrentState"]
295+
service_pid = service_status_process_enum["ProcessId"]
296+
297+
service_view = ServiceView(scm_handle, service_name)
266298

267299
if 'ALL' not in services:
268300
for service_filter in service_filters:
@@ -276,16 +308,20 @@ def check(self, instance):
276308
except (pywintypes.error, OSError) as e:
277309
self.log.exception("Exception at service match for %s", service_filter)
278310
self.warning(
279-
"Failed to query %s service config for filter %s: %s", short_name, service_filter, str(e)
311+
"Failed to query %s service config for filter %s: %s", service_name, service_filter, str(e)
280312
)
281313
else:
282314
continue
283315

284-
state = service_status[1]
316+
service_uptime = 0
317+
# If service_pid is 0, the service is not running
318+
if service_pid != 0:
319+
service_uptime = _get_process_uptime_from_cache(service_pid, process_cache)
320+
285321
status = self.STATE_TO_STATUS.get(state, self.UNKNOWN)
286-
state_string = self.STATE_TO_STRING.get(state, "unknown")
322+
state_string = self.STATE_TO_STRING.get(state, self.UNKNOWN_LITERAL)
287323

288-
tags = ['windows_service:{}'.format(short_name), 'windows_service_state:{}'.format(state_string)]
324+
tags = ['windows_service:{}'.format(service_name), 'windows_service_state:{}'.format(state_string)]
289325
tags.extend(custom_tags)
290326

291327
if instance.get('collect_display_name_as_tag', False):
@@ -297,22 +333,23 @@ def check(self, instance):
297333
except pywintypes.error as e:
298334
self.log.exception("Exception at windows_service_startup_type tag for %s", service_filter)
299335
self.warning(
300-
"Failed to query %s service config for filter %s: %s", short_name, service_filter, str(e)
336+
"Failed to query %s service config for filter %s: %s", service_name, service_filter, str(e)
301337
)
302338

303339
if not instance.get('disable_legacy_service_tag', False):
304340
self._log_deprecation('service_tag', 'windows_service')
305-
tags.append('service:{}'.format(short_name))
341+
tags.append('service:{}'.format(service_name))
306342

307343
self.service_check(self.SERVICE_CHECK_NAME, status, tags=tags)
308-
self.log.debug('service state for %s %s', short_name, status)
344+
self.log.debug('service state for %s %s', service_name, status)
345+
self.gauge('windows_service.uptime', service_uptime, tags=tags)
309346

310347
if 'ALL' not in services:
311348
for service in services_unseen:
312349
# if a name doesn't match anything (wrong name or no permission to access the service), report UNKNOWN
313350
status = self.UNKNOWN
314351

315-
tags = ['windows_service:{}'.format(service), 'windows_service_state:{}'.format("unknown")]
352+
tags = ['windows_service:{}'.format(service), 'windows_service_state:{}'.format(self.UNKNOWN_LITERAL)]
316353

317354
tags.extend(custom_tags)
318355

windows_service/manifest.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
"events": {
4949
"creates_events": false
5050
},
51+
"metrics": {
52+
"prefix": "windows_service.",
53+
"check": "windows_service.uptime",
54+
"metadata_path": "metadata.csv"
55+
},
5156
"service_checks": {
5257
"metadata_path": "assets/service_checks.json"
5358
},

windows_service/metadata.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric,sample_tags
2+
windows_service.uptime,gauge,,second,,The uptime (in seconds) of the host process of the Windows service,0,windows_service,uptime,,

windows_service/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dynamic = [
3737
[project.optional-dependencies]
3838
deps = [
3939
"pywin32==311; sys_platform == 'win32'",
40+
"psutil==6.0.0",
4041
]
4142

4243
[project.urls]

windows_service/tests/test_bench.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# (C) Datadog, Inc. 2018-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
45
from datadog_checks.windows_service import WindowsService
56

67

0 commit comments

Comments
 (0)