This rule detects inefficient logging format interpolation in Python code. It encourages using the native Python logging format (%s with kwargs) instead of built-in string formatters (.format() or f-strings).
Bad Practice:
logging.info("Hello {}".format(name))
logging.info(f"Hello {name}")
logger.warn("Warning {}".format(msg))
loguru_logger.success(f"Done: {task}")Good Practice:
logging.info("Hello %s", name)
logger.warn("Warning %s", msg)
loguru_logger.success("Done: %s", task)Using .format() or f-strings with logging methods causes unnecessary string interpolation even when the log level is not active. The native logging format (%s) defers string formatting until it's actually needed, improving performance and reducing resource consumption.
Performance Impact:
- String interpolation occurs immediately, consuming CPU cycles
- If the log level is disabled, the formatted string is never used (wasted resources)
- With
%sformat, interpolation is deferred until the log is actually written
Standard methods:
debug(),info(),warning(),error(),critical(),exception(),log()
Aliases (legacy/compatibility):
warn()- Deprecated alias forwarning()(still widely used in legacy code)fatal()- Alias forcritical()(used in some codebases)
Usage patterns detected:
import logging
from logging import getLogger, Logger
# Direct module usage
logging.info("Hello {}".format(name)) # Detected ✓
# Via getLogger
logger = logging.getLogger(__name__)
logger.info("Hello {}".format(name)) # Detected ✓
# Via imported getLogger
log = getLogger(__name__)
log.info("Hello {}".format(name)) # Detected ✓
# Via Logger class
LOGGER = Logger(__name__)
LOGGER.info("Hello {}".format(name)) # Detected ✓Standard methods:
debug(),info(),warning(),error(),critical()
Loguru-specific methods:
trace()- Lowest level (below debug)success()- Custom level between info and warning
Usage patterns detected:
from loguru import logger
# Direct usage
logger.info("Hello {}".format(name)) # Detected ✓
# With alias
from loguru import logger as log
log.success(f"Done: {task}") # Detected ✓This rule implementation uses the Visitor Pattern, a behavioral design pattern that allows adding new operations to existing object structures without modifying them.
Key concepts in our implementation:
- Elements: Python AST nodes (ImportFrom, AssignmentStatement, CallExpression, etc.)
- Visitors:
LoggingImportVisitorandLoggerAssignmentVisitorthat traverse the AST - Accept method: Each AST node accepts a visitor via
node.accept(visitor) - Visit methods: Visitors implement specific logic for each node type
Why Visitor Pattern for this rule?
- ✅ Separation of concerns: Detection logic is separated from AST structure
- ✅ Extensibility: Easy to add new detection patterns without modifying AST classes
- ✅ Reusability: Visitors can be reused across different files
- ✅ Double-dispatch: Method selection based on both visitor type and node type
Pattern flow in our implementation:
FileInput (root)
↓
fileInput.accept(LoggingImportVisitor)
→ Visitor traverses import nodes
→ Collects logging library imports
↓
fileInput.accept(LoggerAssignmentVisitor)
→ Visitor traverses assignment nodes
→ Tracks logger variable names
↓
Result: Context collected for analysis phase
The rule implementation uses a two-phase approach following the SonarQube Python API:
- FILE_INPUT Phase: Scans the entire file once to collect context
- CALL_EXPR Phase: Checks each method call for bad patterns
┌─────────────────────────────────────────────────┐
│ FILE_INPUT Phase │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ LoggingImportVisitor │ │
│ │ • Detect: import logging, import loguru │ │
│ │ • Detect: from logging import X │ │
│ │ • Track: imported logger names │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ LoggerAssignmentVisitor │ │
│ │ • Track: logger = logging.getLogger() │ │
│ │ • Track: log = getLogger(__name__) │ │
│ │ • Track: LOGGER = Logger(__name__) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Result: isUsingLoggingLib flag │
│ loggerVariableNames set │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ CALL_EXPR Phase (for each method call) │
│ │
│ 1. Check if method name in LOGGING_METHODS │
│ 2. Check if qualifier is logging module/logger │
│ 3. Extract message argument (special case: log)│
│ 4. Check for bad patterns: │
│ • .format() call │
│ • f-string (f"", F"", f'', F'') │
│ 5. Raise issue if bad pattern detected │
└─────────────────────────────────────────────────┘
@Rule(key = "GCI111")
public class DetectBadLoggingFormatInterpolation extends PythonSubscriptionCheck {
// Constants
protected static final String MESSAGE_RULE = "For logging format, prefer using %s with kwargs instead of builtin formatter \"\".format() or f\"\"";
private static final Set<String> LOGGING_METHODS = new HashSet<>(Arrays.asList(
"debug", "info", "warning", "warn", "error", "critical", "fatal", "exception", "log",
"trace", "success" // loguru specific methods
));
private static final Set<String> LOGGING_MODULE_NAMES = new HashSet<>(Arrays.asList(
"logging", "loguru"
));
// State variables (reset for each file)
private boolean isUsingLoggingLib = false;
private final Set<String> loggerVariableNames = new HashSet<>();
// Entry point: registers the two phases
@Override
public void initialize(Context context) {
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::visitFile);
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkCallExpression);
}
}Key components:
- MESSAGE_RULE: The exact message shown to developers
- LOGGING_METHODS: Set of all supported logging methods (11 methods total)
- LOGGING_MODULE_NAMES: Supported frameworks (logging, loguru)
- State variables: Track context per file (critical for multi-file analysis)
Complete implementation:
/**
* Visitor to detect logging imports and track directly imported loggers
*/
private static class LoggingImportVisitor extends BaseTreeVisitor {
private boolean isLoggingImported = false;
private final Set<String> importedLoggerNames = new HashSet<>();
@Override
public void visitImportFrom(ImportFrom importFrom) {
// Check if importing from logging/loguru
List<Name> names = importFrom.module() != null ? importFrom.module().names() : null;
if (names != null && !names.isEmpty() && LOGGING_MODULE_NAMES.contains(names.get(0).name())) {
isLoggingImported = true;
// Track directly imported logger names
for (AliasedName importedItem : importFrom.importedNames()) {
if (importedItem.alias() != null) {
// from loguru import logger as log -> "log"
importedLoggerNames.add(importedItem.alias().name());
} else {
// from loguru import logger -> "logger"
List<Name> itemNames = importedItem.dottedName().names();
if (!itemNames.isEmpty()) {
importedLoggerNames.add(itemNames.get(itemNames.size() - 1).name());
}
}
}
}
super.visitImportFrom(importFrom);
}
@Override
public void visitImportName(ImportName importName) {
// Detect: import logging, import loguru
for (AliasedName aliasedName : importName.modules()) {
List<Name> names = aliasedName.dottedName().names();
if (!names.isEmpty() && LOGGING_MODULE_NAMES.contains(names.get(0).name())) {
isLoggingImported = true;
}
}
super.visitImportName(importName);
}
}Visitor Pattern in action:
- BaseTreeVisitor: Base class providing default implementations
- visitImportFrom(): Called when encountering
from X import Y - visitImportName(): Called when encountering
import X - super.visit...(): Ensures child nodes are also visited
Detection capabilities:
# All these are detected:
import logging # ✓ visitImportName
from logging import getLogger # ✓ visitImportFrom
from logging import Logger # ✓ visitImportFrom
import loguru # ✓ visitImportName
from loguru import logger # ✓ visitImportFrom + tracks "logger"
from loguru import logger as log # ✓ visitImportFrom + tracks "log"Why track imported logger names?
- Loguru typically uses
from loguru import logger(direct import) - Standard logging uses
import loggingorfrom logging import getLogger - We need to track both patterns to detect all cases
- Without tracking, we'd miss:
logger.info("{}".format(x))when logger is from loguru
Complete implementation:
/**
* Visitor to detect logger variable assignments
* Examples: logger = logging.getLogger(), log = getLogger(), LOGGER = Logger()
*/
private static class LoggerAssignmentVisitor extends BaseTreeVisitor {
private final Set<String> loggerVariables = new HashSet<>();
@Override
public void visitAssignmentStatement(AssignmentStatement assignment) {
// Check if right side is a logging-related call
for (ExpressionList expressionList : assignment.lhsExpressions()) {
for (Expression expr : expressionList.expressions()) {
if (expr.is(Tree.Kind.NAME)) {
Name name = (Name) expr;
if (isLoggingRelatedAssignment(assignment.assignedValue())) {
loggerVariables.add(name.name());
}
}
}
}
super.visitAssignmentStatement(assignment);
}
private boolean isLoggingRelatedAssignment(Expression expression) {
if (expression.is(Tree.Kind.CALL_EXPR)) {
CallExpression callExpr = (CallExpression) expression;
Expression callee = callExpr.callee();
// Check for logging.getLogger(), getLogger(), or Logger()
if (callee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualExpr = (QualifiedExpression) callee;
String methodName = qualExpr.name().name();
return "getLogger".equals(methodName) || "Logger".equals(methodName);
} else if (callee.is(Tree.Kind.NAME)) {
Name name = (Name) callee;
return "getLogger".equals(name.name()) || "Logger".equals(name.name());
}
}
return false;
}
}Visitor Pattern in action:
- visitAssignmentStatement(): Called for each assignment in the AST
- Pattern matching: Checks if right-hand side is
getLogger()orLogger() - Variable tracking: Stores the left-hand side variable name
Detection capabilities:
# All these variable names are tracked:
logger = logging.getLogger(__name__) # ✓ Tracks "logger"
log = getLogger(__name__) # ✓ Tracks "log"
LOGGER = Logger(__name__) # ✓ Tracks "LOGGER"
my_custom_logger = logging.getLogger() # ✓ Tracks "my_custom_logger"
# Later usage is then detected:
logger.info("{}".format(x)) # ✓ Detected (logger is tracked)
log.debug(f"Value: {x}") # ✓ Detected (log is tracked)Why this approach?
- Logger objects can have any variable name (not just "logger")
- We detect the assignment pattern:
variable = getLogger()orvariable = Logger() - Variable names are stored in
loggerVariableNamesset - This allows detection of method calls on these variables later
Complete implementation:
/**
* Phase 1: Scan file to detect logging imports and logger variable assignments
* This method is called once per file and resets all state
*/
private void visitFile(SubscriptionContext ctx) {
// CRITICAL: Reset state for each file
isUsingLoggingLib = false;
loggerVariableNames.clear();
FileInput fileInput = (FileInput) ctx.syntaxNode();
// Step 1: Check imports using Visitor pattern
LoggingImportVisitor importVisitor = new LoggingImportVisitor();
fileInput.accept(importVisitor); // Traverse entire AST for imports
isUsingLoggingLib = importVisitor.isLoggingImported;
// Step 2: Add directly imported logger variables (e.g., from loguru import logger)
loggerVariableNames.addAll(importVisitor.importedLoggerNames);
// Step 3: Check logger assignments using Visitor pattern (only if logging is used)
if (isUsingLoggingLib) {
LoggerAssignmentVisitor assignmentVisitor = new LoggerAssignmentVisitor();
fileInput.accept(assignmentVisitor); // Traverse entire AST for assignments
loggerVariableNames.addAll(assignmentVisitor.loggerVariables);
}
}Visitor Pattern flow:
visitFile() called
↓
1. Reset state (clear all tracking)
↓
2. Create LoggingImportVisitor
↓
3. fileInput.accept(importVisitor)
→ Visitor walks the AST
→ Calls visitImportFrom() for each import
→ Collects: isLoggingImported, importedLoggerNames
↓
4. Create LoggerAssignmentVisitor (if logging detected)
↓
5. fileInput.accept(assignmentVisitor)
→ Visitor walks the AST
→ Calls visitAssignmentStatement() for each assignment
→ Collects: loggerVariables
↓
6. Merge results into loggerVariableNames
Why state reset is CRITICAL:
- In test environments, multiple files are analyzed sequentially
- Without reset, state from File A would contaminate File B
- Example without reset:
File A: import loguru → isUsingLoggingLib = true File B: no imports → isUsingLoggingLib still true (BUG!) - With reset: Each file starts with clean state
Performance optimization:
- Skip
LoggerAssignmentVisitorif no logging library detected - Avoids unnecessary AST traversal when file doesn't use logging
Complete implementation:
/**
* Phase 2: Check if a call expression is a logging method with bad format interpolation
* This method is called for EVERY function/method call in the code
*/
private void checkCallExpression(SubscriptionContext ctx) {
// Quick exit: skip if file doesn't use logging
if (!isUsingLoggingLib) {
return;
}
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
Expression callee = callExpression.callee();
// Check if this is a method call (not a simple function call)
if (callee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualifiedExpression = (QualifiedExpression) callee;
String methodName = qualifiedExpression.name().name();
// Check if method name is a logging method
if (LOGGING_METHODS.contains(methodName)) {
Expression qualifier = qualifiedExpression.qualifier();
// Check if qualifier is "logging" module or a known logger variable
if (isLoggingQualifier(qualifier)) {
checkLoggingArguments(ctx, callExpression);
}
}
}
}
/**
* Check if the qualifier is a logging module or logger variable
*/
private boolean isLoggingQualifier(Expression qualifier) {
if (qualifier.is(Tree.Kind.NAME)) {
String qualifierName = ((Name) qualifier).name();
// Check if it's the logging module or a known logger variable
return LOGGING_MODULE_NAMES.contains(qualifierName) ||
loggerVariableNames.contains(qualifierName);
}
return false;
}Detection logic breakdown:
- Quick exit: Return immediately if file doesn't use logging
- Type check: Verify it's a qualified expression (e.g.,
object.method()) - Method name check: Verify method is in
LOGGING_METHODSset - Qualifier check: Verify qualifier is logging module or tracked variable
Examples:
# logging.info(...)
# → callee = QUALIFIED_EXPR
# → methodName = "info" ✓ (in LOGGING_METHODS)
# → qualifier = "logging" ✓ (in LOGGING_MODULE_NAMES)
# → checkLoggingArguments() called
# logger.debug(...)
# → callee = QUALIFIED_EXPR
# → methodName = "debug" ✓ (in LOGGING_METHODS)
# → qualifier = "logger" ✓ (in loggerVariableNames)
# → checkLoggingArguments() called
# print("hello")
# → callee = NAME (not QUALIFIED_EXPR)
# → skipped (not a method call)
# obj.save()
# → methodName = "save" ✗ (not in LOGGING_METHODS)
# → skippedThe log() method has a different signature than other logging methods:
# Other methods: message is first argument
logging.info("message", arg1, arg2)
# log() method: level is first, message is second
logging.log(logging.INFO, "message", arg1, arg2)Complete implementation:
/**
* Check the arguments of a logging call for bad format interpolation
*/
private void checkLoggingArguments(SubscriptionContext ctx, CallExpression callExpression) {
List<Argument> arguments = callExpression.arguments();
if (arguments.isEmpty()) {
return;
}
// For log(), message is the second argument (index 1)
// For other methods, message is the first argument (index 0)
int messageArgIndex = 0;
Expression callee = callExpression.callee();
if (callee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualExpr = (QualifiedExpression) callee;
if ("log".equals(qualExpr.name().name())) {
messageArgIndex = 1; // Special case: log() has level first
if (arguments.size() < 2) {
return; // Not enough arguments
}
}
}
// Get the message argument
Argument messageArg = arguments.get(messageArgIndex);
if (messageArg.is(Tree.Kind.REGULAR_ARGUMENT)) {
RegularArgument regularArg = (RegularArgument) messageArg;
Expression expression = regularArg.expression();
// Check for f-strings
if (isFString(expression)) {
ctx.addIssue(callExpression, MESSAGE_RULE);
return;
}
// Check for .format() calls
if (expression.is(Tree.Kind.CALL_EXPR)) {
CallExpression innerCall = (CallExpression) expression;
Expression innerCallee = innerCall.callee();
if (innerCallee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualExpr = (QualifiedExpression) innerCallee;
if ("format".equals(qualExpr.name().name())) {
ctx.addIssue(callExpression, MESSAGE_RULE);
}
}
}
}
}Argument extraction logic:
# Standard methods (index 0)
logging.info("message", x, y)
↑ messageArgIndex = 0
# log() method (index 1)
logging.log(logging.INFO, "message", x, y)
↑ level ↑ messageArgIndex = 1Complete implementation:
/**
* Check if an expression is an f-string
* Detects: f"...", f'...', F"...", F'...'
*/
private boolean isFString(Expression expression) {
if (expression.is(Tree.Kind.STRING_LITERAL)) {
StringLiteral stringLiteral = (StringLiteral) expression;
String value = stringLiteral.firstToken().value();
// Check if string starts with 'f' or 'F' followed by quote
return value != null &&
(value.startsWith("f\"") || value.startsWith("f'") ||
value.startsWith("F\"") || value.startsWith("F'"));
}
return false;
}Detection examples:
logger.info(f"Hello {name}") # ✓ value = 'f"Hello {name}"' → startsWith("f\"")
logger.info(f'Hello {name}') # ✓ value = "f'Hello {name}'" → startsWith("f'")
logger.info(F"Hello {name}") # ✓ value = 'F"Hello {name}"' → startsWith("F\"")
logger.info(F'Hello {name}') # ✓ value = "F'Hello {name}'" → startsWith("F'")
logger.info("Hello %s", name) # ✗ value = '"Hello %s"' → doesn't start with f/FWhy check first token value?
- AST preserves the original string prefix (f/F)
- Direct inspection of token is most reliable
- Alternative (regex) would be fragile and error-prone
Complete implementation:
// Check for .format() calls
if (expression.is(Tree.Kind.CALL_EXPR)) {
CallExpression innerCall = (CallExpression) expression;
Expression innerCallee = innerCall.callee();
if (innerCallee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualExpr = (QualifiedExpression) innerCallee;
if ("format".equals(qualExpr.name().name())) {
ctx.addIssue(callExpression, MESSAGE_RULE);
}
}
}AST structure for .format():
logging.info("Hello {}".format(name))
AST structure:
CallExpression (logging.info)
└─ Argument: CallExpression ("Hello {}".format)
└─ Callee: QualifiedExpression
├─ Qualifier: StringLiteral "Hello {}"
└─ Name: "format" ← We check thisDetects:
"Hello {}".format(name)✓"Hello {0}".format(name)✓"Hello {key}".format(key=name)✓template.format(**kwargs)✓
Why AST analysis instead of regex?
- ✅ Regex patterns are fragile and prone to false positives/negatives
- ✅ AST-based analysis provides accurate, context-aware detection
- ✅ Handles complex nested expressions correctly
- ✅ No need to worry about string escaping, quotes, etc.
Pattern used throughout the code:
// Always check type before casting
if (callee.is(Tree.Kind.QUALIFIED_EXPR)) {
QualifiedExpression qualifiedExpression = (QualifiedExpression) callee;
// Safe to cast here - type is verified
}
if (qualifier.is(Tree.Kind.NAME)) {
String qualifierName = ((Name) qualifier).name();
// Safe to cast here
}Why this is critical:
- The
calleecan be either aName(simple function call) or aQualifiedExpression(method call) - Direct casting without type checking causes
ClassCastException .is(Tree.Kind.X)is the SonarQube API way to check node types- Always verify type with
.is()before casting
Examples of type checking:
# QUALIFIED_EXPR (method call)
logger.info(...) # callee.is(Tree.Kind.QUALIFIED_EXPR) = true
logging.debug(...) # callee.is(Tree.Kind.QUALIFIED_EXPR) = true
# NAME (simple function call)
print(...) # callee.is(Tree.Kind.NAME) = true
len(...) # callee.is(Tree.Kind.NAME) = trueFull execution flow for a logging call:
1. Python code: logging.info("{}".format(x))
↓
2. checkCallExpression() called (Phase 2)
↓
3. isUsingLoggingLib? → Yes (from Phase 1)
↓
4. Is QUALIFIED_EXPR? → Yes (logging.info)
↓
5. Method name "info" in LOGGING_METHODS? → Yes
↓
6. isLoggingQualifier("logging")? → Yes (in LOGGING_MODULE_NAMES)
↓
7. checkLoggingArguments() called
↓
8. messageArgIndex = 0 (not log() method)
↓
9. Get first argument: "{}".format(x)
↓
10. isFString()? → No
↓
11. Is CALL_EXPR? → Yes
↓
12. Callee is QUALIFIED_EXPR? → Yes
↓
13. Method name is "format"? → Yes
↓
14. ctx.addIssue() → ISSUE RAISED! ✓
Separation of Concerns:
LoggingImportVisitor: Only handles import detectionLoggerAssignmentVisitor: Only handles assignment tracking- Main class: Orchestrates and makes final decisions
Extensibility:
- Want to support structlog? Add it to
LOGGING_MODULE_NAMES - Want to detect new pattern? Add a method to check for it
- Want to track more assignment types? Extend
LoggerAssignmentVisitor
Reusability:
- Visitors are stateless (except for their collection results)
- Can be reused across multiple files
- BaseTreeVisitor provides default traversal logic
Performance:
- Two focused AST traversals (imports + assignments)
- Then efficient point checks on method calls
- No need to traverse entire AST for every check
src/test/resources/checks/detectBadLoggingFormat/
├── logging/
│ ├── loggingCompliant.py (33 lines, 0 issues)
│ ├── loggingNonCompliant.py (39 lines, 24 issues)
│ ├── detectBad...Compliant.py (46 lines, 0 issues)
│ └── detectBad...NonCompliant.py (54 lines, 31 issues)
└── loguru/
├── loguruCompliant.py (15 lines, 0 issues)
└── loguruNonCompliant.py (19 lines, 11 issues)
public class DetectBadLoggingFormatInterpolationTest {
@Test
public void test_logging_bad_format_interpolation() {
// Test standard logging library
PythonCheckVerifier.verify("...loggingNonCompliant.py", ...);
PythonCheckVerifier.verifyNoIssue("...loggingCompliant.py", ...);
}
@Test
public void test_loguru_bad_format_interpolation() {
// Test loguru framework
PythonCheckVerifier.verify("...loguruNonCompliant.py", ...);
PythonCheckVerifier.verifyNoIssue("...loguruCompliant.py", ...);
}
@Test
public void test_combined_logging_formats() {
// Test combined files (backwards compatibility)
PythonCheckVerifier.verify("...detectBad...NonCompliant.py", ...);
PythonCheckVerifier.verifyNoIssue("...detectBad...Compliant.py", ...);
}
}| Framework | Files | Lines | Issues | Coverage |
|---|---|---|---|---|
| logging | 4 | 172 | 55 | 100% ✓ |
| loguru | 2 | 34 | 11 | 100% ✓ |
| Total | 6 | 206 | 66 | 100% ✓ |
Currently supported:
- ✓ logging (Python standard library)
- ✓ loguru (modern framework)
Not supported:
- ✗ structlog
- ✗ logbook
- ✗ Custom logging frameworks
Impact: Code using these frameworks won't be analyzed.
def get_logger():
return logging.getLogger(__name__)
logger = get_logger() # Not tracked
logger.info("Hello {}".format(name)) # Not detectedImpact: Loggers returned from functions or passed as parameters are not tracked.
# Simple case - Detected ✓
logging.info("Hello {}".format(name))
# Complex nested case - May not be detected
logging.info(get_message_function()("Hello {}").format(name))Impact: Very complex nested expressions may not be detected.
from loguru import logger
# Basic usage - Detected ✓
logger.info("Hello {}".format(name))
# Advanced features - Not detected ✗
logger.opt(colors=True).info("Hello {}".format(name))
logger.bind(user=user).info("Hello {}".format(name))Impact: Loguru's .opt(), .bind(), and .patch() methods are not checked.
# Detected ✓
logging.info("Hello {}".format(name))
logging.info(f"Hello {name}")
# Not detected ✗
logging.info("Hello %s" % name) # Old-style string formattingImpact: The % operator (old-style string formatting) is also inefficient but not detected.
- Add structlog support - Popular structured logging library
- Add logbook support - Alternative logging library
- Detect % operator -
"Hello %s" % namepattern
- Track function-returned loggers - Support
logger = get_logger() - Loguru advanced methods - Support
.opt(),.bind(),.patch() - Configuration file - Allow users to add custom frameworks
- Auto-fix suggestions - Propose corrections automatically
- Performance metrics - Show estimated resource savings
- Custom logger wrappers - Detect custom logging classes
- Fast: Only processes files that import logging libraries
- Efficient: Uses two-phase approach to minimize tree traversals
- Scalable: State reset ensures no memory leaks across files
Before fix:
# String interpolation happens immediately
logging.info("User {} logged in at {}".format(username, timestamp))
# Cost: 2 string operations + concatenation, even if log level is disabledAfter fix:
# String interpolation deferred until needed
logging.info("User %s logged in at %s", username, timestamp)
# Cost: None if log level is disabled, minimal if enabledEstimated savings:
- 10-30% reduction in string operations for applications with extensive logging
- More significant in production with higher log levels (INFO, WARNING, ERROR only)
- Python logging documentation: https://docs.python.org/3/library/logging.html
- Python logging performance: https://docs.python.org/3/howto/logging.html#optimization
- Loguru documentation: https://loguru.readthedocs.io/
- SonarQube Python API: https://github.com/SonarSource/sonar-python
- Green Code Initiative: https://green-code-initiative.org
- Rule ID: GCI111
- Rule Key:
GCI111 - Implementation Class:
DetectBadLoggingFormatInterpolation - Test Class:
DetectBadLoggingFormatInterpolationTest - Package:
org.greencodeinitiative.creedengo.python.checks - Category: Performance / Green Code
- Severity: Minor
- Type: Code Smell
- Remediation: 5 minutes
- Tags:
performance,eco-design,logging
Added:
- ✓ Support for
warnandfatalmethods (logging aliases) - ✓ Full support for Loguru framework (trace, success methods)
- ✓ Detection of directly imported loggers (
from loguru import logger) - ✓ Improved state management for multi-file analysis
- ✓ Organized test structure (logging/ and loguru/ directories)
- ✓ 3 separate test methods for better isolation
Enhanced:
- ✓ Extended test coverage to 66 test cases (55 logging + 11 loguru)
- ✓ Added comprehensive documentation
- ✓ Improved null safety and type checking
Initial Implementation:
- ✓ Support for standard
logginglibrary - ✓ Detection of
.format()and f-strings - ✓ Support for main logging methods (debug, info, warning, error, critical, exception, log)
- ✓ Proper state management for multi-file analysis
- ✓ AST-based pattern detection
Last Updated: February 6, 2026
Status: ✅ Production Ready
Maintainer: Green Code Initiative