Skip to content

Commit 436dc6f

Browse files
ian28223claude
andauthored
ibm_ace: fix KeyError when ACE omits resource identifier name (DataDog#24826)
* ibm_ace: don't crash on malformed resourceIdentifier entries ACE can emit a resourceIdentifier entry with its `name` key replaced by an empty string instead of being omitted. Handle both cases: parse_tags no longer raises on a missing `name`, and the empty-string key is skipped before metric submission. Includes an anonymized fixture derived from a real captured payload, plus a regression test that replays it through collect(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ibm_ace: log a debug line when skipping a malformed entry Per review feedback: log the resource type and value when a resourceIdentifier entry's malformed key is skipped, and assert on it in the regression test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * ibm_ace: assert the debug log via caplog instead of a mock Per review feedback: use pytest's caplog fixture to assert on the actual rendered log line instead of mocking check.log and inspecting call args. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 7e319b4 commit 436dc6f

5 files changed

Lines changed: 146 additions & 1 deletion

File tree

ibm_ace/changelog.d/24826.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a `KeyError` crash when ACE omits the `name` field on a resource identifier.

ibm_ace/datadog_checks/ibm_ace/resources.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ def full_metric_name(self, metric):
2020
return f'{self.name}.{self.normalized_metric_name(metric)}'
2121

2222
def parse_tags(self, global_tags, metric_data):
23-
group = metric_data.pop('name')
23+
# ACE can omit `name` for some resourceIdentifier entries.
24+
group = metric_data.pop('name', None)
25+
if group is None:
26+
return list(global_tags)
27+
2428
return [f'group:{group}', *global_tags]
2529

2630
def submit(self, check, metric, value, tags):

ibm_ace/datadog_checks/ibm_ace/subscription.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ def collect(self):
189189
tags = resource.parse_tags(resource_tags, metric_data)
190190

191191
for metric, value in metric_data.items():
192+
# ACE can emit a malformed entry with an empty key instead of `name`.
193+
if not metric:
194+
self.check.log.debug(
195+
'Skipping resourceIdentifier entry with malformed key for resource %s: '
196+
'value=%r, tags=%s',
197+
resource_data['name'],
198+
value,
199+
tags,
200+
)
201+
continue
202+
192203
resource.submit(self.check, metric, value, tags)
193204

194205

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"ResourceStatistics": {
3+
"brokerLabel": "integration_server",
4+
"brokerUUID": "00000000-0000-0000-0000-000000000000",
5+
"executionGroupName": "ACESERVER",
6+
"executionGroupUUID": "00000000-0000-0000-0000-000000000000",
7+
"ResourceType": [
8+
{
9+
"name": "JDBCConnectionPools",
10+
"resourceIdentifier": [
11+
{
12+
"name": "summary",
13+
"NameOfJDBCProvider": "jdbc_DataSourceA",
14+
"MaxSizeOfPool": 100,
15+
"ActualSizeOfPool": 1,
16+
"CumulativeRequests": 891,
17+
"CumulativeDelayedRequests": 0,
18+
"CumulativeTimedOutRequests": 0,
19+
"MaxDelayInMilliseconds": 0
20+
},
21+
{
22+
"": "summary0",
23+
"NameOfJDBCProvider": "jdbc_DataSourceB",
24+
"MaxSizeOfPool": 100,
25+
"ActualSizeOfPool": 1,
26+
"CumulativeRequests": 891,
27+
"CumulativeDelayedRequests": 0,
28+
"CumulativeTimedOutRequests": 0,
29+
"MaxDelayInMilliseconds": 0
30+
},
31+
{
32+
"": "summary0",
33+
"NameOfJDBCProvider": "jdbc_DataSourceA",
34+
"MaxSizeOfPool": 100,
35+
"ActualSizeOfPool": 1,
36+
"CumulativeRequests": 29,
37+
"CumulativeDelayedRequests": 0,
38+
"CumulativeTimedOutRequests": 0,
39+
"MaxDelayInMilliseconds": 0
40+
}
41+
]
42+
}
43+
]
44+
}
45+
}

ibm_ace/tests/test_unit.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# (C) Datadog, Inc. 2022-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import os
5+
46
from datadog_checks.ibm_ace.check import IbmAceCheck
7+
from datadog_checks.ibm_ace.resources import get_resource
58
from datadog_checks.ibm_ace.subscription import FlowMonitoringSubscription, ResourceStatisticsSubscription
69

10+
from .common import HERE
11+
712

