Overview
This guide defines mandatory logging rules for contributors working on the Keyple codebase.
The objective is to ensure:
- predictable behavior for integrators,
- compatibility with Android ProGuard / R8 optimizations,
- consistent logging across all libraries.
SLF4J usage
Dependency configuration
All Keyple JVM libraries using logging MUST declare SLF4J using the following configuration:
compileOnly("org.slf4j:slf4j-api:1.7.36")
Both the dependency scope (compileOnly) and the exact version (1.7.36) are mandatory and must not be changed locally.
Why compileOnly is required:
- Keyple must remain logging-backend agnostic
Keyple is a library and must not impose any SLF4J binding or logging implementation (Timber, Logback, Log4j, Android Log, etc.) on integrator applications. - Avoids leaking dependencies into the application classpath
Declaring SLF4J asimplementationwould make it a transitive dependency, potentially overriding or conflicting with the version selected by the application. - Preserves full control for integrators
Integrators decide:- which SLF4J binding to use,
- how logging is routed (Android Log, Timber, file, remote, etc.),
- which log levels are enabled per build type.
- Ensures correct ProGuard / R8 behavior on Android
ProGuard and R8 rules that disable or optimize logging are applied at the application level. UsingcompileOnlyensures those rules apply uniformly to Keyple and to all third-party libraries. - Prevents runtime coupling to logging behavior
Keyple code relies strictly on SLF4J API contracts and never on runtime side effects, guaranteeing predictable behavior across all environments.
Why SLF4J 1.7.36 is mandatory:
- Last stable release of the SLF4J 1.7.x line
Version1.7.36is the final and most stable release before the major 2.x API changes. - Binary compatibility with existing bindings
Most Android-compatible SLF4J bridges (includingslf4j-timber) target the 1.7.x API. - Allows integrators to freely choose their SLF4J version (including 2.x)
Keyple is compiled against SLF4J1.7.36, whose API remains compatible with SLF4J 2.x.
As a result:- integrator applications may safely use SLF4J 1.7.x or 2.x at runtime,
- the chosen SLF4J version and binding are fully controlled by the application,
- Keyple remains agnostic to logging backend and runtime logging behavior.
- Predictable behavior with Android tooling
Version1.7.36is well-tested with:- Android Gradle Plugin,
- D8 (DEX compiler) / R8,
- ProGuard optimizations.
Logger declaration
Loggers must be declared once per class as a static field (Java) or inside a companion object (Kotlin).
This ensures:
- a single logger instance per class,
- consistent logger naming,
- minimal runtime overhead,
- better analysis and optimization by R8.
Java
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
Kotlin
companion object {
private val logger = LoggerFactory.getLogger(MyClass::class.java)
}
Android constraints and rationale
ProGuard / R8 limitations
On Android:
- SLF4J loggers are accessed via interfaces,
- concrete implementations are resolved dynamically at runtime,
- R8 cannot safely assume that unguarded interface calls have no side effects.
As a result, when logs are not conditionally guarded:
logger.debug(...)calls may remain in the optimized bytecode,- log arguments may still be evaluated,
- unnecessary allocations and computations may occur,
- performance may be impacted, even in release builds.
What R8 can optimize reliably
When the conditional pattern is used:
if (logger.isDebugEnabled()) {
logger.debug(...);
}
And with the appropriate ProGuard / R8 rules:
isDebugEnabled()is treated as alwaysfalse,- the conditional branch is proven unreachable,
- the entire block is removed,
- argument evaluation is completely eliminated.
Logging coding standards
Conditional logging pattern (mandatory)
All DEBUG and TRACE log statements MUST be conditionally guarded.
This pattern is mandatory in all Keyple code, without exception.
Java (required)
if (logger.isDebugEnabled()) {
logger.debug("Card detected [powerOnData={}]", cardPowerOnData);
}
Kotlin (required)
if (logger.isDebugEnabled) {
logger.debug("Card detected [powerOnData={}]", cardPowerOnData)
}
Forbidden logging patterns
Unguarded logging calls are strictly forbidden, even when:
- the log level is expected to be disabled,
- arguments appear inexpensive,
- the message uses constants or string literals.
Java (forbidden)
// ❌ Forbidden: unguarded debug logging
logger.info("Card detected [powerOnData=" + cardPowerOnData + "]");
// ✅ Valid
logger.info("Card detected [powerOnData={}]", cardPowerOnData);
Kotlin (forbidden)
// ❌ Forbidden: unguarded debug logging
logger.info("Card detected [powerOnData=${cardPowerOnData}]")
// ✅ Valid
logger.info("Card detected [powerOnData={}]", cardPowerOnData)
Logging levels in Keyple
| Level | Usage |
|---|---|
| ERROR | Functional or technical failures |
| WARN | Unexpected but recoverable situations |
| INFO | High-level lifecycle events |
| DEBUG | Detailed execution flow (guarded) |
| TRACE | Low-level protocol / byte-level details (guarded) |
Summary for contributors
- Use SLF4J
1.7.36withcompileOnlymode only - Always guard DEBUG and TRACE logs
- Never rely on ProGuard to remove unguarded logs
- Write logs with integrators and performance in mind