Skip to content

Fix object name field in rpc query_completion events and activity_id fields for error events #20302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sqlserver/changelog.d/20302.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add object_name to rpc_completed XE events, add activity ID and activity ID Xfer to error events
21 changes: 10 additions & 11 deletions sqlserver/datadog_checks/sqlserver/xe_collection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class XESessionBase(DBMAsyncJob):
"client_app_name",
"username",
"activity_id",
"activity_id_xfer",
]

BASE_SQL_FIELDS = [
Expand Down Expand Up @@ -731,23 +732,21 @@ def _create_rqt_event(self, event, raw_sql_fields, query_details):
"event_fire_timestamp": query_details.get("event_fire_timestamp"),
}

# Only include duration and query_start for non-error events
is_error_event = self.session_name == "datadog_query_errors"
if not is_error_event:
# Only exclude duration and query_start for error_reported events, not attention events
is_error_reported = event.get("event_name") == "error_reported"
if not is_error_reported:
sqlserver_fields.update(
{
"duration_ms": event.get("duration_ms"),
"query_start": query_details.get("query_start"),
}
)
else:
# Include error_number and message for error events
sqlserver_fields.update(
{
"error_number": event.get("error_number"),
"message": event.get("message"),
}
)

# Include error_number and message if they're present in the event
if event.get("error_number") is not None:
sqlserver_fields["error_number"] = event.get("error_number")
if event.get("message"):
sqlserver_fields["message"] = event.get("message")

# Add additional SQL fields to the sqlserver section
# but only if they're not the primary field and not empty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,14 @@ def _process_error_reported_event(self, event, event_data):
# Extract action elements
for action in event.findall('./action'):
action_name = action.get('name')
if action_name:
if not action_name:
continue

if action_name == 'attach_activity_id':
event_data['activity_id'] = extract_value(action)
elif action_name == 'attach_activity_id_xfer':
event_data['activity_id_xfer'] = extract_value(action)
else:
event_data[action_name] = extract_value(action)

return True
Expand Down Expand Up @@ -122,7 +129,11 @@ def _process_attention_event(self, event, event_data):
if not action_name:
continue

if action_name == 'session_id' or action_name == 'request_id':
if action_name == 'attach_activity_id':
event_data['activity_id'] = extract_value(action)
elif action_name == 'attach_activity_id_xfer':
event_data['activity_id_xfer'] = extract_value(action)
elif action_name == 'session_id' or action_name == 'request_id':
# These are numeric values in the actions
value = extract_int_value(action)
if value is not None:
Expand All @@ -137,11 +148,12 @@ def _normalize_event_impl(self, event):
# First use the base normalization with type-specific fields
normalized = self._normalize_event(event)

# For error events, remove query_start and duration_ms fields since they're not applicable
if 'query_start' in normalized:
del normalized['query_start']
if 'duration_ms' in normalized:
del normalized['duration_ms']
# For error_reported events only, remove query_start and duration_ms fields since they're not applicable
if normalized.get('xe_type') == 'error_reported':
if 'query_start' in normalized:
del normalized['query_start']
if 'duration_ms' in normalized:
del normalized['duration_ms']

return normalized

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,12 @@ class QueryCompletionEventsHandler(XESessionBase):
"physical_reads": 0,
"logical_reads": 0,
"writes": 0,
"spills": 0,
"row_count": 0,
"object_id": 0,
"line_number": 0,
}

RPC_SPECIFIC_STRING_FIELDS = [
"result",
"procedure_name",
"object_name",
"data_stream",
"connection_reset_option",
]
Expand Down Expand Up @@ -146,9 +143,10 @@ def _process_action_elements(self, event, event_data):
for action in event.findall('./action'):
action_name = action.get('name')
if action_name:
# Add activity_id support
if action_name == 'attach_activity_id':
event_data['activity_id'] = extract_value(action)
elif action_name == 'attach_activity_id_xfer':
event_data['activity_id_xfer'] = extract_value(action)
else:
event_data[action_name] = extract_value(action)

Expand Down
148 changes: 148 additions & 0 deletions sqlserver/tests/test_xe_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def rpc_completed_expected_values():
'client_hostname': 'EC2AMAZ-ML3E0PH',
'client_app_name': 'SQLAgent - Job Manager',
'username': 'NT AUTHORITY\\NETWORK SERVICE',
'object_name': 'sp_executesql',
}


Expand All @@ -208,6 +209,8 @@ def error_expected_values():
'client_app_name': 'go-mssqldb',
'username': 'shopper_4',
'message': "'REPEAT' is not a recognized built-in function name.",
'activity_id': 'F961B15C-752A-487E-AC4F-F2A9BAB11DB7-1',
'activity_id_xfer': 'AFCCDE6F-EACD-47F3-9B62-CC02D517191B-0',
}