813
def test_flow_monitoring_subscription(instance, global_tags):
914
check = IbmAceCheck('ibm_ace', {}, [instance])
@@ -118,3 +123,82 @@ def test_non_truncation_error_given_connection_broken_returns_critical(instance,
118123
sc_calls = [c for c in check.service_check.call_args_list if c[0][0] == 'mq.subscription']
119124
assert len(sc_calls) == 1
120125
assert sc_calls[0][0][1] == ServiceCheck.CRITICAL
126+
127+
128+
def test_parse_tags_with_name():
129+
resource = get_resource('JDBCConnectionPools')
130+
metric_data = {'name': 'MyDataSource', 'NameOfJDBCProvider': 'Oracle'}
131+
132+
tags = resource.parse_tags(['mq_server:x'], metric_data)
133+
134+
assert tags == ['group:MyDataSource', 'mq_server:x', 'jdbc_provider:Oracle']
135+
136+
137+
def test_parse_tags_without_name():
138+
# ACE can omit `name`; this must not raise.
139+
resource = get_resource('JDBCConnectionPools')
140+
metric_data = {'NameOfJDBCProvider': 'Oracle'}
141+
142+
tags = resource.parse_tags(['mq_server:x'], metric_data)
143+
144+
assert tags == ['mq_server:x', 'jdbc_provider:Oracle']
145+
146+
147+
def test_collect_survives_malformed_resource_identifier(instance, global_tags, caplog):
148+
# ACE 12.0.9 payload where repeated `resourceIdentifier` entries
149+
# have their `name` key replaced with an empty string. Must not crash.
150+
import logging
151+
from unittest.mock import MagicMock, PropertyMock, patch
152+
153+
caplog.set_level(logging.DEBUG)
154+
155+
fixture_path = os.path.join(HERE, 'fixtures', 'resource_statistics_malformed_jdbc.json')
156+
with open(fixture_path, 'rb') as f:
157+
payload = f.read()
158+
159+
mock_config = MagicMock()
160+
mock_config.max_message_length = 65536
161+
162+
check = IbmAceCheck('ibm_ace', {}, [instance])
163+
check.gauge = MagicMock()
164+
check.count = MagicMock()
165+
check.service_check = MagicMock()
166+
167+
sub = ResourceStatisticsSubscription(check, global_tags)
168+
169+
mock_sub = MagicMock()
170+
mock_sub.get.side_effect = [payload]
171+
172+
with (
173+
patch.object(type(check), 'config', new_callable=PropertyMock, return_value=mock_config),
174+
patch.object(type(sub), 'sub', new_callable=PropertyMock, return_value=mock_sub),
175+
patch.object(sub, '_get_elapsed_time', return_value=25),
176+
):
177+
sub.collect() # must not raise
178+
179+
submitted = check.count.call_args_list + check.gauge.call_args_list
180+
submitted_metrics = {c.args[0] for c in submitted}
181+
182+
# `self.check.count`/`.gauge` are mocked directly, so the `ibm_ace.` namespace
183+
# prefix (normally applied inside AgentCheck.count/gauge) isn't present here.
184+
assert 'JDBCConnectionPools.CumulativeRequests' in submitted_metrics
185+
# The malformed entries' empty-string key must never become a metric name.
186+
assert not any(c.args[0].endswith('.') for c in submitted)
187+
188+
# The well-formed entry still gets its `group` tag; the malformed ones don't.
189+
well_formed_call = next(
190+
c for c in submitted if 'jdbc_provider:jdbc_DataSourceA' in c.kwargs['tags'] and c.args[1] == 891
191+
)
192+
assert any(tag.startswith('group:') for tag in well_formed_call.kwargs['tags'])
193+
194+
malformed_calls = [c for c in submitted if 'jdbc_provider:jdbc_DataSourceB' in c.kwargs['tags']]
195+
assert malformed_calls
196+
assert not any(tag.startswith('group:') for c in malformed_calls for tag in c.kwargs['tags'])
197+
198+
# The fixture has two malformed entries; each is logged at debug level when skipped.
199+
skip_lines = [r for r in caplog.records if 'Skipping resourceIdentifier entry with malformed key' in r.message]
200+
assert len(skip_lines) == 2
201+
for record in skip_lines:
202+
assert record.levelname == 'DEBUG'
203+
assert 'JDBCConnectionPools' in record.message
204+
assert "value='summary0'" in record.message

0 commit comments

Comments
 (0)