Skip to content
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

Add option for sampling idle recently active sessions #19479

Merged
merged 11 commits into from
Jan 24, 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
8 changes: 8 additions & 0 deletions sqlserver/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@ files:
value:
type: number
example: 10
- name: sample_recently_active_idle_sessions
description: |
This option enables the sampling of idle sessions that were active since the last collection.
Recommended for APM customers to boost the APM/DBM correlation.
value:
type: boolean
example: true

- name: stored_procedure_characters_limit
description: |
Limit the number of characters of the text of a stored procedure that is collected.
Expand Down
1 change: 1 addition & 0 deletions sqlserver/changelog.d/19479.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option for sampling idle recently active sessions. If yuo're an APM customer, set ``sample_recently_active_idle_sessions`` to ``true`` to boost APM/DBM correlations.
64 changes: 48 additions & 16 deletions sqlserver/datadog_checks/sqlserver/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@
).strip()

# Turns out sys.dm_exec_requests does not contain idle sessions.
# Inner joining dm_exec_sessions with dm_exec_requests will not return any idle blocking sessions.
# This prevent us reusing the same ACTIVITY_QUERY for regular activities and idle blocking sessions.
# Inner joining dm_exec_sessions with dm_exec_requests will not return any idle sessions.
# This prevent us reusing the same ACTIVITY_QUERY for regular activities and idle sessions.
# The query below is used for idle sessions and does not join with dm_exec_requests.
# The last query execution on the connection is fetched from dm_exec_connections.most_recent_sql_handle.
IDLE_BLOCKING_SESSIONS_QUERY = re.sub(
IDLE_SESSIONS_QUERY = re.sub(
r'\s+',
' ',
"""\
Expand All @@ -107,17 +107,25 @@
c.client_net_address as client_address,
sess.host_name as host_name,
sess.program_name as program_name,
sess.is_user_process as is_user_process
sess.is_user_process as is_user_process,
sess.context_info as context_info
FROM sys.dm_exec_sessions sess
INNER JOIN sys.dm_exec_connections c
ON sess.session_id = c.session_id
CROSS APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) lqt
WHERE sess.status = 'sleeping'
AND sess.session_id IN ({blocking_session_ids})
AND c.session_id IN ({blocking_session_ids})
WHERE
""",
).strip()

IDLE_BLOCKERS_FILTER = """(sess.status = 'sleeping'
AND sess.session_id IN ({blocking_session_ids})
AND c.session_id IN ({blocking_session_ids}))"""

IDLE_RECENTLY_ACTIVE_FILTER = (
"(sess.status = 'sleeping' AND sess.last_request_start_time > DATEADD(SECOND, - ?, GETDATE()))"
)


# enumeration of the columns we collect
# from sys.dm_exec_requests
DM_EXEC_REQUESTS_COLS = [
Expand Down Expand Up @@ -182,6 +190,10 @@ def __init__(self, check, config: SQLServerConfig):
self._conn_key_prefix = "dbm-activity-"
self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES
self._exec_requests_cols_cached = None
self._sample_recently_active_idle_sessions = is_affirmative(
self._config.activity_config.get('sample_recently_active_idle_sessions', False)
)
self._time_since_last_activity_event = 0

def _close_db_conn(self):
pass
Expand All @@ -200,15 +212,34 @@ def _get_active_connections(self, cursor):
self.log.debug("loaded sql server current connections len(rows)=%s", len(rows))
return rows

def _is_sample_idle_recently_active_sessions(self) -> bool:
return self._sample_recently_active_idle_sessions and self._time_since_last_activity_event

@tracked_method(agent_check_getter=agent_check_getter, track_result_length=True)
def _get_idle_blocking_sessions(self, cursor, blocking_session_ids):
# The IDLE_BLOCKING_SESSIONS_QUERY contains minimum information on idle blocker
query = IDLE_BLOCKING_SESSIONS_QUERY.format(
blocking_session_ids=",".join(map(str, blocking_session_ids)),
def _get_idle_sessions(self, cursor, blocking_session_ids):
query = IDLE_SESSIONS_QUERY.format(
proc_char_limit=self._config.stored_procedure_characters_limit,
)
idle_filter = None

# a stateful closure that appends filters to the query
def _append_filter(filter: str) -> str:
if idle_filter:
return f"{idle_filter} OR {filter}"
return filter

if blocking_session_ids:
idle_filter = _append_filter(
IDLE_BLOCKERS_FILTER.format(blocking_session_ids=",".join(map(str, blocking_session_ids)))
)
if self._is_sample_idle_recently_active_sessions():
idle_filter = _append_filter(IDLE_RECENTLY_ACTIVE_FILTER)
query += idle_filter
self.log.debug("Running query [%s]", query)
cursor.execute(query)
if self._is_sample_idle_recently_active_sessions():
cursor.execute(query, (int(time.time() - self._time_since_last_activity_event),))
else:
cursor.execute(query)
columns = [i[0] for i in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
return rows
Expand All @@ -232,9 +263,9 @@ def _get_activity(self, cursor, exec_request_columns):
blocking_session_ids = {r['blocking_session_id'] for r in rows if r['blocking_session_id']}
# if there are blocking sessions and some of the session(s) are not captured in the activity query
idle_blocking_session_ids = blocking_session_ids - session_ids
if idle_blocking_session_ids:
idle_blocking_sessions = self._get_idle_blocking_sessions(cursor, idle_blocking_session_ids)
rows.extend(idle_blocking_sessions)
if idle_blocking_session_ids or self._is_sample_idle_recently_active_sessions():
idle_sessions = self._get_idle_sessions(cursor, idle_blocking_session_ids)
rows.extend(idle_sessions)
return rows

def _normalize_queries_and_filter_rows(self, rows, max_bytes_limit):
Expand Down Expand Up @@ -356,6 +387,7 @@ def _get_estimated_row_size_bytes(row):
return len(str(row))

def _create_activity_event(self, active_sessions, active_connections):
self._time_since_last_activity_event = time.time()
event = {
"host": self._check.resolved_hostname,
"ddagentversion": datadog_agent.get_version(),
Expand All @@ -376,7 +408,7 @@ def _create_activity_event(self, active_sessions, active_connections):
@tracked_method(agent_check_getter=agent_check_getter)
def collect_activity(self):
"""
Collects all current activity for the SQLServer intance.
Collects all current activity for the SQLServer instance.
:return:
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class QueryActivity(BaseModel):
)
collection_interval: Optional[float] = None
enabled: Optional[bool] = None
sample_recently_active_idle_sessions: Optional[bool] = None


class QueryMetrics(BaseModel):
Expand Down
6 changes: 6 additions & 0 deletions sqlserver/datadog_checks/sqlserver/data/conf.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ instances:
#
# collection_interval: 10

## @param sample_recently_active_idle_sessions - boolean - optional - default: true
## This option enables the sampling of idle sessions that were active since the last collection.
## Recommended for APM customers to boost the APM/DBM correlation.
#
# sample_recently_active_idle_sessions: true

## @param stored_procedure_characters_limit - integer - optional - default: 500
## Limit the number of characters of the text of a stored procedure that is collected.
## The characters limit is applicable to both query metrics and query samples.
Expand Down
11 changes: 11 additions & 0 deletions sqlserver/tests/test_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ def dbm_instance(instance_docker):
return copy(instance_docker)


@pytest.mark.usefixtures('dd_environment')
def test_idle_sessions_sampling(dbm_instance, dd_run_check):
check = SQLServer(CHECK_NAME, {}, [dbm_instance])
assert (
not check.activity._sample_recently_active_idle_sessions
), "Sample recently active idle sessions switched off by default"
check.activity._sample_recently_active_idle_sessions = True
check.activity._time_since_last_activity_event = 10
dd_run_check(check)


@pytest.mark.flaky
@pytest.mark.integration
@pytest.mark.usefixtures('dd_environment')
Expand Down
Loading