google-site-verification: google61fe8ba583a51912.html
Structured Logging with JSON in Java

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

  1. Machine Readable: Easy to parse and process by log management systems
  2. Rich Context: Include structured data with each log entry
  3. Better Searchability: Easy to search and filter logs by specific fields
  4. Performance Monitoring: Built-in support for timing and metrics
  5. Correlation: Track requests across distributed systems
  6. 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.

Leave a Reply

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


Macro Nepal Helper