Skip to content

Commit 99a7ce4

Browse files
authored
Allow tag aliases for wmi tag_by and tag_queries parameters (#21792)
* adding alias to tag_queries * Adding tag_by_prefix and test * updating tag_by alias * removing version changes * fixing linter erors * adding changelogs * Updating tag_queries alias * Updating changelogs * fxing lint errors * updating alias parsing * updating alias parsing * move alias parsing to init * fixing lint errors * changes to parse_alias * removing .lower() calls
1 parent c494756 commit 99a7ce4

File tree

8 files changed

+284
-22
lines changed

8 files changed

+284
-22
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[wmi_check] Allow tag aliases for wmi `tag_by` and `tag_queries` parameters

datadog_checks_base/datadog_checks/base/checks/win/wmi/base.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ def _raise_on_invalid_tag_query_result(self, sampler, wmi_obj, tag_query):
110110
)
111111
raise TypeError
112112

113-
def _get_tag_query_tag(self, sampler, wmi_obj, tag_query):
114-
# type: (WMISampler, WMIObject, TagQuery) -> str
113+
def _get_tag_query_tag(self, sampler, wmi_obj, tag_query, alias):
114+
# type: (WMISampler, WMIObject, TagQuery, str) -> str
115115
"""
116116
Design a query based on the given WMIObject to extract a tag.
117117
@@ -133,13 +133,14 @@ def _get_tag_query_tag(self, sampler, wmi_obj, tag_query):
133133

134134
link_value = str(tag_query_sampler[0][target_property]).lower()
135135

136-
tag = "{tag_name}:{tag_value}".format(tag_name=target_property.lower(), tag_value="_".join(link_value.split()))
136+
tag_name = alias.lower()
137+
tag = "{tag_name}:{tag_value}".format(tag_name=tag_name, tag_value="_".join(link_value.split()))
137138

138139
self.log.debug("Extracted `tag_queries` tag: '%s'", tag)
139140
return tag
140141

141-
def _extract_metrics(self, wmi_sampler, tag_by, tag_queries, constant_tags):
142-
# type: (WMISampler, str, List[List[str]], List[str]) -> List[WMIMetric]
142+
def _extract_metrics(self, wmi_sampler, tag_by, tag_queries, constant_tags, tag_by_aliases, tag_queries_aliases):
143+
# type: (WMISampler, str, List[List[str]], List[str], Dict[str, str], List[str]) -> List[WMIMetric]
143144
"""
144145
Extract and tag metrics from the WMISampler.
145146
@@ -170,9 +171,9 @@ def _extract_metrics(self, wmi_sampler, tag_by, tag_queries, constant_tags):
170171
tags = list(constant_tags) if constant_tags else []
171172

172173
# Tag with `tag_queries` parameter
173-
for query in tag_queries:
174+
for index, query in enumerate(tag_queries):
174175
try:
175-
tags.append(self._get_tag_query_tag(wmi_sampler, wmi_obj, query))
176+
tags.append(self._get_tag_query_tag(wmi_sampler, wmi_obj, query, tag_queries_aliases[index]))
176177
except TagQueryUniquenessFailure:
177178
continue
178179

@@ -193,17 +194,17 @@ def _extract_metrics(self, wmi_sampler, tag_by, tag_queries, constant_tags):
193194
continue
194195
# Tag with `tag_by` parameter
195196
for t in tag_by.split(','):
196-
t = t.strip()
197-
if wmi_property == t:
197+
if normalized_wmi_property == t:
198198
tag_value = str(wmi_value).lower()
199199
if tag_queries and tag_value.find("#") > 0:
200200
tag_value = tag_value[: tag_value.find("#")]
201201

202-
tags.append("{name}:{value}".format(name=t, value=tag_value))
202+
alias = tag_by_aliases.get(t)
203+
tags.append("{name}:{value}".format(name=alias, value=tag_value))
203204
continue
204205

205206
# No metric extraction on 'Name' and properties in tag_by
206-
if wmi_property == 'name' or normalized_wmi_property in tag_by.lower():
207+
if normalized_wmi_property == 'name' or normalized_wmi_property in tag_by.lower():
207208
continue
208209

