Serilog-Style Structured Logging in Java

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

  1. Structured Logging: Properties are first-class citizens
  2. Message Templates: {Property} syntax with formatting
  3. Fluent Configuration: Clean, readable setup
  4. Multiple Sinks: Console, file, Elasticsearch
  5. Enrichers: Add context to all log events
  6. Contextual Logging: Create loggers with fixed properties
  7. Performance: Async processing where appropriate
  8. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper