domino-slf4j-logger is an SLF4J implementation for browser-side Java applications built with GWT or J2CL. It keeps the familiar SLF4J API, but adds browser-friendly routing, scoped MDC helpers, and console adapters that work well in web apps.
Logger, Marker, and MDC entry pointsAdd the dependency, then inherit the GWT module in your app module.
<dependency>
<groupId>org.dominokit</groupId>
<artifactId>domino-slf4j-logger</artifactId>
<version>${domino.slf4j.version}</version>
</dependency>
<inherits name="org.dominokit.domino.logger.Logging"/>
That is usually enough to start using SLF4J in the client.
Basic logging looks the same as regular SLF4J. By default, output goes to the browser console through the built-in console adapter.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Hello {
private static final Logger log = LoggerFactory.getLogger(Hello.class);
public void run() {
log.info("Hello from the browser");
log.warn("Be careful: {}", "something happened");
try {
risky();
} catch (Exception e) {
log.error("Operation failed", e);
}
}
private void risky() {
throw new RuntimeException("boom");
}
}
LoggerFactory.getLogger(...) returns a DominoLoggingAdapter.LoggingRouter chooses the adapter for marker-aware calls.LoggingAdapter writes to the final destination, such as the browser console or a remote endpoint.Marker routing lets you send different log streams to different adapters. Register routes once during startup, using marker names rather than marker instances.
import org.dominokit.domino.logger.ConsoleLoggingAdapter;
import org.dominokit.domino.logger.LoggingRouter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
public class PaymentFlow {
private static final Logger log = LoggerFactory.getLogger(PaymentFlow.class);
private static final Marker PAYMENT = MarkerFactory.getMarker("PAYMENT");
private static final Marker ONLINE = MarkerFactory.getMarker("ONLINE");
public void initLogging() {
PAYMENT.add(ONLINE);
LoggingRouter.setDefaultAdapter(new ConsoleLoggingAdapter());
LoggingRouter.register("PAYMENT", new ConsoleLoggingAdapter());
LoggingRouter.register("ONLINE", new ConsoleLoggingAdapter());
}
public void charge(String orderId) {
log.info(PAYMENT, "Charging order {}", orderId);
}
}
LoggingRouter.clear() removes all marker mappings, and unregister(...) removes one mapping.Implement LoggingAdapter when you want to send logs somewhere other than the browser console.
import java.util.Map;
import org.dominokit.domino.logger.LoggingAdapter;
import org.slf4j.Marker;
import org.slf4j.event.Level;
public class RemoteAdapter implements LoggingAdapter {
@Override
public void log(
Level level,
String loggerName,
Marker marker,
String message,
Throwable throwable,
Map<String, String> mdc) {
// Serialize the event and send it to your backend here.
}
}
If you need to fan out to more than one destination, use CompositeLoggingAdapter.
import org.dominokit.domino.logger.CompositeLoggingAdapter;
import org.dominokit.domino.logger.ConsoleLoggingAdapter;
import org.dominokit.domino.logger.LoggingRouter;
LoggingRouter.setDefaultAdapter(
new CompositeLoggingAdapter(new ConsoleLoggingAdapter(), new RemoteAdapter()));
LoggingRouter.register("AUDIT", new RemoteAdapter());
CompositeLoggingAdapter broadcasts to every delegate. Because it stores delegates in a set, the call order is not guaranteed. If order matters, write a small custom adapter instead.
The MDC helpers are scoped and work well with browser-style, single-threaded code. Use them for request IDs, user IDs, correlation IDs, and similar context.
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import org.dominokit.domino.logger.MDCUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserFlow {
private static final Logger log = LoggerFactory.getLogger(UserFlow.class);
public void execute(String userId) throws Exception {
try (AutoCloseable scope = MDCUtils.withMdc("userId", userId)) {
log.info("Starting user flow");
nested();
}
}
public void executeWithContext() throws Exception {
Map<String, String> ctx = new LinkedHashMap<>();
ctx.put("flow", "onboarding");
ctx.put("step", "emailVerification");
try (AutoCloseable scope = MDCUtils.withMdc(ctx)) {
log.info("Verifying email");
}
}
private void nested() {
Deque<String> userIdStack = MDCUtils.getMdcStack("userId");
log.info("Nested step {}", userIdStack);
}
}
getMdcStack(...) returns a defensive copy, so reading it does not mutate live MDC state.This adapter writes to the browser console and maps levels to matching console methods: TRACE and DEBUG to console.debug, INFO to console.info, WARN to console.warn, and ERROR to console.error.
This adapter forwards each event to multiple adapters. It is useful when you want console output and a remote sink at the same time.
The module defines a few GWT properties with defaults.
<define-property name="domino.slf4j.logging.level" values="INFO,WARN,DEBUG,TRACE,ERROR,OFF"/>
<define-property name="domino.slf4j.logging.showCaller" values="true,false"/>
<define-property name="domino.slf4j.logging.showMdc" values="true,false"/>
Default values are domino.slf4j.logging.level=INFO, domino.slf4j.logging.showCaller=false, and domino.slf4j.logging.showMdc=false.
Override them in your app module before compilation.
<module>
<inherits name="org.dominokit.domino.logger.Logging"/>
<set-property name="domino.slf4j.logging.level" value="DEBUG"/>
<set-property name="domino.slf4j.logging.showCaller" value="true"/>
<set-property name="domino.slf4j.logging.showMdc" value="true"/>
</module>
showCaller prepends source location information like [MyClass:42]. showMdc prepends the current MDC context when one is available.
The library ships a default SLF4J service provider for GWT/J2CL environments. In normal use, you just inherit the GWT module and start logging. You do not need to wire META-INF/services by hand.
If you need custom behavior, extend DefaultDominoLoggingServiceProvider and install your provider early in application startup, before the first LoggerFactory, MarkerFactory, or MDC access.
The bundled marker factory caches markers by name, so repeated MarkerFactory.getMarker("PAYMENT") calls return the same marker instance. Use getDetachedMarker(...) when you need a standalone marker that is not cached.
Routing is global state. Register adapters during bootstrap, before the first log event, if you want marker-specific routing to apply consistently.
If you reconfigure logging in tests or hot-reload flows, clear router state and MDC state explicitly so one run does not leak into the next.
The router searches the marker itself first, then walks referenced markers recursively, and finally falls back to the default adapter. That makes marker hierarchies useful for broad categories.
org.dominokit.domino.logger.Logging and that your level property is not set to OFF.MDCUtils.withMdc(...).SECURITY, AUDIT, and PAYMENT.CompositeLoggingAdapter when you want the same event in both the console and a backend.