209210
try:
@@ -260,6 +261,7 @@ def _get_instance_key(self, host, namespace, wmi_class, other=None):
260261
def get_running_wmi_sampler(self, properties, filters, **kwargs):
261262
# type: (List[str], List[Dict[str, WMIFilter]], **Any) -> WMISampler
262263
tag_by = kwargs.pop('tag_by', "")
264+
263265
return self._get_running_wmi_sampler(
264266
instance_key=None,
265267
wmi_class=self.wmi_class,
@@ -303,6 +305,49 @@ def _get_wmi_properties(self, instance_key, metrics, tag_queries):
303305

304306
return self._wmi_props
305307

308+
def parse_tag_queries_aliases(self, tag_queries):
309+
# type: (List[TagQuery]) -> None
310+
"""
311+
Validate tag_queries configuration to ensure aliases are provided when 'AS' is used.
312+
return parsed_tag_queries and aliases
313+
"""
314+
aliases = [] # type: list[str]
315+
parsed_tag_queries = [] # type: list[TagQuery]
316+
for tag_query in tag_queries:
317+
if len(tag_query) < 4:
318+
continue
319+
target_property_str = tag_query[3]
320+
property, alias = self.parse_alias(target_property_str)
321+
aliases.append(alias)
322+
tag_query[3] = property
323+
parsed_tag_queries.append(tag_query)
324+
return parsed_tag_queries, aliases
325+
326+
def parse_alias(self, property):
327+
# type: (str) -> Tuple[str, Optional[str]]
328+
"""
329+
Parse an alias from a string.
330+
"""
331+
property = property.strip().lower()
332+
if ' as ' in property:
333+
# Valid format: property AS alias (with spaces around AS)
334+
property_split = property.split(' as ')
335+
property = property_split[0].strip()
336+
alias = property_split[1].strip()
337+
self.log.debug("Parsed alias: {%s} for property: {%s}", alias, property)
338+
if alias == "":
339+
self.log.warning("No alias provided after 'AS' for property: %s. Using property for tag", property)
340+
alias = property
341+
elif ' as' in property:
342+
# Invalid format: AS found but without proper spacing
343+
raise InvalidWMIQuery(
344+
"Invalid alias syntax in property '{}'. "
345+
"Expected format: 'property AS alias' with spaces around 'AS'".format(property)
346+
)
347+
else:
348+
alias = property
349+
return property, alias
350+
306351

307352
def from_time(
308353
year=None, month=None, day=None, hours=None, minutes=None, seconds=None, microseconds=None, timezone=None

wmi_check/assets/configuration/spec.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,24 @@ files:
143143
The `tag_by` parameter lets you tag each metric with a property from the WMI class you're using.
144144
This is only useful when you will have multiple values for your WMI query.
145145
Comma-separated list of property names
146+
Add aliases to the property tags with by appending AS <ALIAS_NAME> to the property name.
147+
For example: Name AS wmi_name will be tagged as wmi_name:value instead of Name:value.
146148
value:
147149
type: string
148150
display_default: null
149-
example: Name,Label
151+
example: Name AS wmi_name,Label
150152
- name: tag_queries
151153
description: |
152154
The `tag_queries` parameter lets you specify a list of queries, to tag metrics with a target class property.
153155
Each item in the list is a set of :
154156
155-
`[<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY>]`
157+
`[<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY> AS <TAG_ALIAS>]`
156158
157159
* `<LINK_SOURCE_PROPERTY>` contains the link value
158160
* `<TARGET_CLASS>` is the class to link to
159161
* `<LINK_TARGET_CLASS_PROPERTY>` is the target class property to link to
160162
* `<TARGET_PROPERTY>` contains the value to tag with
163+
* `<TAG_ALIAS>`is the alias to use for the tag. If not provided, the target property's name will be used.
161164
162165
It translates to a WMI query:
163166
@@ -170,6 +173,6 @@ files:
170173
type: string
171174
compact_example: true
172175
example:
173-
- [<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY>]
176+
- [<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY> AS <TAG_ALIAS>]
174177

175178
- template: instances/default

wmi_check/changelog.d/21792.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow tag aliases for wmi `tag_by` and `tag_queries` parameters

wmi_check/datadog_checks/wmi_check/data/conf.yaml.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,26 +132,29 @@ instances:
132132
## The `tag_by` parameter lets you tag each metric with a property from the WMI class you're using.
133133
## This is only useful when you will have multiple values for your WMI query.
134134
## Comma-separated list of property names
135+
## Add aliases to the property tags with by appending AS <ALIAS_NAME> to the property name.
136+
## For example: Name AS wmi_name will be tagged as wmi_name:value instead of Name:value.
135137
#
136-
# tag_by: Name,Label
138+
# tag_by: Name AS wmi_name,Label
137139

138140
## @param tag_queries - list of lists - optional
139141
## The `tag_queries` parameter lets you specify a list of queries, to tag metrics with a target class property.
140142
## Each item in the list is a set of :
141143
##
142-
## `[<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY>]`
144+
## `[<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY> AS <TAG_ALIAS>]`
143145
##
144146
## * `<LINK_SOURCE_PROPERTY>` contains the link value
145147
## * `<TARGET_CLASS>` is the class to link to
146148
## * `<LINK_TARGET_CLASS_PROPERTY>` is the target class property to link to
147149
## * `<TARGET_PROPERTY>` contains the value to tag with
150+
## * `<TAG_ALIAS>`is the alias to use for the tag. If not provided, the target property's name will be used.
148151
##
149152
## It translates to a WMI query:
150153
##
151154
## SELECT '<TARGET_PROPERTY>' FROM '<TARGET_CLASS>' WHERE '<LINK_TARGET_CLASS_PROPERTY>' = '<LINK_SOURCE_PROPERTY>'
152155
#
153156
# tag_queries:
154-
# - [<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY>]
157+
# - [<LINK_SOURCE_PROPERTY>, <TARGET_CLASS>, <LINK_TARGET_CLASS_PROPERTY>, <TARGET_PROPERTY> AS <TAG_ALIAS>]
155158

156159
## @param tags - list of strings - optional
157160
## A list of tags to attach to every metric and service check emitted by this instance.

wmi_check/datadog_checks/wmi_check/wmi_check.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,23 @@ def __init__(self, name, init_config, instances):
2323
self.tag_by = self.instance.get('tag_by', "") # type: str
2424
self.tag_queries = self.instance.get('tag_queries', []) # type: List[TagQuery]
2525

26+
# Parse the tag_queries and validate the aliases
27+
self.parsed_tag_queries, self.tag_queries_aliases = self.parse_tag_queries_aliases(self.tag_queries)
28+
2629
custom_tags = self.instance.get('tags', []) # type: List[str]
2730
self.constant_tags = self.instance.get('constant_tags', []) # type: List[str]
2831
if self.constant_tags:
2932
self.log.warning("`constant_tags` is being deprecated, please use `tags`")
3033
self.constant_tags.extend(custom_tags)
3134

35+
self.tag_by_properties = ""
36+
self.tag_by_aliases = {} # type: Dict[str, str]
37+
for t in self.tag_by.split(','):
38+
property, alias = self.parse_alias(t)
39+
self.tag_by_properties += property + ","
40+
self.tag_by_aliases[property.lower()] = alias.lower()
41+
self.tag_by_properties = self.tag_by_properties.rstrip(',')
42+
3243
def check(self, _):
3344
# type: (Any) -> None
3445
"""
@@ -37,7 +48,7 @@ def check(self, _):
3748
# Create or retrieve an existing WMISampler
3849
metric_name_and_type_by_property, properties = self.get_wmi_properties()
3950

40-
wmi_sampler = self.get_running_wmi_sampler(properties, self.filters, tag_by=self.tag_by)
51+
wmi_sampler = self.get_running_wmi_sampler(properties, self.filters, tag_by=self.tag_by_properties)
4152

4253
# Sample, extract & submit metrics
4354
try:
@@ -56,8 +67,15 @@ def check(self, _):
5667

5768
def extract_metrics(self, wmi_sampler):
5869
# type: (WMISampler) -> List[WMIMetric]
59-
return self._extract_metrics(wmi_sampler, self.tag_by, self.tag_queries, self.constant_tags)
70+
return self._extract_metrics(
71+
wmi_sampler,
72+
self.tag_by_properties,
73+
self.parsed_tag_queries,
74+
self.constant_tags,
75+
self.tag_by_aliases,
76+
self.tag_queries_aliases,
77+
)
6078

6179
def get_wmi_properties(self):
6280
# type: () -> WMIProperties
63-
return self._get_wmi_properties(None, self.metrics_to_capture, self.tag_queries)
81+
return self._get_wmi_properties(None, self.metrics_to_capture, self.parsed_tag_queries)

wmi_check/tests/conftest.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
# All rights reserved
33
# Licensed under Simplified BSD License (see LICENSE)
44

5+
from unittest.mock import MagicMock
6+
57
import pytest
68
from mock import patch
79

10+
from datadog_checks.base.checks.win.wmi.sampler import CaseInsensitiveDict
811
from datadog_checks.wmi_check import WMICheck
912

1013

@@ -21,7 +24,15 @@ def __init__(self, wmi_objects=None, properties=None, filters=None):
2124
properties = []
2225

2326
self._wmi_objects = []
24-
self._mock_wmi_objects = wmi_objects
27+
# Convert regular dicts to CaseInsensitiveDict to match real WMISampler behavior
28+
# Must iterate through items to ensure keys are properly lowercased
29+
converted_objects = []
30+
for obj in wmi_objects:
31+
case_insensitive_obj = CaseInsensitiveDict()
32+
for key, value in obj.items():
33+
case_insensitive_obj[key] = value
34+
converted_objects.append(case_insensitive_obj)
35+
self._mock_wmi_objects = converted_objects
2536
self.property_names = properties
2637
self._filters = []
2738

@@ -73,3 +84,63 @@ def mock_disk_sampler():
7384

7485
with patch("datadog_checks.wmi_check.WMICheck._get_running_wmi_sampler", return_value=sampler):
7586
yield
87+
88+
89+
@pytest.fixture
90+
def mock_sampler_with_tag_queries():
91+
# Main sampler with IDProcess for tag queries
92+
main_wmi_objects = [
93+
{
94+
"IOReadBytesPerSec": 20455,
95+
"IDProcess": 1234,
96+
"ThreadCount": 4,
97+
"VirtualBytes": 3811,
98+
"PercentProcessorTime": 5,
99+
}
100+
]
101+
main_property_names = ["ThreadCount", "IOReadBytesPerSec", "VirtualBytes", "PercentProcessorTime", "IDProcess"]
102+
main_sampler = MockSampler(main_wmi_objects, main_property_names)
103+
main_sampler.class_name = 'Win32_PerfFormattedData_PerfProc_Process'
104+
105+
# Tag query sampler for process names
106+
tag_wmi_objects = [{'Name': 'chrome.exe'}]
107+
tag_property_names = ['Name']
108+
tag_sampler = MockSampler(tag_wmi_objects, tag_property_names)
109+
tag_sampler.class_name = 'Win32_Process'
110+
tag_sampler.sample() # Populate the mock data
111+
112+
with patch("datadog_checks.wmi_check.WMICheck._get_running_wmi_sampler", return_value=main_sampler):
113+
with patch("datadog_checks.base.checks.win.wmi.base.WMISampler") as mock_wmi_sampler:
114+
# Setup context manager to return tag_sampler for tag queries
115+
mock_wmi_sampler.return_value.__enter__ = MagicMock(return_value=tag_sampler)
116+
mock_wmi_sampler.return_value.__exit__ = MagicMock(return_value=False)
117+
yield
118+
119+
120+
@pytest.fixture
121+
def mock_sampler_with_tag_by_alias():
122+
main_wmi_objects = [
123+
{
124+
"IOReadBytesPerSec": 20455,
125+
"IDProcess": 1234,
126+
"Name": "foo",
127+
"ThreadCount": 4,
128+
"VirtualBytes": 3811,
129+
"PercentProcessorTime": 5,
130+
"Label": "bar",
131+
}
132+
]
133+
main_property_names = [
134+
"ThreadCount",
135+
"IOReadBytesPerSec",
136+
"VirtualBytes",
137+
"PercentProcessorTime",
138+
"IDProcess",
139+
"Name",
140+
"Label",
141+
]
142+
main_sampler = MockSampler(main_wmi_objects, main_property_names)
143+
main_sampler.class_name = 'Win32_PerfFormattedData_PerfProc_Process'
144+
145+
with patch("datadog_checks.wmi_check.WMICheck._get_running_wmi_sampler", return_value=main_sampler):
146+
yield

0 commit comments

Comments
 (0)