Expand Down Expand Up @@ -253,6 +256,8 @@ def attention_expected_values():
'client_hostname': 'COMP-MX2YQD7P2P',
'client_app_name': 'azdata',
'username': 'datadog',
'activity_id': 'F961B15C-752A-487E-AC4F-F2A9BAB11DB7-1',
'activity_id_xfer': 'AFCCDE6F-EACD-47F3-9B62-CC02D517191B-0',
}


Expand Down Expand Up @@ -621,6 +626,8 @@ def test_process_events_rpc_completed(
assert 'sp_executesql' in event['statement']
assert 'sql_text' in event
assert 'EXECUTE [msdb].[dbo].[sp_agent_log_job_history]' in event['sql_text']
assert 'object_name' in event
assert event['object_name'] == 'sp_executesql'

def test_process_events_error_reported(self, error_events_handler, sample_error_event_xml, error_expected_values):
"""Test processing of error reported events"""
Expand Down Expand Up @@ -821,6 +828,34 @@ def test_normalize_error_event(self, error_events_handler):
assert 'duration_ms' not in normalized
assert 'query_start' not in normalized

def test_normalize_attention_event(self, error_events_handler):
"""Test attention event normalization"""
# Test attention event with fields
event = {
'event_name': 'attention',
'timestamp': '2023-01-01T12:00:00.123Z',
'duration_ms': 328.677,
'session_id': 123,
'request_id': 456,
'database_name': 'TestDB',
'sql_text': 'SELECT * FROM Customers WHERE CustomerId = 123',
}

normalized = error_events_handler._normalize_event_impl(event)

# Verify normalized fields
assert normalized['xe_type'] == 'attention'
assert normalized['event_fire_timestamp'] == '2023-01-01T12:00:00.123Z'
assert normalized['session_id'] == 123
assert normalized['request_id'] == 456
assert normalized['database_name'] == 'TestDB'
assert normalized['sql_text'] == 'SELECT * FROM Customers WHERE CustomerId = 123'

# Verify duration_ms and query_start are preserved for attention events
assert 'duration_ms' in normalized
assert normalized['duration_ms'] == 328.677
assert 'query_start' in normalized # Query start should be calculated from timestamp and duration

@patch('datadog_checks.sqlserver.xe_collection.base.datadog_agent')
def test_create_event_payload(self, mock_agent, query_completion_handler):
"""Test creation of event payload"""
Expand Down Expand Up @@ -899,6 +934,62 @@ def test_create_rqt_event(self, mock_agent, query_completion_handler):
assert rqt_event['sqlserver']['duration_ms'] == 10.0
assert rqt_event['sqlserver']['query_start'] == '2023-01-01T11:59:50.123Z'

@patch('datadog_checks.sqlserver.xe_collection.base.datadog_agent')
def test_create_rqt_event_attention(self, mock_agent, error_events_handler):
"""Test creation of Raw Query Text event for attention event"""
mock_agent.get_version.return_value = '7.30.0'

# Create attention event with SQL fields - from the error_events_handler
event = {
'event_name': 'attention',
'timestamp': '2023-01-01T12:00:00.123Z',
'duration_ms': 328.677,
'session_id': 123,
'database_name': 'TestDB',
'sql_text': 'SELECT * FROM Customers WHERE CustomerId = ?',
'query_signature': 'abc123',
'primary_sql_field': 'sql_text',
'dd_tables': ['Customers'],
'dd_commands': ['SELECT'],
}

# Create raw SQL fields
raw_sql_fields = {
'sql_text': 'SELECT * FROM Customers WHERE CustomerId = 123',
'raw_query_signature': 'def456',
}

# Query details with formatted timestamps
query_details = {
'event_fire_timestamp': '2023-01-01T12:00:00.123Z',
'query_start': '2023-01-01T11:59:59.795Z', # 328.677ms before timestamp
'duration_ms': 328.677,
}

# Create RQT event
rqt_event = error_events_handler._create_rqt_event(event, raw_sql_fields, query_details)

# Validate common payload fields
validate_common_payload_fields(rqt_event, expected_source='datadog_query_errors', expected_type='rqt')

# Verify DB fields
assert rqt_event['db']['instance'] == 'TestDB'
assert rqt_event['db']['query_signature'] == 'abc123'
assert rqt_event['db']['raw_query_signature'] == 'def456'
assert rqt_event['db']['statement'] == 'SELECT * FROM Customers WHERE CustomerId = 123'

# Verify sqlserver fields
assert rqt_event['sqlserver']['session_id'] == 123
assert rqt_event['sqlserver']['xe_type'] == 'attention'
assert rqt_event['sqlserver']['event_fire_timestamp'] == '2023-01-01T12:00:00.123Z'

# Key check: verify that duration_ms and query_start are present for attention events
# even though they come from the error_events_handler
assert 'duration_ms' in rqt_event['sqlserver']
assert rqt_event['sqlserver']['duration_ms'] == 328.677
assert 'query_start' in rqt_event['sqlserver']
assert rqt_event['sqlserver']['query_start'] == '2023-01-01T11:59:59.795Z'

def test_create_rqt_event_disabled(self, mock_check, mock_config):
"""Test RQT event creation when disabled"""
# Disable raw query collection
Expand Down Expand Up @@ -945,6 +1036,63 @@ def test_create_rqt_event_missing_signature(self, query_completion_handler):
# Should return None when missing signature
assert query_completion_handler._create_rqt_event(event, raw_sql_fields, query_details) is None

@patch('datadog_checks.sqlserver.xe_collection.base.datadog_agent')
def test_create_rqt_event_error_reported(self, mock_agent, error_events_handler):
"""Test creation of Raw Query Text event for error_reported event"""
mock_agent.get_version.return_value = '7.30.0'

# Create error_reported event with SQL fields
event = {
'event_name': 'error_reported',
'timestamp': '2023-01-01T12:00:00.123Z',
'error_number': 8134,
'severity': 15,
'session_id': 123,
'database_name': 'TestDB',
'sql_text': 'SELECT 1/0',
'message': 'Division by zero error',
'query_signature': 'abc123',
'primary_sql_field': 'sql_text',
}

# Create raw SQL fields
raw_sql_fields = {
'sql_text': 'SELECT 1/0',
'raw_query_signature': 'def456',
}

# Query details would not have duration_ms or query_start for error_reported events
query_details = {
'event_fire_timestamp': '2023-01-01T12:00:00.123Z',
}

# Create RQT event
rqt_event = error_events_handler._create_rqt_event(event, raw_sql_fields, query_details)

# Validate common payload fields
validate_common_payload_fields(rqt_event, expected_source='datadog_query_errors', expected_type='rqt')

# Verify DB fields
assert rqt_event['db']['instance'] == 'TestDB'
assert rqt_event['db']['query_signature'] == 'abc123'
assert rqt_event['db']['raw_query_signature'] == 'def456'
assert rqt_event['db']['statement'] == 'SELECT 1/0'

# Verify sqlserver fields
assert rqt_event['sqlserver']['session_id'] == 123
assert rqt_event['sqlserver']['xe_type'] == 'error_reported'
assert rqt_event['sqlserver']['event_fire_timestamp'] == '2023-01-01T12:00:00.123Z'

# Key check: verify that error_number and message are included for error_reported events
assert 'error_number' in rqt_event['sqlserver']
assert rqt_event['sqlserver']['error_number'] == 8134
assert 'message' in rqt_event['sqlserver']
assert rqt_event['sqlserver']['message'] == 'Division by zero error'

# Verify that duration_ms and query_start are NOT present for error_reported events
assert 'duration_ms' not in rqt_event['sqlserver']
assert 'query_start' not in rqt_event['sqlserver']


@pytest.mark.integration
@pytest.mark.usefixtures('dd_environment')
Expand Down
12 changes: 12 additions & 0 deletions sqlserver/tests/xml_xe_events/attention.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,16 @@
ORDER BY event_timestamp;
</value>
</action>
<action name="attach_activity_id_xfer" package="package0">
<type name="activity_id_xfer" package="package0" />
<value>
AFCCDE6F-EACD-47F3-9B62-CC02D517191B-0
</value>
</action>
<action name="attach_activity_id" package="package0">
<type name="activity_id" package="package0" />
<value>
F961B15C-752A-487E-AC4F-F2A9BAB11DB7-1
</value>
</action>
</event>
12 changes: 12 additions & 0 deletions sqlserver/tests/xml_xe_events/error_reported.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,16 @@
/*dddbs='orders-app',ddps='orders-app',ddh='awbergs-sqlserver2019-test.c7ug0vvtkhqv.us-east-1.rds.amazonaws.com',dddb='dbmorders',ddprs='orders-sqlserver'*/ SELECT discount_percent, store_name, description, discount_in_currency, dbm_item_id, REPEAT('a', 1000) from discount where id BETWEEN 6117 AND 6127 GROUP by dbm_item_id, store_name, description, discount_in_currency, discount_percent /* date='12%2F31',key='val' */
</value>
</action>
<action name="attach_activity_id_xfer" package="package0">
<type name="activity_id_xfer" package="package0" />
<value>
AFCCDE6F-EACD-47F3-9B62-CC02D517191B-0
</value>
</action>
<action name="attach_activity_id" package="package0">
<type name="activity_id" package="package0" />
<value>
F961B15C-752A-487E-AC4F-F2A9BAB11DB7-1
</value>
</action>
</event>
Loading