Skip to content

Commit 1c26e0b

Browse files
sonar-nigel[bot]Sonar Vibe BotSeppli11
authored andcommitted
SONARPY-4048 Rule S8572 "logging.exception()" should be used instead of "logging.error()" in exception handlers (#1082)
Co-authored-by: Sonar Vibe Bot <vibe-bot@sonarsource.com> Co-authored-by: Sebastian Zumbrunn <sebastian.zumbrunn@sonarsource.com> GitOrigin-RevId: 4fc8aefd30f88d7c07e91a68255fecb9732a6ce4
1 parent 8e896fc commit 1c26e0b

7 files changed

Lines changed: 375 additions & 1 deletion

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.SubscriptionContext;
22+
import org.sonar.plugins.python.api.tree.CallExpression;
23+
import org.sonar.plugins.python.api.tree.Name;
24+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
25+
import org.sonar.plugins.python.api.tree.RegularArgument;
26+
import org.sonar.plugins.python.api.tree.Tree;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
28+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
29+
import org.sonar.python.checks.utils.Expressions;
30+
import org.sonar.python.tree.TreeUtils;
31+
32+
@Rule(key = "S8572")
33+
public class LoggingExceptionCheck extends PythonSubscriptionCheck {
34+
35+
private static final String MESSAGE = "Use \"logging.exception()\" or explicitly pass \"exc_info=False\".";
36+
37+
private static final TypeMatcher LOGGING_ERROR_MATCHER = TypeMatchers.any(
38+
TypeMatchers.isType("logging.error"),
39+
TypeMatchers.isType("logging.Logger.error"),
40+
TypeMatchers.isType("logging.LoggerAdapter.error")
41+
);
42+
43+
@Override
44+
public void initialize(Context context) {
45+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, LoggingExceptionCheck::checkCall);
46+
}
47+
48+
private static void checkCall(SubscriptionContext ctx) {
49+
CallExpression callExpr = (CallExpression) ctx.syntaxNode();
50+
if (!(callExpr.callee() instanceof QualifiedExpression qualifiedCallee)) {
51+
return;
52+
}
53+
if (!LOGGING_ERROR_MATCHER.isTrueFor(qualifiedCallee, ctx)) {
54+
return;
55+
}
56+
if (!isDirectlyInsideExceptClause(callExpr)) {
57+
return;
58+
}
59+
RegularArgument excInfoArg = TreeUtils.argumentByKeyword("exc_info", callExpr.arguments());
60+
if (excInfoArg != null && !Expressions.isTruthy(excInfoArg.expression())) {
61+
return;
62+
}
63+
Name errorName = qualifiedCallee.name();
64+
ctx.addIssue(errorName, MESSAGE);
65+
}
66+
67+
private static boolean isDirectlyInsideExceptClause(Tree tree) {
68+
Tree parent = tree.parent();
69+
while (parent != null) {
70+
if (parent.is(Tree.Kind.FUNCDEF, Tree.Kind.LAMBDA)) {
71+
return false;
72+
}
73+
if (parent.is(Tree.Kind.EXCEPT_CLAUSE, Tree.Kind.EXCEPT_GROUP_CLAUSE)) {
74+
return true;
75+
}
76+
parent = parent.parent();
77+
}
78+
return false;
79+
}
80+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ public Stream<Class<?>> getChecks() {
312312
ListIterableFirstElementCheck.class,
313313
LocalVariableAndParameterNameConventionCheck.class,
314314
LoggersConfigurationCheck.class,
315+
LoggingExceptionCheck.class,
315316
LongIntegerWithLowercaseSuffixUsageCheck.class,
316317
LoopExecutingAtMostOnceCheck.class,
317318
LoopOverDictKeyValuesCheck.class,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<p>This rule raises an issue when <code>logging.error()</code> is used within an exception handler (<code>except</code> block) to log exception
2+
information.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>When handling exceptions in Python, it’s important to capture not just the error message, but also the full traceback. The traceback shows the
5+
sequence of function calls that led to the exception, which is essential for understanding what went wrong and where.</p>
6+
<p>The <code>logging</code> module provides two methods that might seem similar:</p>
7+
<ul>
8+
<li><code>logging.error()</code>: Logs an error message at the ERROR level</li>
9+
<li><code>logging.exception()</code>: Logs an error message at the ERROR level AND automatically includes the full exception traceback</li>
10+
</ul>
11+
<p>When you’re inside an exception handler (<code>except</code> block), you’re dealing with an exception that just occurred. In this context,
12+
<code>logging.exception()</code> is the appropriate choice because:</p>
13+
<ul>
14+
<li>It automatically captures the traceback without requiring <code>exc_info=True</code></li>
15+
<li>It clearly signals to other developers that you’re logging an exception</li>
16+
<li>It follows Python’s principle of being explicit about intent</li>
17+
</ul>
18+
<p>Using <code>logging.error()</code> in an exception handler means you’re either:</p>
19+
<ul>
20+
<li>Missing the traceback entirely (if you don’t pass <code>exc_info=True</code>), making debugging much harder</li>
21+
<li>Writing more verbose code (if you do pass <code>exc_info=True</code>) when a simpler, more idiomatic option exists</li>
22+
</ul>
23+
<p>The <code>logging.exception()</code> method is specifically designed for this use case and is the established Python idiom for logging
24+
exceptions.</p>
25+
<p>If omitting the traceback is intentional, explicitly passing <code>exc_info=False</code> (or <code>exc_info=None</code>) makes that intent clear
26+
and is therefore not flagged.</p>
27+
<h3>What is the potential impact?</h3>
28+
<p>Using <code>logging.error()</code> instead of <code>logging.exception()</code> in exception handlers reduces code maintainability and makes
29+
debugging more difficult.</p>
30+
<p>When traceback information is missing from logs, developers must spend additional time reproducing issues to understand their root cause. In
31+
production environments, where issues may be intermittent or difficult to reproduce, missing traceback information can make it nearly impossible to
32+
diagnose problems effectively.</p>
33+
<p>Additionally, using <code>logging.error()</code> with <code>exc_info=True</code> instead of the more idiomatic <code>logging.exception()</code>
34+
makes the code less readable and may confuse developers who are unfamiliar with this pattern.</p>
35+
<h2>How to fix it</h2>
36+
<p>Replace <code>logging.error()</code> with <code>logging.exception()</code> when logging within an exception handler. If you were using
37+
<code>exc_info=True</code>, you can remove it since <code>logging.exception()</code> includes traceback information by default.</p>
38+
<p>If you intentionally do not want to log the traceback, explicitly pass <code>exc_info=False</code> to <code>logging.error()</code> to signal that
39+
intent.</p>
40+
<h3>Code examples</h3>
41+
<h4>Noncompliant code example</h4>
42+
<pre data-diff-id="1" data-diff-type="noncompliant">
43+
import logging
44+
45+
try:
46+
raise ValueError("Invalid value")
47+
except ValueError:
48+
logging.error("Exception occurred", exc_info=True) # Noncompliant
49+
</pre>
50+
<h4>Compliant solution</h4>
51+
<pre data-diff-id="1" data-diff-type="compliant">
52+
import logging
53+
54+
try:
55+
raise ValueError("Invalid value")
56+
except ValueError:
57+
logging.exception("Exception occurred")
58+
</pre>
59+
<p>If logging the traceback is not desired, explicitly opt out with <code>exc_info=False</code>:</p>
60+
<pre>
61+
import logging
62+
63+
try:
64+
raise ValueError("Invalid value")
65+
except ValueError:
66+
logging.error("Exception occurred", exc_info=False)
67+
</pre>
68+
<h2>Resources</h2>
69+
<h3>Documentation</h3>
70+
<ul>
71+
<li>Python logging documentation - logging.exception() - <a href="https://docs.python.org/3/library/logging.html#logging.exception">Official Python
72+
documentation for the logging.exception() method</a></li>
73+
<li>Python logging HOWTO - <a href="https://docs.python.org/3/howto/logging.html">Python’s official guide to using the logging module
74+
effectively</a></li>
75+
</ul>
76+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "\"logging.exception()\" should be used instead of \"logging.error()\" in exception handlers",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5 min"
8+
},
9+
"tags": [
10+
"logging",
11+
"exception"
12+
],
13+
"defaultSeverity": "Major",
14+
"ruleSpecification": "RSPEC-8572",
15+
"sqKey": "S8572",
16+
"scope": "Main",
17+
"quickfix": "unknown",
18+
"code": {
19+
"impacts": {
20+
"MAINTAINABILITY": "HIGH"
21+
},
22+
"attribute": "COMPLETE"
23+
}
24+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@
342342
"S8517",
343343
"S8519",
344344
"S8520",
345-
"S8521"
345+
"S8521",
346+
"S8572"
346347
]
347348
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class LoggingExceptionCheckTest {
23+
24+
private final LoggingExceptionCheck check = new LoggingExceptionCheck();
25+
26+
@Test
27+
void test() {
28+
PythonCheckVerifier.verify("src/test/resources/checks/loggingException.py", check);
29+
}
30+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import logging
2+
import sys
3+
4+
logger = logging.getLogger(__name__)
5+
6+
7+
def noncompliant_bare_except():
8+
try:
9+
risky()
10+
except:
11+
logging.error("Something went wrong") # Noncompliant {{Use "logging.exception()" or explicitly pass "exc_info=False".}}
12+
13+
14+
def noncompliant_typed_except():
15+
try:
16+
raise ValueError("bad value")
17+
except ValueError:
18+
logging.error("Value error occurred") # Noncompliant
19+
20+
21+
def noncompliant_with_exc_info_true():
22+
try:
23+
raise RuntimeError("runtime error")
24+
except RuntimeError:
25+
logging.error("Runtime error occurred", exc_info=True) # Noncompliant
26+
27+
28+
def compliant_with_exc_info_sys():
29+
try:
30+
risky()
31+
except Exception:
32+
logging.error("Exception occurred", exc_info=sys.exc_info())
33+
34+
35+
def compliant_with_exc_info_variable(some_var):
36+
try:
37+
risky()
38+
except Exception:
39+
logging.error("Exception occurred", exc_info=some_var)
40+
41+
42+
def compliant_with_exc_info_function_call():
43+
try:
44+
risky()
45+
except Exception:
46+
logging.error("Exception occurred", exc_info=should_log_traceback())
47+
48+
49+
def noncompliant_named_logger():
50+
try:
51+
risky()
52+
except Exception:
53+
logger.error("Named logger error") # Noncompliant {{Use "logging.exception()" or explicitly pass "exc_info=False".}}
54+
55+
56+
def noncompliant_named_logger_exc_info():
57+
try:
58+
risky()
59+
except Exception:
60+
logger.error("Named logger error", exc_info=True) # Noncompliant
61+
62+
63+
def noncompliant_nested_except():
64+
try:
65+
risky()
66+
except ValueError:
67+
try:
68+
other()
69+
except TypeError:
70+
logging.error("Nested error") # Noncompliant
71+
72+
73+
def noncompliant_except_group():
74+
try:
75+
risky()
76+
except* ValueError:
77+
logging.error("Grouped error") # Noncompliant
78+
79+
80+
class MyService:
81+
def noncompliant_in_method(self):
82+
try:
83+
risky()
84+
except Exception:
85+
logging.error("Error in method") # Noncompliant
86+
87+
88+
def compliant_logging_exception():
89+
try:
90+
risky()
91+
except Exception:
92+
logging.exception("Exception occurred")
93+
94+
95+
def compliant_named_logger_exception():
96+
try:
97+
risky()
98+
except Exception:
99+
logger.exception("Named logger exception")
100+
101+
102+
def compliant_error_outside_except():
103+
logging.error("Not inside an except block")
104+
if some_condition:
105+
logging.error("Error in conditional")
106+
107+
108+
def compliant_other_log_levels_in_except():
109+
try:
110+
risky()
111+
except Exception:
112+
logging.warning("Warning")
113+
logging.debug("Debug info")
114+
logging.critical("Critical failure")
115+
logging.info("Informational message")
116+
117+
118+
def compliant_error_in_finally():
119+
try:
120+
risky()
121+
except Exception:
122+
logging.exception("Caught exception")
123+
finally:
124+
logging.error("Cleanup step")
125+
126+
127+
def compliant_error_in_nested_function():
128+
try:
129+
risky()
130+
except Exception:
131+
def handle():
132+
logging.error("Inside nested function, not directly in except")
133+
handle()
134+
135+
136+
def compliant_error_in_lambda():
137+
try:
138+
risky()
139+
except Exception:
140+
handler = lambda: logging.error("Inside lambda, not directly in except")
141+
handler()
142+
143+
144+
def compliant_exc_info_false():
145+
try:
146+
risky()
147+
except Exception:
148+
logging.error("No traceback wanted", exc_info=False)
149+
150+
151+
def compliant_named_logger_exc_info_false():
152+
try:
153+
risky()
154+
except Exception:
155+
logger.error("No traceback wanted", exc_info=False)
156+
157+
158+
def compliant_exc_info_none():
159+
try:
160+
risky()
161+
except Exception:
162+
logging.error("No traceback wanted", exc_info=None)

0 commit comments

Comments
 (0)