Introduction to Structured Logging
Structured logging involves outputting logs in a structured format (like JSON) rather than plain text. This makes logs easier to parse, search, and analyze using log management systems.
Core Implementation
Basic JSON Logger Interface
package com.structuredlogging;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.time.Instant;
import java.util.*;
public interface StructuredLogger {
void log(String level, String message, Map<String, Object> context);
void debug(String message, Map<String, Object> context);
void info(String message, Map<String, Object> context);
void warn(String message, Map<String, Object> context);
void error(String message, Map<String, Object> context);
void error(String message, Throwable throwable, Map<String, Object> context);
}
JSON Logger Implementation
package com.structuredlogging;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.io.PrintStream;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class JsonLogger implements StructuredLogger {
private final String loggerName;
private final PrintStream output;
private final ObjectMapper objectMapper;
private final Map<String, Object> globalContext;
private final boolean prettyPrint;
public JsonLogger(String loggerName) {
this(loggerName, System.out, false, new HashMap<>());
}
public JsonLogger(String loggerName, PrintStream output, boolean prettyPrint,
Map<String, Object> globalContext) {
this.loggerName = loggerName;
this.output = output;
this.prettyPrint = prettyPrint;
this.globalContext = new ConcurrentHashMap<>(globalContext);
this.objectMapper = new ObjectMapper();
if (prettyPrint) {
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
}
@Override
public void log(String level, String message, Map<String, Object> context) {
try {
Map<String, Object> logEntry = createLogEntry(level, message, context);
String json = objectMapper.writeValueAsString(logEntry);
output.println(json);
} catch (JsonProcessingException e) {
// Fallback to plain text if JSON serialization fails
output.printf("{\"error\": \"Failed to serialize log entry: %s\"}%n", e.getMessage());
}
}
@Override
public void debug(String message, Map<String, Object> context) {
log("DEBUG", message, context);
}
@Override
public void info(String message, Map<String, Object> context) {
log("INFO", message, context);
}
@Override
public void warn(String message, Map<String, Object> context) {
log("WARN", message, context);
}
@Override
public void error(String message, Map<String, Object> context) {
log("ERROR", message, context);
}
@Override
public void error(String message, Throwable throwable, Map<String, Object> context) {
Map<String, Object> errorContext = new HashMap<>(context != null ? context : new HashMap<>());
errorContext.put("exception", createExceptionInfo(throwable));
log("ERROR", message, errorContext);
}
private Map<String, Object> createLogEntry(String level, String message,
Map<String, Object> context) {
Map<String, Object> logEntry = new LinkedHashMap<>();
// Standard fields
logEntry.put("timestamp", Instant.now().toString());
logEntry.put("level", level);
logEntry.put("logger", loggerName);
logEntry.put("message", message);
logEntry.put("thread", Thread.currentThread().getName());
// Add global context
logEntry.putAll(globalContext);
// Add specific context
if (context != null) {
logEntry.putAll(context);
}
return logEntry;
}
private Map<String, Object> createExceptionInfo(Throwable throwable) {
Map<String, Object> exceptionInfo = new LinkedHashMap<>();
exceptionInfo.put("type", throwable.getClass().getName());
exceptionInfo.put("message", throwable.getMessage());
exceptionInfo.put("stackTrace", getStackTrace(throwable));
if (throwable.getCause() != null) {
exceptionInfo.put("cause", createExceptionInfo(throwable.getCause()));
}
return exceptionInfo;
}
private List<String> getStackTrace(Throwable throwable) {
List<String> stackTrace = new ArrayList<>();
for (StackTraceElement element : throwable.getStackTrace()) {
stackTrace.add(element.toString());
}
return stackTrace;
}
// Builder for fluent configuration
public static class Builder {
private String loggerName;
private PrintStream output = System.out;
private boolean prettyPrint = false;
private Map<String, Object> globalContext = new HashMap<>();
public Builder(String loggerName) {
this.loggerName = loggerName;
}
public Builder output(PrintStream output) {
this.output = output;
return this;
}
public Builder prettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
return this;
}
public Builder addGlobalContext(String key, Object value) {
this.globalContext.put(key, value);
return this;
}
public Builder globalContext(Map<String, Object> context) {
this.globalContext.putAll(context);
return this;
}
public JsonLogger build() {
return new JsonLogger(loggerName, output, prettyPrint, globalContext);
}
}
// Utility methods for common context patterns
public Map<String, Object> context(String key, Object value) {
return Collections.singletonMap(key, value);
}
public Map<String, Object> context(String k1, Object v1, String k2, Object v2) {
Map<String, Object> context = new HashMap<>();
context.put(k1, v1);
context.put(k2, v2);
return context;
}
public Map<String, Object> context(String k1, Object v1, String k2, Object v2,
String k3, Object v3) {
Map<String, Object> context = new HashMap<>();
context.put(k1, v1);
context.put(k2, v2);
context.put(k3, v3);
return context;
}
public Map<String, Object> context(Map<String, Object> base, String key, Object value) {
Map<String, Object> context = new HashMap<>(base);
context.put(key, value);
return context;
}
}
Advanced Structured Logger with SLF4J Integration
SLF4J JSON Appender
package com.structuredlogging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class Slf4jJsonLogger {
private final Logger slf4jLogger;
private final ObjectMapper objectMapper;
private final Map<String, Object> staticFields;
private final boolean includeMDC;
private final String loggerName;
public Slf4jJsonLogger(Class<?> clazz) {
this(clazz, new ObjectMapper(), new HashMap<>(), true);
}
public Slf4jJsonLogger(Class<?> clazz, ObjectMapper objectMapper,
Map<String, Object> staticFields, boolean includeMDC) {
this.slf4jLogger = LoggerFactory.getLogger(clazz);
this.objectMapper = objectMapper;
this.staticFields = new ConcurrentHashMap<>(staticFields);
this.includeMDC = includeMDC;
this.loggerName = clazz.getName();
// Configure object mapper for pretty printing in development
if (Boolean.getBoolean("dev.mode")) {
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
}
public void debug(String message) {
debug(message, null);
}
public void debug(String message, Map<String, Object> context) {
if (slf4jLogger.isDebugEnabled()) {
log("DEBUG", message, null, context);
}
}
public void info(String message) {
info(message, null);
}
public void info(String message, Map<String, Object> context) {
if (slf4jLogger.isInfoEnabled()) {
log("INFO", message, null, context);
}
}
public void warn(String message) {
warn(message, null, null);
}
public void warn(String message, Map<String, Object> context) {
warn(message, null, context);
}
public void warn(String message, Throwable throwable) {
warn(message, throwable, null);
}
public void warn(String message, Throwable throwable, Map<String, Object> context) {
if (slf4jLogger.isWarnEnabled()) {
log("WARN", message, throwable, context);
}
}
public void error(String message) {
error(message, null, null);
}
public void error(String message, Map<String, Object> context) {
error(message, null, context);
}
public void error(String message, Throwable throwable) {
error(message, throwable, null);
}
public void error(String message, Throwable throwable, Map<String, Object> context) {
if (slf4jLogger.isErrorEnabled()) {
log("ERROR", message, throwable, context);
}
}
private void log(String level, String message, Throwable throwable,
Map<String, Object> context) {
try {
String jsonLog = createJsonLog(level, message, throwable, context);
writeLog(level, jsonLog, throwable);
} catch (Exception e) {
// Fallback to traditional logging
slf4jLogger.error("Failed to create JSON log entry", e);
traditionalLog(level, message, throwable);
}
}
private String createJsonLog(String level, String message, Throwable throwable,
Map<String, Object> context) throws Exception {
Map<String, Object> logEntry = new LinkedHashMap<>();
// Standard fields
logEntry.put("timestamp", Instant.now().toString());
logEntry.put("level", level);
logEntry.put("logger", loggerName);
logEntry.put("message", message);
logEntry.put("thread", Thread.currentThread().getName());
// Add static fields
logEntry.putAll(staticFields);
// Add MDC context
if (includeMDC) {
Map<String, String> mdc = MDC.getCopyOfContextMap();
if (mdc != null && !mdc.isEmpty()) {
logEntry.put("mdc", mdc);
}
}
// Add specific context
if (context != null && !context.isEmpty()) {
logEntry.putAll(context);
}
// Add exception information
if (throwable != null) {
logEntry.put("exception", createExceptionData(throwable));
}
return objectMapper.writeValueAsString(logEntry);
}
private Map<String, Object> createExceptionData(Throwable throwable) {
Map<String, Object> exceptionData = new LinkedHashMap<>();
exceptionData.put("type", throwable.getClass().getName());
exceptionData.put("message", throwable.getMessage());
exceptionData.put("stackTrace", getStackTraceAsString(throwable));
if (throwable.getCause() != null) {
exceptionData.put("cause", createExceptionData(throwable.getCause()));
}
return exceptionData;
}
private String getStackTraceAsString(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
private void writeLog(String level, String jsonLog, Throwable throwable) {
switch (level) {
case "DEBUG":
slf4jLogger.debug(jsonLog);
break;
case "INFO":
slf4jLogger.info(jsonLog);
break;
case "WARN":
slf4jLogger.warn(jsonLog);
break;
case "ERROR":
if (throwable != null) {
slf4jLogger.error(jsonLog, throwable);
} else {
slf4jLogger.error(jsonLog);
}
break;
default:
slf4jLogger.info(jsonLog);
}
}
private void traditionalLog(String level, String message, Throwable throwable) {
switch (level) {
case "DEBUG":
slf4jLogger.debug(message, throwable);
break;
case "INFO":
slf4jLogger.info(message, throwable);
break;
case "WARN":
slf4jLogger.warn(message, throwable);
break;
case "ERROR":
slf4jLogger.error(message, throwable);
break;
}
}
// Fluent API for context building
public LogEvent withContext(String key, Object value) {
return new LogEvent(this).withContext(key, value);
}
public LogEvent withContext(Map<String, Object> context) {
return new LogEvent(this).withContext(context);
}
// Static field management
public void addStaticField(String key, Object value) {
staticFields.put(key, value);
}
public void removeStaticField(String key) {
staticFields.remove(key);
}
// Fluent logging event class
public static class LogEvent {
private final Slf4jJsonLogger logger;
private final Map<String, Object> context = new HashMap<>();
public LogEvent(Slf4jJsonLogger logger) {
this.logger = logger;
}
public LogEvent withContext(String key, Object value) {
context.put(key, value);
return this;
}
public LogEvent withContext(Map<String, Object> additionalContext) {
if (additionalContext != null) {
context.putAll(additionalContext);
}
return this;
}
public void debug(String message) {
logger.debug(message, context);
}
public void info(String message) {
logger.info(message, context);
}
public void warn(String message) {
logger.warn(message, null, context);
}
public void warn(String message, Throwable throwable) {
logger.warn(message, throwable, context);
}
public void error(String message) {
logger.error(message, null, context);
}
public void error(String message, Throwable throwable) {
logger.error(message, throwable, context);
}
}
}
Logback JSON Configuration
logback-json.xml Configuration
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- JSON Layout -->
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampPattern>
<customFields>{"app":"my-application","environment":"${ENVIRONMENT:-development}"}</customFields>
<!-- Standard field names -->
<fieldNames>
<timestamp>timestamp</timestamp>
<message>message</message>
<logger>logger</logger>
<level>level</level>
<thread>thread</thread>
<stackTrace>stack_trace</stackTrace>
<mdc>mdc</mdc>
</fieldNames>
<!-- Include MDC -->
<includeMdc>true</includeMdc>
<!-- Include context -->
<includeContext>true</includeContext>
<!-- Stack trace configuration -->
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<maxLength>2048</maxLength>
<shortenedClassNameLength>20</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</encoder>
</appender>
<!-- Async JSON File Appender -->
<appender name="JSON_FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="JSON_FILE" />
<queueSize>10000</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>false</includeCallerData>
</appender>
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.json</file>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace>
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>50</maxDepthPerThrowable>
</throwableConverter>
</stackTrace>
<pattern>
<pattern>
{
"thread": "%thread",
"app": "my-application",
"environment": "${ENVIRONMENT:-development}"
}
</pattern>
</pattern>
</providers>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="JSON_CONSOLE" />
<appender-ref ref="JSON_FILE_ASYNC" />
</root>
<!-- Application-specific logger -->
<logger name="com.myapp" level="DEBUG" additivity="false">
<appender-ref ref="JSON_CONSOLE" />
<appender-ref ref="JSON_FILE_ASYNC" />
</logger>
</configuration>
Context Management and MDC Integration
Structured MDC Context Manager
package com.structuredlogging;
import org.slf4j.MDC;
import java.util.*;
import java.util.function.Supplier;
public class LoggingContext {
private static final ThreadLocal<Deque<Map<String, String>>> contextStack =
ThreadLocal.withInitial(ArrayDeque::new);
public static void pushContext(String key, String value) {
Map<String, String> context = new HashMap<>();
context.put(key, value);
pushContext(context);
}
public static void pushContext(Map<String, String> context) {
Deque<Map<String, String>> stack = contextStack.get();
Map<String, String> currentContext = stack.isEmpty() ? new HashMap<>() :
new HashMap<>(stack.peek());
currentContext.putAll(context);
stack.push(currentContext);
// Update MDC
MDC.setContextMap(currentContext);
}
public static void popContext() {
Deque<Map<String, String>> stack = contextStack.get();
if (!stack.isEmpty()) {
stack.pop();
// Restore previous context
Map<String, String> previousContext = stack.isEmpty() ?
Collections.emptyMap() : stack.peek();
MDC.setContextMap(previousContext);
}
}
public static void clearContext() {
Deque<Map<String, String>> stack = contextStack.get();
stack.clear();
MDC.clear();
}
public static Map<String, String> getCurrentContext() {
Deque<Map<String, String>> stack = contextStack.get();
return stack.isEmpty() ? Collections.emptyMap() :
Collections.unmodifiableMap(stack.peek());
}
public static void withContext(String key, String value, Runnable action) {
pushContext(key, value);
try {
action.run();
} finally {
popContext();
}
}
public static void withContext(Map<String, String> context, Runnable action) {
pushContext(context);
try {
action.run();
} finally {
popContext();
}
}
public static <T> T withContext(String key, String value, Supplier<T> action) {
pushContext(key, value);
try {
return action.get();
} finally {
popContext();
}
}
public static <T> T withContext(Map<String, String> context, Supplier<T> action) {
pushContext(context);
try {
return action.get();
} finally {
popContext();
}
}
// Common context keys
public static class Keys {
public static final String CORRELATION_ID = "correlationId";
public static final String USER_ID = "userId";
public static final String SESSION_ID = "sessionId";
public static final String REQUEST_ID = "requestId";
public static final String CLIENT_IP = "clientIp";
public static final String USER_AGENT = "userAgent";
public static final String ENDPOINT = "endpoint";
public static final String HTTP_METHOD = "httpMethod";
public static final String RESPONSE_STATUS = "responseStatus";
public static final String DURATION_MS = "durationMs";
}
}
Performance Monitoring with Structured Logs
Performance Logger
package com.structuredlogging;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class PerformanceLogger {
private final Slf4jJsonLogger logger;
private final Map<String, AtomicLong> operationCounters;
private final Map<String, AtomicLong> operationDurations;
public PerformanceLogger(Class<?> clazz) {
this.logger = new Slf4jJsonLogger(clazz);
this.operationCounters = new ConcurrentHashMap<>();
this.operationDurations = new ConcurrentHashMap<>();
}
public Timer startTimer(String operation) {
return new Timer(operation);
}
public void logOperation(String operation, Duration duration, boolean success) {
logOperation(operation, duration, success, null);
}
public void logOperation(String operation, Duration duration, boolean success,
Map<String, Object> context) {
// Update metrics
operationCounters.computeIfAbsent(operation, k -> new AtomicLong()).incrementAndGet();
operationDurations.computeIfAbsent(operation, k -> new AtomicLong())
.addAndGet(duration.toMillis());
// Log the operation
Map<String, Object> logContext = new HashMap<>();
if (context != null) {
logContext.putAll(context);
}
logContext.put("operation", operation);
logContext.put("durationMs", duration.toMillis());
logContext.put("success", success);
logContext.put("operationType", "performance");
String level = success ? "INFO" : "WARN";
String message = String.format("Operation '%s' completed in %d ms",
operation, duration.toMillis());
logger.log(level, message, null, logContext);
}
public void reportMetrics() {
Map<String, Object> metricsContext = new HashMap<>();
metricsContext.put("type", "metrics_report");
operationCounters.forEach((operation, counter) -> {
long count = counter.get();
long totalDuration = operationDurations.get(operation).get();
double averageDuration = (double) totalDuration / count;
Map<String, Object> operationMetrics = new HashMap<>();
operationMetrics.put("count", count);
operationMetrics.put("totalDurationMs", totalDuration);
operationMetrics.put("averageDurationMs", Math.round(averageDuration * 100) / 100.0);
metricsContext.put(operation, operationMetrics);
});
logger.info("Performance metrics report", metricsContext);
}
public class Timer {
private final String operation;
private final Instant startTime;
private Map<String, Object> context;
public Timer(String operation) {
this.operation = operation;
this.startTime = Instant.now();
this.context = new HashMap<>();
}
public Timer withContext(String key, Object value) {
context.put(key, value);
return this;
}
public Timer withContext(Map<String, Object> additionalContext) {
if (additionalContext != null) {
context.putAll(additionalContext);
}
return this;
}
public void stop(boolean success) {
Duration duration = Duration.between(startTime, Instant.now());
logOperation(operation, duration, success, context);
}
public void stopAndLogSuccess() {
stop(true);
}
public void stopAndLogFailure() {
stop(false);
}
public <T> T stopAndReturn(T result) {
stop(true);
return result;
}
}
}
Web Application Structured Logging
HTTP Request/Response Logger
package com.structuredlogging;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
public class StructuredLoggingFilter implements Filter {
private final Slf4jJsonLogger logger = new Slf4jJsonLogger(StructuredLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Generate correlation ID if not present
String correlationId = getCorrelationId(httpRequest);
// Create request context
Map<String, Object> requestContext = createRequestContext(httpRequest, correlationId);
// Push context to MDC
LoggingContext.pushContext(Collections.singletonMap("correlationId", correlationId));
Instant startTime = Instant.now();
boolean success = false;
int statusCode = 0;
try {
// Continue with the filter chain
chain.doFilter(request, response);
statusCode = httpResponse.getStatus();
success = statusCode < 400; // 2xx and 3xx are considered success
} finally {
Instant endTime = Instant.now();
long duration = Duration.between(startTime, endTime).toMillis();
// Log the request
logRequest(httpRequest, statusCode, duration, success, requestContext);
// Pop context from MDC
LoggingContext.popContext();
}
}
private String getCorrelationId(HttpServletRequest request) {
String correlationId = request.getHeader("X-Correlation-ID");
if (correlationId == null || correlationId.trim().isEmpty()) {
correlationId = UUID.randomUUID().toString();
}
return correlationId;
}
private Map<String, Object> createRequestContext(HttpServletRequest request,
String correlationId) {
Map<String, Object> context = new HashMap<>();
context.put("correlationId", correlationId);
context.put("httpMethod", request.getMethod());
context.put("requestUri", request.getRequestURI());
context.put("queryString", request.getQueryString());
context.put("clientIp", getClientIp(request));
context.put("userAgent", request.getHeader("User-Agent"));
context.put("contentType", request.getContentType());
context.put("contentLength", request.getContentLength());
// Add headers if needed (be careful with sensitive headers)
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!isSensitiveHeader(headerName)) {
headers.put(headerName, request.getHeader(headerName));
}
}
context.put("headers", headers);
return context;
}
private void logRequest(HttpServletRequest request, int statusCode, long duration,
boolean success, Map<String, Object> requestContext) {
Map<String, Object> logContext = new HashMap<>(requestContext);
logContext.put("responseStatus", statusCode);
logContext.put("durationMs", duration);
logContext.put("success", success);
logContext.put("logType", "http_request");
String message = String.format("%s %s - %d - %d ms",
request.getMethod(),
request.getRequestURI(),
statusCode,
duration);
if (success) {
logger.info(message, logContext);
} else {
logger.warn(message, logContext);
}
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
private boolean isSensitiveHeader(String headerName) {
String lowerHeader = headerName.toLowerCase();
return lowerHeader.contains("auth") ||
lowerHeader.contains("cookie") ||
lowerHeader.contains("password") ||
lowerHeader.contains("token") ||
lowerHeader.contains("secret");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization if needed
}
@Override
public void destroy() {
// Cleanup if needed
}
}
Usage Examples
Basic Usage
public class StructuredLoggingExample {
private static final JsonLogger logger = new JsonLogger.Builder("MyApp")
.addGlobalContext("app", "my-application")
.addGlobalContext("version", "1.0.0")
.addGlobalContext("environment", System.getenv("APP_ENV"))
.prettyPrint(true)
.build();
public void processOrder(Order order) {
// Simple logging with context
logger.info("Processing order",
logger.context("orderId", order.getId(), "customerId", order.getCustomerId()));
try {
// Process order logic
validateOrder(order);
processPayment(order);
updateInventory(order);
logger.info("Order processed successfully",
logger.context("orderId", order.getId(), "status", "completed"));
} catch (Exception e) {
logger.error("Failed to process order", e,
logger.context("orderId", order.getId(), "status", "failed"));
throw new RuntimeException("Order processing failed", e);
}
}
// Fluent API usage
public void fluentLoggingExample(User user) {
Slf4jJsonLogger fluentLogger = new Slf4jJsonLogger(StructuredLoggingExample.class);
fluentLogger.withContext("userId", user.getId())
.withContext("action", "profile_update")
.info("User profile updated");
}
}
Web Application Usage
@RestController
public class UserController {
private final Slf4jJsonLogger logger = new Slf4jJsonLogger(UserController.class);
private final PerformanceLogger perfLogger = new PerformanceLogger(UserController.class);
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
// The correlation ID is automatically set by the filter
return LoggingContext.withContext("userId", id, () -> {
PerformanceLogger.Timer timer = perfLogger.startTimer("get_user");
try {
logger.info("Fetching user profile");
User user = userService.findById(id);
if (user == null) {
logger.warn("User not found",
Collections.singletonMap("reason", "not_found"));
return ResponseEntity.notFound().build();
}
logger.info("User profile retrieved successfully");
return timer.stopAndReturn(ResponseEntity.ok(user));
} catch (Exception e) {
logger.error("Error fetching user", e);
timer.stop(false);
return ResponseEntity.status(500).build();
}
});
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
return LoggingContext.withContext("operation", "create_user", () -> {
Map<String, Object> context = new HashMap<>();
context.put("userEmail", user.getEmail());
context.put("userRole", user.getRole());
logger.info("Creating new user", context);
try {
User created = userService.create(user);
logger.info("User created successfully",
Collections.singletonMap("userId", created.getId()));
return ResponseEntity.ok(created);
} catch (Exception e) {
logger.error("Failed to create user", e, context);
return ResponseEntity.badRequest().build();
}
});
}
}
Database Operation Logging
@Repository
public class UserRepository {
private final Slf4jJsonLogger logger = new Slf4jJsonLogger(UserRepository.class);
private final PerformanceLogger perfLogger = new PerformanceLogger(UserRepository.class);
public User findById(String id) {
PerformanceLogger.Timer timer = perfLogger.startTimer("database_query")
.withContext("table", "users")
.withContext("operation", "select")
.withContext("userId", id);
try {
// Simulate database query
User user = executeQuery("SELECT * FROM users WHERE id = ?", id);
if (user != null) {
logger.debug("User found in database",
Collections.singletonMap("userId", id));
} else {
logger.debug("User not found in database",
Collections.singletonMap("userId", id));
}
return timer.stopAndReturn(user);
} catch (SQLException e) {
logger.error("Database query failed", e,
Collections.singletonMap("query", "SELECT * FROM users WHERE id = ?"));
timer.stop(false);
throw new DataAccessException("Query failed", e);
}
}
public void updateUser(User user) {
LoggingContext.withContext("userId", user.getId(), () -> {
PerformanceLogger.Timer timer = perfLogger.startTimer("database_update")
.withContext("table", "users")
.withContext("operation", "update");
try {
executeUpdate("UPDATE users SET name = ?, email = ? WHERE id = ?",
user.getName(), user.getEmail(), user.getId());
logger.info("User updated successfully");
timer.stopAndLogSuccess();
} catch (SQLException e) {
logger.error("Failed to update user", e);
timer.stopAndLogFailure();
throw new DataAccessException("Update failed", e);
}
});
}
}
Key Benefits of Structured JSON Logging
- Machine Readable: Easy to parse and process by log management systems
- Rich Context: Include structured data with each log entry
- Better Searchability: Easy to search and filter logs by specific fields
- Performance Monitoring: Built-in support for timing and metrics
- Correlation: Track requests across distributed systems
- Flexible: Configurable output formats and contexts
This structured logging framework provides comprehensive logging capabilities for modern Java applications with rich context and excellent integration with existing logging frameworks.