Tentackle Logging — The Pluggable Logging API¶
Overview and Motivation¶
Tentackle does not log against a concrete logging library. Instead, the whole framework — and any application
built on it — logs through a thin, backend-agnostic facade in org.tentackle.log (in tentackle-core). At
runtime a single provider is bound to that facade through the
service API: plain java.util.logging by default, or
SLF4J or Log4J 2 by simply putting the matching jar on the path. No code changes, no per-library imports scattered
across the codebase.
Why a facade?¶
Tentackle code uses thousands of log statements and is deployed in very different environments — desktop (JavaFX), application servers, embedded, OSGi. A facade buys:
- Backend independence. Application and framework code call
Logger; the operator chooses the backend at assembly time. Swapping SLF4J for Log4J 2 is a dependency change, not a code change. - A uniform model across backends. A single five-value
Levelenum, aMappedDiagnosticContextthat works even on backends that don't natively support MDC, and modern conveniences (Supplier-based lazy messages,{0}-style parameter formatting) regardless of what the backend offers. - Framework value-adds. Declarative method-call logging (
@Log), per-method timing statistics, and small helpers (LoggingRunnable,LoggerOutputStream) that build on the facade. - Modular & classpath parity. Core declares
uses org.tentackle.log.Logger, so providers resolve under JPMS as well as on the plain classpath.
This page is the deep-dive for logging; the core module map links here. Related: the interceptors doc explains the proxy engine that
@Logplugs into.
Getting a Logger¶
The usual idiom is a static final field per class:
private static final Logger LOGGER = Logger.get(MyClass.class);
// or, using the StackWalker to discover the caller automatically:
private static final Logger LOGGER = Logger.get();
Logger.get(...) delegates to
LoggerFactory.getInstance().getLogger(name, null). Loggers are
named (by class name) and cached per name, so repeated lookups return the same instance.
The Logger API¶
Logger is the facade interface. Its surface is deliberately rich, so
call sites stay terse and efficient.
Levels¶
A single enum, ordered from most to least verbose:
These map onto each backend's native scale (see the implementations below). Per-level guards are provided both generically and explicitly:
Three ways to pass a message¶
Every level has overloads covering the three common patterns, so you never build a string you won't emit:
LOGGER.info("plain message");
LOGGER.info("user {0} logged in from {1}", userId, host); // MessageFormat-style params
LOGGER.fine(() -> "expensive " + dump()); // lazy Supplier, only evaluated if enabled
LOGGER.warning("partial failure", throwable); // message + cause
LOGGER.severe(throwable, () -> "context: " + ctx); // cause + lazy supplier
The same matrix exists on the generic log(Level, …) methods. Parameter suppliers
(finer(String, Supplier<?>...)) defer even the argument computation.
Stacktraces and backend access¶
LOGGER.logStacktrace(throwable); // at SEVERE
LOGGER.logStacktrace(Level.FINE, throwable);
Object backend = LOGGER.getLoggerImpl(); // the underlying java.util / slf4j / log4j logger
MappedDiagnosticContext mdc = LOGGER.getMappedDiagnosticContext();
getLoggerImpl() is the escape hatch for the rare case you need a backend-specific feature.
Mapped Diagnostic Context (MDC)¶
MappedDiagnosticContext provides thread-local
key/value pairs that enrich every log line on the current thread (user id, request id, session, …):
MappedDiagnosticContext mdc = LOGGER.getMappedDiagnosticContext();
mdc.put("user", userId);
try {
... // all log lines on this thread can include {user}
}
finally {
mdc.remove("user"); // or mdc.clear()
}
Crucially, the abstraction is uniform even where the backend lacks MDC. SLF4J and Log4J 2 delegate to their
native MDC; for java.util.logging (which has none),
DefaultMappedDiagnosticContext keeps its own
InheritableThreadLocal map — so application behavior does not depend on the chosen provider.
AbstractMappedDiagnosticContext supplies the
shared logic (key set, toString() rendering, pattern matching).
Framework Value-Adds¶
These build on the facade and are reasons the abstraction exists beyond mere backend-swapping.
@Log — declarative method-call logging¶
Annotate a method of an Interceptable (e.g., a PDO or any interceptable proxy) and every call is logged with its
elapsed time:
@Log // defaults to INFO
@Log(Logger.Level.FINE) // or a chosen level
public void recalculate() { ... }
@Log is wired via @Interception(implementedBy = LogInterceptor.class);
LogInterceptor times the invocation with a TimeKeeper (only
when the level is enabled) and logs "<method> took <n>ms". This is a concrete example of the
interception engine in action.
Method statistics¶
MethodStatistics /
StatisticsResult collect per-method invocation counts and
min/max/total durations — used, for instance, to profile remote-delegate calls. Keyed by
MethodStatisticsKey (method + serviced class).
Helpers¶
LoggingRunnable— wraps aRunnableto log its start and duration at a chosen level.LoggerOutputStream— anOutputStreamthat funnels writes into a logger; handy to capturee.printStackTrace(stream)output.Loggable— an interface objects (notably exceptions) implement to declare how they want to be logged (level, with/without stacktrace); honored e.g. by the remote-invocation handler.
How a Backend Is Selected¶
LoggerFactory is a singleton obtained through ServiceFactory
(default DefaultLoggerFactory). At first use the factory
resolves the configured Logger implementation class:
DefaultLoggerFactorycallsServiceFactory.createServiceClass(Logger.class). TheLoggerimplementation a provider jar registers via@Service(Logger.class)(generated intoMETA-INF/services/org.tentackle.log.Logger) becomes the bound class.- If no provider is found, it logs "no logging backend configured → using default java.util.logging" and
falls back to
DefaultLogger. (DefaultLoggeritself carries no@Serviceannotation, precisely so it never competes with a real provider.) - To instantiate a named logger, the factory prefers a static
getLogger(String)factory method on the implementation (all providers offer one, enabling per-name caching), and falls back to aLogger(String)constructor. Reflection lookups are cached per class.
Because the binding is "the single @Service(Logger.class) on the path", choosing a backend is just a matter of
dependencies — put exactly one provider module on the path.
The Implementations¶
| Module | Backend | Registered as |
|---|---|---|
(built into tentackle-core) |
java.util.logging |
fallback — no @Service |
tentackle-log-slf4j |
SLF4J (and whatever SLF4J binds to) | @Service(Logger.class) |
tentackle-log-log4j2v |
Apache Log4J 2 | @Service(Logger.class) |
All providers translate the Tentackle Level onto the backend scale identically:
| Tentackle | java.util.logging |
SLF4J | Log4J 2 |
|---|---|---|---|
FINER |
FINER/FINE |
TRACE |
TRACE |
FINE |
FINE |
DEBUG |
DEBUG |
INFO |
INFO |
INFO |
INFO |
WARNING |
WARNING |
WARN |
WARN |
SEVERE |
SEVERE |
ERROR |
ERROR |
Default — java.util.logging (no extra module)¶
DefaultLogger wraps java.util.logging.Logger. It is the
zero-dependency fallback used when no provider jar is present, with the thread-local
DefaultMappedDiagnosticContext standing in for the MDC that JUL lacks.
SLF4J — tentackle-log-slf4j¶
SLF4JLogger wraps an
org.slf4j.Logger. It detects whether the bound logger is a LocationAwareLogger and, if so, logs through the
location-aware API so the caller's class/line is reported rather than the wrapper's (it excludes its own
classname from the stack). MDC delegates to SLF4J's MDC via
SLF4JMappedDiagnosticContext.
This module then binds to whatever SLF4J backend you add (Logback, slf4j-simple, …). See the
tentackle-log-slf4j module doc for details.
Log4J 2 — tentackle-log-log4j2v¶
Log4J2Logger wraps
Log4J 2 through an ExtendedLoggerWrapper, using the wrapper FQCN so location information stays correct, and maps
messages onto Log4J's Level/SimpleMessage. MDC delegates to Log4J's ThreadContext via
Log4J2MappedDiagnosticContext.
("log4j2v" = Log4J version 2, distinct from the long-dead Log4J 1.) See the
tentackle-log-log4j2v module doc for details.
Packaging and Modularity¶
tentackle-core exports org.tentackle.log, requires java.logging, and declares
uses org.tentackle.log.Logger so providers resolve under JPMS even when they appear as automatic modules.
Each provider registers its Logger (and a ModuleHook service for resource access) at build time via the
@Service annotation processor, which generates META-INF/services/org.tentackle.log.Logger. The provider
modules are themselves modular:
module org.tentackle.log.slf4j { module org.tentackle.log.log4j2v {
requires transitive org.tentackle.core; requires transitive org.tentackle.core;
requires org.slf4j; requires org.apache.logging.log4j;
provides ...ModuleHook with ...; provides ...ModuleHook with ...;
} }
For OSGi, dedicated activator bundles exist under tentackle-osgi/tentackle-osgi-activators/ for the
SLF4J, Log4J 2 and Log4J providers.
Choosing a backend¶
Add exactly one provider module as a dependency (plus its own backend libraries — e.g. a Logback binding for
SLF4J, or the Log4J 2 core). With none present, Tentackle logs via java.util.logging.
<!-- e.g. route Tentackle logging through SLF4J -->
<dependency>
<groupId>org.tentackle</groupId>
<artifactId>tentackle-log-slf4j</artifactId>
</dependency>
Implementing a new provider¶
Implement Logger (extending nothing is required, but providing a static
getLogger(String) for caching is the convention) and a
MappedDiagnosticContext, annotate the logger
@Service(Logger.class), and put the jar on the path. DefaultLoggerFactory discovers and binds it.
Source Map¶
| Type | Location |
|---|---|
Logger, LoggerFactory, MappedDiagnosticContext |
tentackle-core · org.tentackle.log |
DefaultLoggerFactory, DefaultLogger, DefaultMappedDiagnosticContext, AbstractMappedDiagnosticContext |
tentackle-core · org.tentackle.log |
@Log, LogInterceptor, Loggable, LoggingRunnable, LoggerOutputStream |
tentackle-core · org.tentackle.log |
MethodStatistics, MethodStatisticsKey, StatisticsResult |
tentackle-core · org.tentackle.log |
| SLF4J provider | tentackle-log-slf4j · org.tentackle.log.slf4j |
| Log4J 2 provider | tentackle-log-log4j2v · org.tentackle.log.log4j2v |
| OSGi activators | tentackle-osgi/tentackle-osgi-activators/ |