Skip to content

Commit f4c772d

Browse files
pierreln-ddclaude
andcommitted
postgres: fix ERROR-level log spam from explain_parameterized_queries on type mismatch
UndefinedFunction and DatatypeMismatch raised during EXPLAIN EXECUTE (when _explain_prepared_statement runs with untyped NULL params against non-text columns) previously escaped the function body to @tracked_method, which logged at ERROR with no rate limiting and no _explain_errors_cache write — causing hundreds of thousands of ERROR logs per day on ORM-generated queries. Catch these errors before they reach the decorator: log at DEBUG, return None as a sentinel. explain_statement maps None -> DBExplainError.undefined_function so the failure stays observable in collection_errors telemetry, consistent with how the PREPARE phase handles the same error class (PRs #19969/#19998). The April 2025 PREPARE-phase fixes left the EXECUTE phase unpatched; this closes that gap. Confirmed unfixed through current master (HEAD 22faef8). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 22faef8 commit f4c772d

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

postgres/datadog_checks/postgres/explain_parameterized_queries.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def explain_statement(self, dbname, statement, obfuscated_statement, query_signa
9797

9898
try:
9999
result = self._explain_prepared_statement(conn, statement, obfuscated_statement, query_signature)
100-
if result:
100+
if result is None:
101+
return None, DBExplainError.undefined_function, None
102+
elif result:
101103
plan = result[0][0][0]
102104
return plan, DBExplainError.explained_with_prepared_statement, None
103105
else:
@@ -160,6 +162,17 @@ def _explain_prepared_statement(self, conn, statement, obfuscated_statement, que
160162
EXPLAIN_QUERY.format(explain_function=self._explain_function),
161163
(prepared_statement_query,),
162164
)
165+
except (psycopg.errors.UndefinedFunction, psycopg.errors.DatatypeMismatch) as e:
166+
logged_statement = obfuscated_statement
167+
if self._config.log_unobfuscated_plans:
168+
logged_statement = statement
169+
logger.debug(
170+
'Failed to explain parameterized statement(%s)=[%s] due to type mismatch | err=[%s]',
171+
query_signature,
172+
logged_statement,
173+
e,
174+
)
175+
return None
163176
except Exception as e:
164177
logged_statement = obfuscated_statement
165178
if self._config.log_unobfuscated_plans:

postgres/tests/test_explain_parameterized_queries.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,45 @@ def test_create_prepared_statement_exception(integration_check, dbm_instance):
247247
)
248248

249249

250+
@pytest.mark.unit
251+
@requires_over_12
252+
@pytest.mark.parametrize(
253+
"exception_class",
254+
[
255+
psycopg.errors.UndefinedFunction,
256+
psycopg.errors.DatatypeMismatch,
257+
],
258+
)
259+
def test_explain_prepared_statement_type_mismatch_no_error_log(integration_check, dbm_instance, exception_class):
260+
"""Type mismatch errors from EXPLAIN EXECUTE must be handled at DEBUG, not ERROR.
261+
262+
When _explain_prepared_statement encounters UndefinedFunction or DatatypeMismatch (caused by
263+
untyped NULL parameters against non-text columns), it must return None (sentinel) rather than
264+
raise. If it raises, @tracked_method logs at ERROR level for every explain attempt on every
265+
affected query signature with no rate limiting. explain_statement maps None -> undefined_function
266+
so the failure is still observable in collection_errors telemetry.
267+
"""
268+
check = integration_check(dbm_instance)
269+
epq = check.statement_samples._explain_parameterized_queries
270+
271+
with mock.patch.object(epq, '_generate_prepared_statement_query', return_value="EXECUTE dd_test(null)"):
272+
with mock.patch.object(
273+
epq,
274+
'_execute_query_and_fetch_rows',
275+
side_effect=exception_class("operator does not exist: bigint = text"),
276+
):
277+
with mock.patch.object(check.log, 'exception') as mock_exception:
278+
result = epq._explain_prepared_statement(
279+
None,
280+
"SELECT id FROM t WHERE id = $1",
281+
"SELECT id FROM t WHERE id = $1",
282+
"test_sig",
283+
)
284+
285+
assert result is None
286+
mock_exception.assert_not_called()
287+
288+
250289
@pytest.mark.unit
251290
@pytest.mark.parametrize(
252291
"query,statement_is_parameterized_query",

0 commit comments

Comments
 (0)