Production applications need two things to stay healthy: clear logging and reliable error handling. Oorian provides both out of the box, with a lightweight logging facade and a layered error handling system that are entirely opt-in and framework-agnostic. In this post, we'll walk through how both systems work and how to get the most out of them.
Logging with OorianLog
Rather than bundling a specific logging framework, Oorian delegates to the JDK's System.Logger platform logging SPI, introduced in Java 9. This means your application can use any logging backend—SLF4J, Log4j2, java.util.logging, or any other provider—without Oorian requiring additional dependencies.
Getting Started
Create a logger instance as a private static final field in each class that needs logging:
public class OrderService
{
private static final OorianLog LOG = OorianLog.getLogger(OrderService.class);
public void processOrder(String orderId)
{
LOG.info("Processing order {0}", orderId);
try
{
// ... process the order
LOG.debug("Order {0} completed successfully", orderId);
}
catch (Exception ex)
{
LOG.error("Failed to process order", ex);
}
}
}Each logger is named after the fully qualified class name, so log output always identifies its source. The logger instance is lightweight and safe to store as a static field.
Five Log Levels
OorianLog provides five log levels, each mapped to a System.Logger.Level:
- TRACE — Fine-grained detail: per-message operations, heartbeats, pings
- DEBUG — Diagnostic information: request routing, connections opening and closing, session lifecycle
- INFO — Lifecycle events: application startup and shutdown, license status, registration counts
- WARNING — Recoverable issues: CSRF validation failures, missing resources, security events
- ERROR — Failures: exceptions that affect functionality, unrecoverable errors
A good rule of thumb: reserve INFO for true lifecycle events. A healthy application running in steady state should produce little or no INFO output. Use DEBUG for request-level detail and TRACE for per-message granularity.
Three Method Overloads Per Level
Each level provides three overloads for different use cases:
// Simple message
LOG.info("Application started");
// Message with exception (stack trace is included in the output)
LOG.error("Failed to load configuration", ex);
// Parameterized message using MessageFormat syntax: {0}, {1}, ...
LOG.debug("Processing page {0} for session {1}", pageId, sessionId);Parameterized messages use java.text.MessageFormat syntax with numbered placeholders. This is more efficient than string concatenation because the message is only formatted if the level is enabled.
Guarding Expensive Construction
If building a log message is expensive (e.g., serializing objects or iterating over large collections), use isLoggable() to check whether the level is enabled before constructing the message:
if (LOG.isLoggable(System.Logger.Level.DEBUG))
{
String details = buildExpensiveDebugInfo(request);
LOG.debug("Request details: {0}", details);
}For simple parameterized messages, this guard is not necessary—the framework avoids formatting the message if the level is disabled.
Pluggable Backends
By default, System.Logger delegates to java.util.logging (JUL). To route Oorian log output through a different logging framework, add the appropriate bridge library to your classpath:
- SLF4J (Logback, etc.) —
org.slf4j:slf4j-jdk-platform-logging - Log4j2 —
org.apache.logging.log4j:log4j-jpl - JUL — No additional dependency required (default)
Once a bridge is on the classpath, all Oorian log output is automatically routed through your chosen backend. Oorian does not require any logging configuration to run—add a bridge library only if you want to unify Oorian's logging with your application's existing logging framework.
Error Handling
Oorian provides a layered error handling system that covers custom error pages, built-in defaults, centralized exception monitoring, and development-mode diagnostics. All error handling features are opt-in and configured in your Application.initialize() method.
Custom Error Pages
Custom error pages extend ErrorPage and implement createBody(Body body). Build your page using standard Oorian elements—the base class handles the HTML document skeleton, charset, viewport, and a minimal CSS reset:
public class NotFoundPage extends ErrorPage
{
@Override
protected void createBody(Body body)
{
body.addElement(new H1("Error " + getStatusCode()));
body.addElement(new Paragraph(getMessage()));
body.addElement(new Paragraph("Path: " + getRequestPath()));
}
}The ErrorPage base class provides convenience getters: getStatusCode(), getRequestPath(), getException(), getTitle(), and getMessage(). Error pages extend ErrorPage, not HtmlPage—they are lightweight, standalone HTML pages that avoid the WebSocket/event overhead and render reliably even when the normal page infrastructure has failed.
Registering Error Pages
Register custom error pages for specific HTTP status codes in your Application class:
@Override
protected void initialize(ServletContext context)
{
registerPackage("com.myapp");
// Register custom error pages for specific status codes
setErrorPage(404, NotFoundPage.class);
setErrorPage(403, ForbiddenPage.class);
setErrorPage(500, ServerErrorPage.class);
// Set a default for any status code without a specific page
setDefaultErrorPage(GenericErrorPage.class);
}The lookup order is: status-specific page → default error page → built-in default. If no custom error pages are registered, Oorian's built-in DefaultErrorPage automatically activates as the fallback for all status codes, rendering a clean, user-friendly message with the status code, description, and a “Return to Home” link.
Centralized Exception Handler
For application-wide exception monitoring, logging, and alerting, implement the ExceptionHandler interface:
public class AppExceptionHandler implements ExceptionHandler
{
private static final OorianLog LOG =
OorianLog.getLogger(AppExceptionHandler.class);
@Override
public void handle(Exception exception, HtmlPage page)
{
LOG.error("Unhandled exception", exception);
alertService.notify(exception);
}
}Register it in your Application:
setExceptionHandler(new AppExceptionHandler());The handler integrates at three points in the framework:
- Page creation — exceptions during
initializePage(),createHead(), orcreateBody() - WebSocket message handling — exceptions while processing client events
- Worker threads — exceptions in
OorianWorkerThreadexecution
The centralized handler is invoked before the page-level onException() hook, giving you a single place to add logging or alerting without modifying individual pages.
Development Mode
In production (the default), error pages show a user-friendly message with no technical details. In development mode, they include the request path, exception class, message, and full stack trace.
Enable it programmatically:
setDevMode(true);Or via a system property (recommended):
-Doorian.mode=devThe system property approach is preferred because a setDevMode(true) call in initialize() can easily be overlooked during code review and accidentally deployed to production. Never enable development mode in production—it exposes stack traces, class names, and request paths that could help an attacker understand your application's internals.
Putting It All Together
Here's what a well-configured application looks like with both logging and error handling set up:
@WebListener
public class MyApp extends Application
{
@Override
protected void initialize(ServletContext context)
{
registerPackage("com.myapp");
// Custom error pages
setErrorPage(404, NotFoundPage.class);
setErrorPage(500, ServerErrorPage.class);
setDefaultErrorPage(GenericErrorPage.class);
// Centralized exception monitoring
setExceptionHandler(new AppExceptionHandler());
}
}With this configuration, your application has clean logging through whatever backend you prefer, branded error pages for your users, and centralized exception monitoring for your operations team—all without a single line of JavaScript.
