serilogj is a structured logging library for Java, ported from Serilog for .NET. It uses message templates with named placeholders (e.g., "Hello {Name}") and supports object destructuring with the @ prefix (e.g., {@user}). Designed for use with Seq for structured log searching.
Compatible with JDK 8, 11, 17, 21 and 25. Pure Java, no external dependencies.
<dependency>
<groupId>org.serilogj</groupId>
<artifactId>serilogj</artifactId>
<version>0.6.1</version>
</dependency>import serilogj.Log;
import serilogj.LoggerConfiguration;
import serilogj.events.LogEventLevel;
import static serilogj.sinks.coloredconsole.ColoredConsoleSinkConfigurator.*;
import static serilogj.sinks.rollingfile.RollingFileSinkConfigurator.*;
import static serilogj.sinks.seq.SeqSinkConfigurator.*;
Log.setLogger(new LoggerConfiguration()
.setMinimumLevel(LogEventLevel.Verbose)
.writeTo(coloredConsole())
.writeTo(rollingFile("logs/app-{Date}.log"), LogEventLevel.Information)
.writeTo(seq("http://localhost:5341/"))
.createLogger());Log.information("Processing {RecordCount} records", records.length);
Log.warning("Disk space is low: {SpaceMB} MB remaining", spaceMB);
Log.error(exception, "Failed to process order {OrderId}", orderId);Six levels, from least to most severe:
Log.verbose("Detailed trace information");
Log.debug("Internal diagnostic info");
Log.information("Normal operation events");
Log.warning("Something unexpected happened");
Log.error(ex, "An operation failed");
Log.fatal(ex, "Application cannot continue");Use @ to destructure objects into their properties:
User user = new User();
user.setUserName("john");
// Scalar: logs user.toString()
Log.information("Hello {user}", user);
// Destructured: logs all public properties of user as structured data
Log.information("Hello {@user}", user);Add the calling class to log events:
ILogger logger = Log.forContext(MyService.class);
logger.information("Service started");
// Produces: ... [Information] Service started {SourceContext: "com.example.MyService"}ANSI-colored console output:
import static serilogj.sinks.coloredconsole.ColoredConsoleSinkConfigurator.*;
.writeTo(coloredConsole())
.writeTo(coloredConsole("{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"))Simple console output without ANSI codes:
import static serilogj.sinks.console.ConsoleSinkConfigurator.*;
.writeTo(console())
.writeTo(console("{Timestamp:HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"))Daily log file rotation with optional retention:
import static serilogj.sinks.rollingfile.RollingFileSinkConfigurator.*;
.writeTo(rollingFile("logs/app-{Date}.log"))Send structured events to a Seq server:
import static serilogj.sinks.seq.SeqSinkConfigurator.*;
.writeTo(seq("http://localhost:5341/"))
.writeTo(seq("http://localhost:5341/", "your-api-key"))Wrap any sink for non-blocking async writes:
import serilogj.sinks.async.AsyncWrapperSink;
ILogEventSink innerSink = coloredConsole();
.writeTo(new AsyncWrapperSink(innerSink)) // default buffer: 10000
.writeTo(new AsyncWrapperSink(innerSink, 5000)) // custom buffer size.writeTo(coloredConsole(), LogEventLevel.Verbose) // console gets everything
.writeTo(rollingFile("app-{Date}.log"), LogEventLevel.Warning) // file only warnings+Add contextual information to every log event:
import serilogj.core.enrichers.*;
Log.setLogger(new LoggerConfiguration()
.with(new ThreadIdEnricher())
.with(new ThreadNameEnricher())
.with(new ProcessIdEnricher())
.with(new MachineNameEnricher())
.writeTo(coloredConsole())
.createLogger());| Enricher | Property | Value |
|---|---|---|
ThreadIdEnricher |
ThreadId |
Current thread ID |
ThreadNameEnricher |
ThreadName |
Current thread name |
ProcessIdEnricher |
ProcessId |
JVM process ID |
MachineNameEnricher |
MachineName |
Hostname |
Control log verbosity per source context (namespace):
Log.setLogger(new LoggerConfiguration()
.setMinimumLevel(LogEventLevel.Information)
.setMinimumLevelOverride("com.example.noisylib", LogEventLevel.Warning)
.setMinimumLevelOverride("com.example.debug", LogEventLevel.Verbose)
.writeTo(coloredConsole())
.createLogger());Configure logging from a .properties file:
serilogj.properties:
serilogj.minimum-level=Information
serilogj.enrich.0=ThreadId
serilogj.enrich.1=MachineNameimport serilogj.configuration.PropertiesFileConfiguration;
LoggerConfiguration config = PropertiesFileConfiguration.configure("serilogj.properties");
Log.setLogger(config.writeTo(coloredConsole()).createLogger());Register custom sinks:
import serilogj.configuration.SinkRegistry;
SinkRegistry.register("console", props -> new ConsoleSink(
props.getOrDefault("template", "{Message}{NewLine}"), null));Push and pop properties within a scope:
import serilogj.context.LogContext;
import serilogj.core.enrichers.LogContextEnricher;
// Add LogContextEnricher during configuration
.with(new LogContextEnricher())
// Use in code
try (AutoCloseable prop = LogContext.pushProperty("RequestId", requestId)) {
Log.information("Handling request"); // includes RequestId
processRequest();
Log.information("Request complete"); // includes RequestId
}
// RequestId no longer attachedAlways close the logger on application shutdown to flush buffered sinks:
Log.closeAndFlush();mvn compile # Compile
mvn test # Run tests (50 tests)
mvn package # Build JAR
mvn verify # Full build + tests- RollingFileSink — Fixed duplicate condition that could skip file rotation
- BooleanScalarConversionPolicy — Boolean values were detected but never returned (result was always null)
- ScalarValue — TemporalAccessor formatting (LocalDateTime, ZonedDateTime, etc.) was broken due to inverted condition
- LogEventProperty —
isValidName()accepted any string with at least one non-space character; now validates that names start with a letter or underscore and contain only letters, digits, or underscores - LogEvent — Fixed typo
remotePropertyIfPresent→removePropertyIfPresent(old name kept as deprecated) - PropertyBinder — All internal log format strings were invalid (
%1,%2,{0}), causing errors in self-diagnostics - FileSink — File size limit was declared but never enforced; now checks file size before writing
- Reflection —
setAccessible(true)fails on JDK 16+ without--add-opens; now wrapped in try-catch with graceful skip - JsonFormatter — Replaced
new Integer(c)(removed in JDK 16) with(int) c - SeqSink — Replaced hardcoded charset string
"UTF8"withStandardCharsets.UTF_8 - Maven Compiler Plugin — Updated from 3.0 (2012) to 3.13.0
- SeqSink — HTTP connections and streams were never properly closed; now uses try-with-resources and
disconnect(). String concatenation in loop replaced with StringBuilder - FileSink —
outputwas not set to null after close - MessageTemplateCache — HashMap with synchronized blocks replaced with ConcurrentHashMap; eliminates race conditions
- Log —
_loggerfield is nowvolatilefor safe cross-thread visibility - EnumScalarConversionPolicy — HashMap with weak synchronization replaced with ConcurrentHashMap +
computeIfAbsent - JsonFormatter — SimpleDateFormat created on every call replaced with ThreadLocal for reuse
- Plain Console Sink — Simple console output without ANSI escape codes, useful for environments that don't support colors (CI, file redirection)
- Async Wrapper Sink — Wraps any sink with a background thread and BlockingQueue for non-blocking log writes
- Enrichers — ThreadId, ThreadName, ProcessId, MachineName — attach runtime context to every log event
- Minimum Level Override — Set different log levels per source context (e.g., silence a noisy library while keeping your own code verbose)
- Properties File Configuration — Configure minimum level and enrichers from a
.propertiesfile; register custom sinks via SinkRegistry
- 50 unit tests across 9 test classes covering parsers, formatters, policies, sinks, cache, reflection, and end-to-end logging
- JUnit 5 + Maven Surefire Plugin 3.2.5
- CI workflow for GitHub Actions with JDK matrix: 8, 11, 17, 21, 25
- All tests pass on JDK 8, 17, and 25 (tested locally)
Originally created by Jerremy Koot and 80dB as a Java port of Serilog by Nicholas Blumhardt.
Apache License 2.0 — see LICENSE for details.