Serilog is renowned in the .NET world for its clean syntax, powerful structured logging, and flexible sinks. In this article, we'll build a complete Serilog-inspired logging framework for Java that supports structured logging, enrichers, sinks, and the fluent API that makes Serilog so popular.
Project Setup
First, add the necessary dependencies to your pom.xml:
<dependencies> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Reflection for property discovery --> <dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.10.2</version> </dependency> <!-- Utilities --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.13.0</version> </dependency> <!-- For file operations --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.13.0</version> </dependency> <!-- For HTTP sink --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- For async processing --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.2-jre</version> </dependency> </dependencies>
Core Implementation
1. Log Event Models
LogEvent.java - Core log event with structured data
import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; public class LogEvent { private final Instant timestamp; private final LogLevel level; private final String messageTemplate; private final String renderedMessage; private final Map<String, Object> properties; private final Throwable exception; private final Map<String, Object> context; private LogEvent(Builder builder) { this.timestamp = builder.timestamp; this.level = builder.level; this.messageTemplate = builder.messageTemplate; this.renderedMessage = builder.renderedMessage; this.properties = Collections.unmodifiableMap(new HashMap<>(builder.properties)); this.exception = builder.exception; this.context = Collections.unmodifiableMap(new HashMap<>(builder.context)); } public static class Builder { private Instant timestamp = Instant.now(); private LogLevel level; private String messageTemplate; private String renderedMessage; private Map<String, Object> properties = new HashMap<>(); private Throwable exception; private Map<String, Object> context = new HashMap<>(); public Builder timestamp(Instant timestamp) { this.timestamp = timestamp; return this; } public Builder level(LogLevel level) { this.level = level; return this; } public Builder messageTemplate(String messageTemplate) { this.messageTemplate = messageTemplate; return this; } public Builder renderedMessage(String renderedMessage) { this.renderedMessage = renderedMessage; return this; } public Builder property(String key, Object value) { this.properties.put(key, value); return this; } public Builder properties(Map<String, Object> properties) { this.properties.putAll(properties); return this; } public Builder exception(Throwable exception) { this.exception = exception; return this; } public Builder context(String key, Object value) { this.context.put(key, value); return this; } public Builder context(Map<String, Object> context) { this.context.putAll(context); return this; } public LogEvent build() { if (level == null) { throw new IllegalStateException("Log level is required"); } if (messageTemplate == null) { throw new IllegalStateException("Message template is required"); } return new LogEvent(this); } } // Getters public Instant getTimestamp() { return timestamp; } public LogLevel getLevel() { return level; } public String getMessageTemplate() { return messageTemplate; } public String getRenderedMessage() { return renderedMessage; } public Map<String, Object> getProperties() { return properties; } public Throwable getException() { return exception; } public Map<String, Object> getContext() { return context; } public Optional<Object> getProperty(String key) { return Optional.ofNullable(properties.get(key)); } public Optional<Object> getContextValue(String key) { return Optional.ofNullable(context.get(key)); } } LogLevel.java - Log levels with Serilog compatibility
public enum LogLevel { VERBOSE(0), DEBUG(1), INFORMATION(2), WARNING(3), ERROR(4), FATAL(5); private final int value; LogLevel(int value) { this.value = value; } public int getValue() { return value; } public boolean isEnabled(LogLevel minimumLevel) { return this.value >= minimumLevel.value; } public static LogLevel fromString(String level) { if (level == null) return INFORMATION; return switch (level.toUpperCase()) { case "VERBOSE", "VRB" -> VERBOSE; case "DEBUG", "DBG" -> DEBUG; case "INFORMATION", "INFO", "INF" -> INFORMATION; case "WARNING", "WARN", "WRN" -> WARNING; case "ERROR", "ERR" -> ERROR; case "FATAL", "FTL" -> FATAL; default -> INFORMATION; }; } } 2. Message Template System
MessageTemplate.java - Serilog-style message template parser and renderer
import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class MessageTemplate { private static final Pattern PROPERTY_PATTERN = Pattern.compile("\\{(@?)([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]*))?\\}"); private final String template; private final List<TemplateToken> tokens; private final List<String> propertyNames; public MessageTemplate(String template) { this.template = template; this.tokens = parseTemplate(template); this.propertyNames = extractPropertyNames(); } private List<TemplateToken> parseTemplate(String template) { List<TemplateToken> tokens = new ArrayList<>(); Matcher matcher = PROPERTY_PATTERN.matcher(template); int lastIndex = 0; while (matcher.find()) { // Add text before the property if (matcher.start() > lastIndex) { tokens.add(new TextToken(template.substring(lastIndex, matcher.start()))); } // Add property token boolean isDestructuring = "@".equals(matcher.group(1)); String propertyName = matcher.group(2); String format = matcher.group(3); tokens.add(new PropertyToken(propertyName, format, isDestructuring)); lastIndex = matcher.end(); } // Add remaining text if (lastIndex < template.length()) { tokens.add(new TextToken(template.substring(lastIndex))); } return tokens; } private List<String> extractPropertyNames() { return tokens.stream() .filter(token -> token instanceof PropertyToken) .map(token -> ((PropertyToken) token).getPropertyName()) .toList(); } public String render(Map<String, Object> properties) { StringBuilder result = new StringBuilder(); for (TemplateToken token : tokens) { if (token instanceof TextToken) { result.append(((TextToken) token).getText()); } else if (token instanceof PropertyToken) { PropertyToken propToken = (PropertyToken) token; Object value = properties.get(propToken.getPropertyName()); result.append(renderProperty(value, propToken.getFormat())); } } return result.toString(); } private String renderProperty(Object value, String format) { if (value == null) { return "null"; } // Basic formatting - extend this for more complex formatting if (format != null) { return String.format("%" + format, value); } return value.toString(); } public List<String> getPropertyNames() { return propertyNames; } public String getTemplate() { return template; } // Token classes private interface TemplateToken {} private static class TextToken implements TemplateToken { private final String text; TextToken(String text) { this.text = text; } public String getText() { return text; } } private static class PropertyToken implements TemplateToken { private final String propertyName; private final String format; private final boolean destructuring; PropertyToken(String propertyName, String format, boolean destructuring) { this.propertyName = propertyName; this.format = format; this.destructuring = destructuring; } public String getPropertyName() { return propertyName; } public String getFormat() { return format; } public boolean isDestructuring() { return destructuring; } } } 3. Logger Interface and Implementation
SerilogLogger.java - Main logger interface with fluent API
public interface SerilogLogger { // Basic logging methods boolean isEnabled(LogLevel level); void write(LogLevel level, String messageTemplate, Object... propertyValues); void write(LogLevel level, Throwable exception, String messageTemplate, Object... propertyValues); // Fluent API for structured logging LogEventBuilder forContext(String propertyName, Object propertyValue); LogEventBuilder forContext(Class<?> sourceType); // Level-specific methods with Serilog-style names LogEventBuilder verbose(); LogEventBuilder debug(); LogEventBuilder information(); LogEventBuilder warning(); LogEventBuilder error(); LogEventBuilder fatal(); // Close the logger void close(); } LogEventBuilder.java - Fluent API for building log events
import java.util.HashMap; import java.util.Map; public class LogEventBuilder { private final SerilogLogger logger; private final LogLevel level; private final Map<String, Object> properties; private Throwable exception; public LogEventBuilder(SerilogLogger logger, LogLevel level) { this.logger = logger; this.level = level; this.properties = new HashMap<>(); } public LogEventBuilder property(String name, Object value) { properties.put(name, value); return this; } public LogEventBuilder properties(Map<String, Object> properties) { this.properties.putAll(properties); return this; } public LogEventBuilder withException(Throwable exception) { this.exception = exception; return this; } // Main log method public void log(String messageTemplate, Object... propertyValues) { Map<String, Object> mergedProperties = mergeProperties(messageTemplate, propertyValues); logger.write(level, exception, messageTemplate, propertyValues); } private Map<String, Object> mergeProperties(String messageTemplate, Object[] propertyValues) { Map<String, Object> merged = new HashMap<>(properties); // Extract properties from message template MessageTemplate template = new MessageTemplate(messageTemplate); List<String> propertyNames = template.getPropertyNames(); for (int i = 0; i < Math.min(propertyValues.length, propertyNames.size()); i++) { merged.put(propertyNames.get(i), propertyValues[i]); } return merged; } } SerilogCoreLogger.java - Core logger implementation
import java.util.ArrayList; import java.util.List; import java.util.Map; public class SerilogCoreLogger implements SerilogLogger { private final String sourceContext; private final LogLevel minimumLevel; private final List<LogEventSink> sinks; private final List<LogEventEnricher> enrichers; private final MessageTemplateParser templateParser; private final LogEventPropertyFactory propertyFactory; public SerilogCoreLogger(String sourceContext, LogLevel minimumLevel, List<LogEventSink> sinks, List<LogEventEnricher> enrichers) { this.sourceContext = sourceContext; this.minimumLevel = minimumLevel; this.sinks = new ArrayList<>(sinks); this.enrichers = new ArrayList<>(enrichers); this.templateParser = new MessageTemplateParser(); this.propertyFactory = new LogEventPropertyFactory(); } @Override public boolean isEnabled(LogLevel level) { return level.isEnabled(minimumLevel); } @Override public void write(LogLevel level, String messageTemplate, Object... propertyValues) { write(level, null, messageTemplate, propertyValues); } @Override public void write(LogLevel level, Throwable exception, String messageTemplate, Object... propertyValues) { if (!isEnabled(level)) { return; } try { LogEvent logEvent = createLogEvent(level, messageTemplate, exception, propertyValues); emit(logEvent); } catch (Exception e) { // Don't let logging exceptions break the application System.err.println("Logging failed: " + e.getMessage()); } } private LogEvent createLogEvent(LogLevel level, String messageTemplate, Throwable exception, Object[] propertyValues) { MessageTemplate template = templateParser.parse(messageTemplate); Map<String, Object> properties = propertyFactory.createProperties(template, propertyValues); // Render the message String renderedMessage = template.render(properties); LogEvent.Builder builder = new LogEvent.Builder() .level(level) .messageTemplate(messageTemplate) .renderedMessage(renderedMessage) .properties(properties) .exception(exception) .context("SourceContext", sourceContext); // Apply enrichers for (LogEventEnricher enricher : enrichers) { enricher.enrich(builder); } return builder.build(); } private void emit(LogEvent logEvent) { for (LogEventSink sink : sinks) { try { sink.emit(logEvent); } catch (Exception e) { System.err.println("Sink failed: " + e.getMessage()); } } } @Override public LogEventBuilder forContext(String propertyName, Object propertyValue) { return new ContextualLogEventBuilder(this, LogLevel.INFORMATION) .property(propertyName, propertyValue); } @Override public LogEventBuilder forContext(Class<?> sourceType) { return forContext("SourceContext", sourceType.getName()); } @Override public LogEventBuilder verbose() { return new ContextualLogEventBuilder(this, LogLevel.VERBOSE); } @Override public LogEventBuilder debug() { return new ContextualLogEventBuilder(this, LogLevel.DEBUG); } @Override public LogEventBuilder information() { return new ContextualLogEventBuilder(this, LogLevel.INFORMATION); } @Override public LogEventBuilder warning() { return new ContextualLogEventBuilder(this, LogLevel.WARNING); } @Override public LogEventBuilder error() { return new ContextualLogEventBuilder(this, LogLevel.ERROR); } @Override public LogEventBuilder fatal() { return new ContextualLogEventBuilder(this, LogLevel.FATAL); } @Override public void close() { for (LogEventSink sink : sinks) { try { sink.close(); } catch (Exception e) { System.err.println("Error closing sink: " + e.getMessage()); } } } } ContextualLogEventBuilder.java - Context-aware event builder
import java.util.HashMap; import java.util.Map; public class ContextualLogEventBuilder extends LogEventBuilder { private final SerilogLogger logger; public ContextualLogEventBuilder(SerilogLogger logger, LogLevel level) { super(logger, level); this.logger = logger; } @Override public LogEventBuilder property(String name, Object value) { super.property(name, value); return this; } @Override public LogEventBuilder withException(Throwable exception) { super.withException(exception); return this; } public SerilogLogger asLogger() { return new ContextualLogger(logger, getProperties()); } // Internal contextual logger private static class ContextualLogger implements SerilogLogger { private final SerilogLogger innerLogger; private final Map<String, Object> contextProperties; public ContextualLogger(SerilogLogger innerLogger, Map<String, Object> contextProperties) { this.innerLogger = innerLogger; this.contextProperties = new HashMap<>(contextProperties); } @Override public boolean isEnabled(LogLevel level) { return innerLogger.isEnabled(level); } @Override public void write(LogLevel level, String messageTemplate, Object... propertyValues) { write(level, null, messageTemplate, propertyValues); } @Override public void write(LogLevel level, Throwable exception, String messageTemplate, Object... propertyValues) { LogEventBuilder builder = createBuilder(level).withException(exception); builder.log(messageTemplate, propertyValues); } @Override public LogEventBuilder forContext(String propertyName, Object propertyValue) { Map<String, Object> newContext = new HashMap<>(contextProperties); newContext.put(propertyName, propertyValue); return new ContextualLogEventBuilder(this, LogLevel.INFORMATION) .properties(newContext); } @Override public LogEventBuilder forContext(Class<?> sourceType) { return forContext("SourceContext", sourceType.getName()); } @Override public LogEventBuilder verbose() { return createBuilder(LogLevel.VERBOSE); } @Override public LogEventBuilder debug() { return createBuilder(LogLevel.DEBUG); } @Override public LogEventBuilder information() { return createBuilder(LogLevel.INFORMATION); } @Override public LogEventBuilder warning() { return createBuilder(LogLevel.WARNING); } @Override public LogEventBuilder error() { return createBuilder(LogLevel.ERROR); } @Override public LogEventBuilder fatal() { return createBuilder(LogLevel.FATAL); } private LogEventBuilder createBuilder(LogLevel level) { ContextualLogEventBuilder builder = new ContextualLogEventBuilder(this, level); builder.properties(contextProperties); return builder; } @Override public void close() { innerLogger.close(); } } } 4. Sinks System
LogEventSink.java - Base interface for log sinks
public interface LogEventSink extends AutoCloseable { void emit(LogEvent logEvent); void close(); } ConsoleSink.java - Colored console output
import java.time.format.DateTimeFormatter; import java.util.Map; public class ConsoleSink implements LogEventSink { private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); private final boolean useColor; public ConsoleSink() { this(true); } public ConsoleSink(boolean useColor) { this.useColor = useColor; } @Override public void emit(LogEvent logEvent) { String formatted = formatLogEvent(logEvent); System.out.println(formatted); } private String formatLogEvent(LogEvent logEvent) { StringBuilder sb = new StringBuilder(); // Timestamp sb.append(logEvent.getTimestamp().atZone(java.time.ZoneId.systemDefault()).format(formatter)); sb.append(" "); // Level with color if (useColor) { sb.append(getLevelWithColor(logEvent.getLevel())); } else { sb.append(String.format("[%-5s]", logEvent.getLevel().toString().substring(0, 4))); } sb.append(" "); // Source context String sourceContext = (String) logEvent.getContextValue("SourceContext").orElse(""); if (!sourceContext.isEmpty()) { sb.append("[").append(sourceContext).append("] "); } // Message sb.append(logEvent.getRenderedMessage()); // Properties (if any) if (!logEvent.getProperties().isEmpty()) { sb.append(" {"); boolean first = true; for (Map.Entry<String, Object> entry : logEvent.getProperties().entrySet()) { if (!first) sb.append(", "); sb.append(entry.getKey()).append(": ").append(formatValue(entry.getValue())); first = false; } sb.append("}"); } // Exception if (logEvent.getException() != null) { sb.append("\n").append(formatException(logEvent.getException())); } return sb.toString(); } private String getLevelWithColor(LogLevel level) { String colorCode = switch (level) { case VERBOSE -> "\u001B[90m"; // Gray case DEBUG -> "\u001B[36m"; // Cyan case INFORMATION -> "\u001B[32m"; // Green case WARNING -> "\u001B[33m"; // Yellow case ERROR -> "\u001B[31m"; // Red case FATAL -> "\u001B[35m"; // Magenta }; String reset = "\u001B[0m"; String levelStr = String.format("[%-5s]", level.toString().substring(0, 4)); return colorCode + levelStr + reset; } private String formatValue(Object value) { if (value == null) return "null"; if (value instanceof String) return "\"" + value + "\""; return value.toString(); } private String formatException(Throwable exception) { StringBuilder sb = new StringBuilder(); sb.append(exception.toString()).append("\n"); for (StackTraceElement element : exception.getStackTrace()) { sb.append(" at ").append(element).append("\n"); } return sb.toString(); } @Override public void close() { // Console doesn't need cleanup } } FileSink.java - JSON file logging
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class FileSink implements LogEventSink { private final Path logDirectory; private final String logFilePattern; private final boolean useJson; private final ObjectMapper objectMapper; private final Map<String, BufferedWriter> writers; public FileSink(Path logDirectory, String logFilePattern, boolean useJson) { this.logDirectory = logDirectory; this.logFilePattern = logFilePattern; this.useJson = useJson; this.objectMapper = new ObjectMapper(); this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); this.writers = new ConcurrentHashMap<>(); try { Files.createDirectories(logDirectory); } catch (IOException e) { throw new RuntimeException("Failed to create log directory", e); } } @Override public void emit(LogEvent logEvent) { try { String logFileName = resolveLogFileName(logEvent); BufferedWriter writer = writers.computeIfAbsent(logFileName, this::createWriter); String logEntry = useJson ? formatAsJson(logEvent) : formatAsText(logEvent); writer.write(logEntry); writer.newLine(); writer.flush(); } catch (IOException e) { System.err.println("Failed to write log entry: " + e.getMessage()); } } private String resolveLogFileName(LogEvent logEvent) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String date = logEvent.getTimestamp().atZone(java.time.ZoneId.systemDefault()).format(formatter); return logFilePattern.replace("{date}", date); } private BufferedWriter createWriter(String fileName) { try { Path logFile = logDirectory.resolve(fileName); return Files.newBufferedWriter(logFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE); } catch (IOException e) { throw new RuntimeException("Failed to create log file writer", e); } } private String formatAsJson(LogEvent logEvent) throws IOException { Map<String, Object> logEntry = Map.of( "timestamp", logEvent.getTimestamp().toString(), "level", logEvent.getLevel().toString(), "messageTemplate", logEvent.getMessageTemplate(), "message", logEvent.getRenderedMessage(), "properties", logEvent.getProperties(), "context", logEvent.getContext(), "exception", logEvent.getException() != null ? logEvent.getException().toString() : null ); return objectMapper.writeValueAsString(logEntry); } private String formatAsText(LogEvent logEvent) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); String timestamp = logEvent.getTimestamp().atZone(java.time.ZoneId.systemDefault()).format(formatter); return String.format("%s [%s] %s %s", timestamp, logEvent.getLevel(), logEvent.getRenderedMessage(), logEvent.getProperties().isEmpty() ? "" : logEvent.getProperties() ); } @Override public void close() { for (BufferedWriter writer : writers.values()) { try { writer.close(); } catch (IOException e) { System.err.println("Failed to close log writer: " + e.getMessage()); } } writers.clear(); } } ElasticsearchSink.java - Elasticsearch logging sink
import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class ElasticsearchSink implements LogEventSink { private final String elasticsearchUrl; private final String indexName; private final ObjectMapper objectMapper; private final CloseableHttpClient httpClient; private final BlockingQueue<LogEvent> queue; private final int batchSize; private final Thread workerThread; private volatile boolean running; public ElasticsearchSink(String elasticsearchUrl, String indexName, int batchSize) { this.elasticsearchUrl = elasticsearchUrl; this.indexName = indexName; this.batchSize = batchSize; this.objectMapper = new ObjectMapper(); this.httpClient = HttpClients.createDefault(); this.queue = new LinkedBlockingQueue<>(10000); this.running = true; this.workerThread = new Thread(this::processQueue, "elasticsearch-sink-worker"); this.workerThread.setDaemon(true); this.workerThread.start(); } @Override public void emit(LogEvent logEvent) { if (!queue.offer(logEvent)) { System.err.println("Elasticsearch sink queue is full, dropping log event"); } } private void processQueue() { List<LogEvent> batch = new ArrayList<>(batchSize); while (running || !queue.isEmpty()) { try { LogEvent event = queue.poll(100, TimeUnit.MILLISECONDS); if (event != null) { batch.add(event); if (batch.size() >= batchSize) { sendBatch(batch); batch.clear(); } } // Send remaining events periodically if (!batch.isEmpty() && (queue.isEmpty() || batch.size() >= batchSize / 2)) { sendBatch(batch); batch.clear(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } // Final batch if (!batch.isEmpty()) { sendBatch(batch); } } private void sendBatch(List<LogEvent> batch) { if (batch.isEmpty()) return; try { String bulkPayload = createBulkPayload(batch); HttpPost request = new HttpPost(elasticsearchUrl + "/_bulk"); request.setEntity(new StringEntity(bulkPayload, "application/json")); // In production, you'd want to handle the response and retry on failure httpClient.execute(request); } catch (Exception e) { System.err.println("Failed to send batch to Elasticsearch: " + e.getMessage()); } } private String createBulkPayload(List<LogEvent> batch) throws IOException { StringBuilder sb = new StringBuilder(); for (LogEvent event : batch) { // Index action Map<String, Object> indexAction = Map.of( "index", Map.of( "_index", indexName, "_id", generateDocumentId(event) ) ); // Document Map<String, Object> document = new HashMap<>(); document.put("@timestamp", event.getTimestamp()); document.put("level", event.getLevel().toString()); document.put("message", event.getRenderedMessage()); document.put("messageTemplate", event.getMessageTemplate()); document.put("properties", event.getProperties()); document.put("context", event.getContext()); if (event.getException() != null) { document.put("exception", event.getException().toString()); } sb.append(objectMapper.writeValueAsString(indexAction)).append("\n"); sb.append(objectMapper.writeValueAsString(document)).append("\n"); } return sb.toString(); } private String generateDocumentId(LogEvent event) { return event.getTimestamp().toEpochMilli() + "-" + event.getLevel() + "-" + System.currentTimeMillis(); } @Override public void close() { running = false; try { workerThread.join(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } try { httpClient.close(); } catch (IOException e) { System.err.println("Error closing HTTP client: " + e.getMessage()); } } } 5. Enrichers System
LogEventEnricher.java - Interface for log event enrichers
public interface LogEventEnricher { void enrich(LogEvent.Builder builder); } EnvironmentEnricher.java - Adds environment information
import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Map; public class EnvironmentEnricher implements LogEventEnricher { private final String hostName; private final String environment; public EnvironmentEnricher(String environment) { this.environment = environment; this.hostName = getHostName(); } @Override public void enrich(LogEvent.Builder builder) { builder.context("Environment", environment) .context("MachineName", hostName) .context("ProcessId", getProcessId()); } private String getHostName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return "unknown"; } } private long getProcessId() { return ProcessHandle.current().pid(); } } ThreadEnricher.java - Adds thread information
public class ThreadEnricher implements LogEventEnricher { @Override public void enrich(LogEvent.Builder builder) { Thread currentThread = Thread.currentThread(); builder.context("ThreadId", currentThread.threadId()) .context("ThreadName", currentThread.getName()); } } 6. Configuration and Factory
LoggerConfiguration.java - Fluent configuration API
import java.nio.file.Path; import java.util.ArrayList; import java.util.List; public class LoggerConfiguration { private LogLevel minimumLevel = LogLevel.INFORMATION; private final List<LogEventSink> sinks = new ArrayList<>(); private final List<LogEventEnricher> enrichers = new ArrayList<>(); public LoggerConfiguration minimumLevel(LogLevel level) { this.minimumLevel = level; return this; } public LoggerConfiguration writeTo(LogEventSink sink) { this.sinks.add(sink); return this; } public LoggerConfiguration writeToConsole() { return writeTo(new ConsoleSink()); } public LoggerConfiguration writeToFile(Path directory, String filePattern) { return writeTo(new FileSink(directory, filePattern, true)); } public LoggerConfiguration writeToElasticsearch(String url, String indexName) { return writeTo(new ElasticsearchSink(url, indexName, 100)); } public LoggerConfiguration enrichWith(LogEventEnricher enricher) { this.enrichers.add(enricher); return this; } public LoggerConfiguration enrichWithThread() { return enrichWith(new ThreadEnricher()); } public LoggerConfiguration enrichWithEnvironment(String environment) { return enrichWith(new EnvironmentEnricher(environment)); } public SerilogLogger createLogger() { return createLogger("Default"); } public SerilogLogger createLogger(Class<?> sourceType) { return createLogger(sourceType.getName()); } public SerilogLogger createLogger(String sourceContext) { return new SerilogCoreLogger(sourceContext, minimumLevel, sinks, enrichers); } } Serilog.java - Main entry point (similar to Serilog's Log class)
public final class Serilog { private static SerilogLogger defaultLogger; static { // Initialize with default configuration defaultLogger = new LoggerConfiguration() .writeToConsole() .enrichWithThread() .enrichWithEnvironment("Development") .createLogger(); } private Serilog() { // Static utility class } public static LoggerConfiguration configure() { return new LoggerConfiguration(); } public static SerilogLogger getLogger() { return defaultLogger; } public static SerilogLogger getLogger(Class<?> sourceType) { return defaultLogger.forContext(sourceType).asLogger(); } public static void close() { if (defaultLogger != null) { defaultLogger.close(); defaultLogger = null; } } // Static convenience methods public static void verbose(String messageTemplate, Object... propertyValues) { defaultLogger.verbose().log(messageTemplate, propertyValues); } public static void debug(String messageTemplate, Object... propertyValues) { defaultLogger.debug().log(messageTemplate, propertyValues); } public static void information(String messageTemplate, Object... propertyValues) { defaultLogger.information().log(messageTemplate, propertyValues); } public static void warning(String messageTemplate, Object... propertyValues) { defaultLogger.warning().log(messageTemplate, propertyValues); } public static void error(String messageTemplate, Object... propertyValues) { defaultLogger.error().log(messageTemplate, propertyValues); } public static void error(Throwable exception, String messageTemplate, Object... propertyValues) { defaultLogger.error().withException(exception).log(messageTemplate, propertyValues); } public static void fatal(String messageTemplate, Object... propertyValues) { defaultLogger.fatal().log(messageTemplate, propertyValues); } } 7. Support Classes
MessageTemplateParser.java - Enhanced template parsing
import java.util.Map; import java.util.HashMap; public class MessageTemplateParser { public MessageTemplate parse(String messageTemplate) { return new MessageTemplate(messageTemplate); } } LogEventPropertyFactory.java - Creates properties from template and values
import java.util.HashMap; import java.util.Map; public class LogEventPropertyFactory { public Map<String, Object> createProperties(MessageTemplate template, Object[] propertyValues) { Map<String, Object> properties = new HashMap<>(); var propertyNames = template.getPropertyNames(); for (int i = 0; i < Math.min(propertyValues.length, propertyNames.size()); i++) { properties.put(propertyNames.get(i), propertyValues[i]); } return properties; } } 8. Demonstration and Usage
SerilogDemo.java - Complete demonstration
import java.nio.file.Paths; public class SerilogDemo { public static void main(String[] args) { try { // Example 1: Basic static usage basicStaticUsage(); // Example 2: Fluent configuration fluentConfiguration(); // Example 3: Structured logging with properties structuredLogging(); // Example 4: Contextual logging contextualLogging(); } finally { // Clean up Serilog.close(); } } private static void basicStaticUsage() { System.out.println("=== Basic Static Usage ==="); Serilog.verbose("This is a verbose message"); Serilog.debug("Debugging process {ProcessName}", "DataProcessor"); Serilog.information("Application started at {StartTime}", java.time.Instant.now()); Serilog.warning("Low disk space: {FreeSpace} MB", 125); Serilog.error("Failed to process user {UserId}", 12345); try { throw new RuntimeException("Something went wrong!"); } catch (Exception e) { Serilog.error(e, "Error processing request {RequestId}", "req-123"); } } private static void fluentConfiguration() { System.out.println("\n=== Fluent Configuration ==="); SerilogLogger logger = Serilog.configure() .minimumLevel(LogLevel.DEBUG) .writeToConsole() .writeToFile(Paths.get("logs"), "app-{date}.log.json") .enrichWithThread() .enrichWithEnvironment("Production") .createLogger(SerilogDemo.class); logger.debug("Configured logger with file and console sinks"); logger.information("System configuration loaded: {ConfigCount} items", 42); // Create a contextual logger SerilogLogger requestLogger = logger.forContext("RequestId", "req-789").asLogger(); requestLogger.information("Processing request"); requestLogger.information("Request completed in {Duration}ms", 150); } private static void structuredLogging() { System.out.println("\n=== Structured Logging ==="); SerilogLogger logger = Serilog.getLogger(SerilogDemo.class); // Log with complex objects User user = new User(123, "[email protected]", "John Doe"); Order order = new Order("ORD-001", 99.99, "USD"); logger.information("User {User} placed order {Order}", user, order); // Log with collections logger.debug("Processing items: {Items}", new String[]{"item1", "item2", "item3"}); } private static void contextualLogging() { System.out.println("\n=== Contextual Logging ==="); SerilogLogger baseLogger = Serilog.getLogger(SerilogDemo.class); // Create contextual loggers SerilogLogger apiLogger = baseLogger .forContext("Component", "API") .forContext("Version", "1.0") .asLogger(); SerilogLogger dbLogger = baseLogger .forContext("Component", "Database") .asLogger(); // Use contextual loggers apiLogger.information("Received request {Method} {Path}", "GET", "/api/users"); dbLogger.debug("Executing query: {Query}", "SELECT * FROM users WHERE active = true"); apiLogger.information("Response sent with status {StatusCode}", 200); } // Demo data classes static class User { private final int id; private final String email; private final String name; public User(int id, String email, String name) { this.id = id; this.email = email; this.name = name; } @Override public String toString() { return String.format("User{id=%d, name='%s', email='%s'}", id, name, email); } } static class Order { private final String orderId; private final double amount; private final String currency; public Order(String orderId, double amount, String currency) { this.orderId = orderId; this.amount = amount; this.currency = currency; } @Override public String toString() { return String.format("Order{id='%s', amount=%.2f %s}", orderId, amount, currency); } } } Configuration Examples
application.yml
logging: serilog: minimumLevel: "Information" sinks: console: enabled: true useColor: true file: enabled: true path: "./logs" pattern: "app-{date}.log.json" useJson: true elasticsearch: enabled: false url: "http://localhost:9200" index: "app-logs" enrichers: - "thread" - "environment:Production" Key Serilog Features Implemented
- Structured Logging: Properties are first-class citizens
- Message Templates:
{Property}syntax with formatting - Fluent Configuration: Clean, readable setup
- Multiple Sinks: Console, file, Elasticsearch
- Enrichers: Add context to all log events
- Contextual Logging: Create loggers with fixed properties
- Performance: Async processing where appropriate
- JSON Output: Structured log format
This implementation brings the power and elegance of Serilog's structured logging to Java, providing a much more expressive and flexible logging experience compared to traditional Java logging frameworks.