Introduction to Structured Logging
Structured logging involves writing logs in a structured format (like JSON) rather than plain text. This enables better parsing, filtering, and analysis by logging systems. JSON-structured logs are machine-readable and provide consistent field names and data types.
System Architecture Overview
Structured Logging Pipeline ├── Application Code │ ├ - Log Events with Context │ ├ - JSON Formatter │ └ - MDC (Mapped Diagnostic Context) ├── Logging Framework │ ├ - Logback with JSON Layout │ ├ - Log4j2 JSON Template │ └ - Custom Appenders ├── Log Processing │ ├ - Elastic Stack (ELK) │ ├ - Splunk │ ├ - Datadog │ └ - Cloud Watch └── Analysis & Visualization ├ - Kibana ├ - Grafana └ - Custom Dashboards
Core Implementation
1. Maven Dependencies
<properties> <logback.version>1.4.14</logback.version> <logstash.version>7.4</logstash.version> <slf4j.version>2.0.9</slf4j.version> <jackson.version>2.15.2</jackson.version> </properties> <dependencies> <!-- SLF4J API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <!-- Logback Classic (includes SLF4J impl) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- Logstash Logback Encoder --> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>${logstash.version}</version> </dependency> <!-- Jackson for JSON --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <!-- Spring Boot Starter (optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.7.0</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.7.0</version> <scope>test</scope> </dependency> </dependencies> 2. Logback Configuration with JSON
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="30 seconds"> <!-- JSON Layout Properties --> <property name="LOG_LEVEL" value="INFO"/> <property name="JSON_LOG_FILE" value="logs/app-json.log"/> <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/> <!-- JSON Pattern --> <property name="JSON_PATTERN" value='{ "timestamp": "%d{yyyy-MM-dd HH:mm:ss.SSS, UTC}", "level": "%level", "thread": "%thread", "logger": "%logger{40}", "message": "%message", "exception": "%exception", "traceId": "%mdc{traceId}", "spanId": "%mdc{spanId}", "userId": "%mdc{userId}", "service": "order-service", "environment": "${ENVIRONMENT:-local}", "version": "1.0.0" }'/> <!-- Console Appender (Plain Text for Development) --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> </encoder> </appender> <!-- JSON File Appender --> <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${JSON_LOG_FILE}</file> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp> <timeZone>UTC</timeZone> <fieldName>timestamp</fieldName> </timestamp> <logLevel> <fieldName>level</fieldName> </logLevel> <loggerName> <fieldName>logger</fieldName> </loggerName> <message> <fieldName>message</fieldName> </message> <mdc/> <stackTrace> <fieldName>stack_trace</fieldName> </stackTrace> <threadName> <fieldName>thread</fieldName> </threadName> <pattern> <pattern> { "service": "order-service", "environment": "${ENVIRONMENT:-local}", "version": "1.0.0" } </pattern> </pattern> </providers> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/app-json.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender> <!-- Async JSON Appender for Production --> <appender name="ASYNC_JSON" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="JSON_FILE"/> <queueSize>10000</queueSize> <discardingThreshold>0</discardingThreshold> <includeCallerData>true</includeCallerData> </appender> <!-- Root Logger --> <root level="${LOG_LEVEL}"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC_JSON"/> </root> <!-- Custom Logger for Application --> <logger name="com.example.orderservice" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC_JSON"/> </logger> <!-- Reduce Noise from Framework Logs --> <logger name="org.springframework" level="INFO"/> <logger name="org.hibernate" level="WARN"/> <logger name="com.zaxxer.hikari" level="WARN"/> </configuration> 3. Structured Logger Service
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class StructuredLogger { private static final Logger logger = LoggerFactory.getLogger(StructuredLogger.class); private final ObjectMapper objectMapper; // Log event types public enum EventType { USER_LOGIN, USER_LOGOUT, ORDER_CREATED, ORDER_UPDATED, PAYMENT_PROCESSED, INVENTORY_CHECK, SHIPPING_CREATED, SYSTEM_ERROR, BUSINESS_METRIC } public StructuredLogger(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } /** * Basic structured log with event type and message */ public void logEvent(EventType eventType, String message) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", eventType.name()); logData.put("message", message); logData.put("timestamp", System.currentTimeMillis()); logStructured("INFO", logData); } /** * Structured log with custom fields */ public void logEvent(EventType eventType, String message, Map<String, Object> customFields) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", eventType.name()); logData.put("message", message); logData.put("timestamp", System.currentTimeMillis()); logData.putAll(customFields); logStructured("INFO", logData); } /** * Business transaction log */ public void logBusinessTransaction(String transactionId, String transactionType, String status, Map<String, Object> details) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", EventType.BUSINESS_METRIC.name()); logData.put("transaction_id", transactionId); logData.put("transaction_type", transactionType); logData.put("status", status); logData.put("timestamp", System.currentTimeMillis()); logData.putAll(details); logStructured("INFO", logData); } /** * Error log with exception details */ public void logError(EventType eventType, String message, Throwable throwable, Map<String, Object> context) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", eventType.name()); logData.put("message", message); logData.put("timestamp", System.currentTimeMillis()); logData.put("error_type", throwable.getClass().getSimpleName()); logData.put("error_message", throwable.getMessage()); logData.put("stack_trace", getStackTrace(throwable)); if (context != null) { logData.putAll(context); } logStructured("ERROR", logData); } /** * Performance metric log */ public void logPerformanceMetric(String operation, long durationMs, String status, Map<String, Object> metadata) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", "PERFORMANCE_METRIC"); logData.put("operation", operation); logData.put("duration_ms", durationMs); logData.put("status", status); logData.put("timestamp", System.currentTimeMillis()); if (metadata != null) { logData.putAll(metadata); } logStructured("INFO", logData); } /** * Audit log for security events */ public void logAuditEvent(String action, String resource, String userId, String outcome, Map<String, Object> details) { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", "AUDIT_EVENT"); logData.put("action", action); logData.put("resource", resource); logData.put("user_id", userId); logData.put("outcome", outcome); logData.put("timestamp", System.currentTimeMillis()); logData.put("ip_address", MDC.get("ipAddress")); if (details != null) { logData.putAll(details); } logStructured("INFO", logData); } /** * Generic structured logging method */ private void logStructured(String level, Map<String, Object> logData) { try { // Add MDC context to log data addMDCToLogData(logData); String jsonLog = objectMapper.writeValueAsString(logData); switch (level.toUpperCase()) { case "ERROR": logger.error(jsonLog); break; case "WARN": logger.warn(jsonLog); break; case "DEBUG": logger.debug(jsonLog); break; case "TRACE": logger.trace(jsonLog); break; default: logger.info(jsonLog); } } catch (JsonProcessingException e) { // Fallback to traditional logging if JSON serialization fails logger.error("Failed to serialize structured log: {}", logData, e); } } /** * Add MDC context to log data */ private void addMDCToLogData(Map<String, Object> logData) { // Copy relevant MDC fields to log data String traceId = MDC.get("traceId"); String spanId = MDC.get("spanId"); String userId = MDC.get("userId"); String sessionId = MDC.get("sessionId"); if (traceId != null) logData.put("trace_id", traceId); if (spanId != null) logData.put("span_id", spanId); if (userId != null) logData.put("user_id", userId); if (sessionId != null) logData.put("session_id", sessionId); } /** * Get stack trace as string */ private String getStackTrace(Throwable throwable) { if (throwable == null) return null; StringBuilder sb = new StringBuilder(); sb.append(throwable.toString()).append("\n"); for (StackTraceElement element : throwable.getStackTrace()) { sb.append("\tat ").append(element.toString()).append("\n"); } return sb.toString(); } /** * MDC management methods */ public void setMDCContext(String traceId, String spanId, String userId) { MDC.put("traceId", traceId); MDC.put("spanId", spanId); MDC.put("userId", userId); } public void setMDCField(String key, String value) { if (value != null) { MDC.put(key, value); } } public void clearMDC() { MDC.clear(); } /** * Fluent API for building structured logs */ public LogBuilder buildLog(EventType eventType) { return new LogBuilder(eventType); } public class LogBuilder { private final EventType eventType; private final Map<String, Object> fields = new HashMap<>(); private String message; public LogBuilder(EventType eventType) { this.eventType = eventType; this.fields.put("timestamp", System.currentTimeMillis()); } public LogBuilder withMessage(String message) { this.message = message; return this; } public LogBuilder withField(String key, Object value) { if (value != null) { fields.put(key, value); } return this; } public LogBuilder withFields(Map<String, Object> additionalFields) { if (additionalFields != null) { fields.putAll(additionalFields); } return this; } public void info() { logEvent(eventType, message, fields); } public void error(Throwable throwable) { logError(eventType, message, throwable, fields); } public void warn() { Map<String, Object> logData = new HashMap<>(); logData.put("event_type", eventType.name()); logData.put("message", message); logData.putAll(fields); logStructured("WARN", logData); } } } 4. Spring Boot Configuration
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.CommonsRequestLoggingFilter; import javax.servlet.Filter; @Configuration public class LoggingConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } @Bean public StructuredLogger structuredLogger(ObjectMapper objectMapper) { return new StructuredLogger(objectMapper); } @Bean public Filter requestLoggingFilter() { CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter() { @Override protected void beforeRequest(HttpServletRequest request, String message) { // Set MDC context before request processing MDC.put("requestId", generateRequestId()); MDC.put("method", request.getMethod()); MDC.put("uri", request.getRequestURI()); MDC.put("ipAddress", getClientIpAddress(request)); super.beforeRequest(request, message); } @Override protected void afterRequest(HttpServletRequest request, String message) { super.afterRequest(request, message); // Clear MDC after request processing MDC.clear(); } }; filter.setIncludeClientInfo(true); filter.setIncludeQueryString(true); filter.setIncludePayload(true); filter.setMaxPayloadLength(1000); filter.setIncludeHeaders(false); return filter; } private String generateRequestId() { return java.util.UUID.randomUUID().toString(); } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } } 5. Aspect-Oriented Logging
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Aspect @Component public class ServiceLoggingAspect { private final StructuredLogger structuredLogger; public ServiceLoggingAspect(StructuredLogger structuredLogger) { this.structuredLogger = structuredLogger; } @Around("@within(org.springframework.stereotype.Service) || @within(org.springframework.web.bind.annotation.RestController)") public Object logServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String className = signature.getDeclaringType().getSimpleName(); String methodName = signature.getName(); String operation = className + "." + methodName; long startTime = System.currentTimeMillis(); Map<String, Object> logContext = new HashMap<>(); try { // Log method entry logContext.put("operation", operation); logContext.put("stage", "start"); logContext.put("parameters", getMethodParameters(joinPoint)); structuredLogger.logEvent(StructuredLogger.EventType.BUSINESS_METRIC, "Method execution started", logContext); // Execute the method Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; // Log successful completion logContext.put("stage", "complete"); logContext.put("duration_ms", duration); logContext.put("status", "success"); structuredLogger.logPerformanceMetric(operation, duration, "success", logContext); return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; // Log error logContext.put("stage", "error"); logContext.put("duration_ms", duration); logContext.put("status", "error"); structuredLogger.logError(StructuredLogger.EventType.SYSTEM_ERROR, "Method execution failed", e, logContext); throw e; } } private Map<String, Object> getMethodParameters(ProceedingJoinPoint joinPoint) { Map<String, Object> parameters = new HashMap<>(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String[] paramNames = signature.getParameterNames(); Object[] paramValues = joinPoint.getArgs(); if (paramNames != null) { for (int i = 0; i < paramNames.length; i++) { // Don't log sensitive parameters if (!isSensitiveParameter(paramNames[i])) { parameters.put(paramNames[i], safeToString(paramValues[i])); } } } return parameters; } private boolean isSensitiveParameter(String paramName) { String lowerParamName = paramName.toLowerCase(); return lowerParamName.contains("password") || lowerParamName.contains("token") || lowerParamName.contains("secret") || lowerParamName.contains("key"); } private String safeToString(Object obj) { if (obj == null) return "null"; // Avoid calling toString on large objects or arrays if (obj.getClass().isArray()) { return "array[" + java.lang.reflect.Array.getLength(obj) + "]"; } if (obj instanceof Collection) { return "collection[" + ((Collection<?>) obj).size() + "]"; } if (obj instanceof Map) { return "map[" + ((Map<?, ?>) obj).size() + "]"; } return obj.toString(); } } 6. REST Controller with Structured Logging
import org.slf4j.MDC; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/orders") public class OrderController { private final StructuredLogger structuredLogger; private final OrderService orderService; public OrderController(StructuredLogger structuredLogger, OrderService orderService) { this.structuredLogger = structuredLogger; this.orderService = orderService; } @PostMapping public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request, HttpServletRequest httpRequest) { // Set MDC context MDC.put("userId", request.getCustomerId()); MDC.put("operation", "create_order"); try { structuredLogger.buildLog(StructuredLogger.EventType.ORDER_CREATED) .withMessage("Creating new order") .withField("customer_id", request.getCustomerId()) .withField("order_amount", request.getAmount()) .withField("item_count", request.getItems().size()) .info(); Order order = orderService.createOrder(request); structuredLogger.buildLog(StructuredLogger.EventType.ORDER_CREATED) .withMessage("Order created successfully") .withField("order_id", order.getId()) .withField("status", order.getStatus()) .info(); return ResponseEntity.ok(order); } catch (Exception e) { structuredLogger.buildLog(StructuredLogger.EventType.SYSTEM_ERROR) .withMessage("Failed to create order") .withField("customer_id", request.getCustomerId()) .withField("error", e.getMessage()) .error(e); throw e; } } @GetMapping("/{orderId}") public ResponseEntity<Order> getOrder(@PathVariable String orderId) { MDC.put("order_id", orderId); MDC.put("operation", "get_order"); try { structuredLogger.logEvent(StructuredLogger.EventType.BUSINESS_METRIC, "Fetching order details", Map.of("order_id", orderId)); Order order = orderService.getOrder(orderId); if (order == null) { structuredLogger.logEvent(StructuredLogger.EventType.BUSINESS_METRIC, "Order not found", Map.of("order_id", orderId, "status", "not_found")); return ResponseEntity.notFound().build(); } return ResponseEntity.ok(order); } finally { MDC.remove("order_id"); MDC.remove("operation"); } } @PostMapping("/{orderId}/payments") public ResponseEntity<PaymentResponse> processPayment(@PathVariable String orderId, @RequestBody PaymentRequest request) { MDC.put("order_id", orderId); MDC.put("payment_method", request.getPaymentMethod()); long startTime = System.currentTimeMillis(); try { structuredLogger.buildLog(StructuredLogger.EventType.PAYMENT_PROCESSED) .withMessage("Processing payment for order") .withField("order_id", orderId) .withField("payment_amount", request.getAmount()) .withField("payment_method", request.getPaymentMethod()) .info(); PaymentResponse response = orderService.processPayment(orderId, request); long duration = System.currentTimeMillis() - startTime; structuredLogger.logPerformanceMetric("process_payment", duration, "success", Map.of("order_id", orderId, "payment_id", response.getPaymentId(), "payment_status", response.getStatus())); return ResponseEntity.ok(response); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; structuredLogger.logPerformanceMetric("process_payment", duration, "error", Map.of("order_id", orderId, "error", e.getMessage())); structuredLogger.buildLog(StructuredLogger.EventType.SYSTEM_ERROR) .withMessage("Payment processing failed") .withField("order_id", orderId) .withField("duration_ms", duration) .error(e); throw e; } } } 7. Custom Log Appender for External Systems
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; public class ExternalLogAppender extends AppenderBase<ILoggingEvent> { private String externalUrl; private String apiKey; private final RestTemplate restTemplate = new RestTemplate(); private final ObjectMapper objectMapper = new ObjectMapper(); @Override protected void append(ILoggingEvent event) { if (externalUrl == null || externalUrl.isEmpty()) { return; } try { Map<String, Object> logEntry = new HashMap<>(); logEntry.put("timestamp", event.getTimeStamp()); logEntry.put("level", event.getLevel().toString()); logEntry.put("logger", event.getLoggerName()); logEntry.put("message", event.getFormattedMessage()); logEntry.put("thread", event.getThreadName()); // Add MDC properties if (event.getMDCPropertyMap() != null && !event.getMDCPropertyMap().isEmpty()) { logEntry.putAll(event.getMDCPropertyMap()); } // Add exception information if (event.getThrowableProxy() != null) { logEntry.put("exception", event.getThrowableProxy().getClassName()); logEntry.put("exception_message", event.getThrowableProxy().getMessage()); } sendToExternalSystem(logEntry); } catch (Exception e) { // Don't let logging failures affect application addError("Failed to send log to external system", e); } } private void sendToExternalSystem(Map<String, Object> logEntry) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); if (apiKey != null && !apiKey.isEmpty()) { headers.set("Authorization", "Bearer " + apiKey); } HttpEntity<Map<String, Object>> request = new HttpEntity<>(logEntry, headers); restTemplate.postForObject(externalUrl, request, String.class); } // Getters and setters for configuration public void setExternalUrl(String externalUrl) { this.externalUrl = externalUrl; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } } 8. Log Configuration in application.yml
# application.yml logging: level: com.example.orderservice: DEBUG org.springframework.web: INFO org.hibernate: WARN file: name: logs/application.log pattern: file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" # Custom logging properties app: logging: json: enabled: true external: enabled: false url: ${EXTERNAL_LOG_URL:} api-key: ${EXTERNAL_LOG_API_KEY:} metrics: enabled: true # Logback configuration spring: jackson: date-format: com.fasterxml.jackson.databind.util.StdDateFormat time-zone: UTC 9. Testing Structured Logging
import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import org.springframework.boot.test.context.SpringBootTest; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest class StructuredLoggingTest { @Test void testStructuredLogging() { // Get logger and create test appender Logger logger = (Logger) LoggerFactory.getLogger(StructuredLogger.class); ListAppender<ILoggingEvent> listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); // Create structured logger and log event StructuredLogger structuredLogger = new StructuredLogger(new ObjectMapper()); structuredLogger.logEvent(StructuredLogger.EventType.USER_LOGIN, "User logged in"); // Verify log was created assertThat(listAppender.list).hasSize(1); ILoggingEvent loggingEvent = listAppender.list.get(0); String logMessage = loggingEvent.getFormattedMessage(); assertThat(logMessage).contains("USER_LOGIN"); assertThat(logMessage).contains("User logged in"); assertThat(logMessage).contains("timestamp"); logger.detachAppender(listAppender); } @Test void testMDCContext() { MDC.put("traceId", "12345"); MDC.put("userId", "user-123"); StructuredLogger structuredLogger = new StructuredLogger(new ObjectMapper()); // Get logger and create test appender Logger logger = (Logger) LoggerFactory.getLogger(StructuredLogger.class); ListAppender<ILoggingEvent> listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); structuredLogger.logEvent(StructuredLogger.EventType.BUSINESS_METRIC, "Test event"); ILoggingEvent loggingEvent = listAppender.list.get(0); String logMessage = loggingEvent.getFormattedMessage(); assertThat(logMessage).contains("traceId"); assertThat(logMessage).contains("12345"); assertThat(logMessage).contains("userId"); assertThat(logMessage).contains("user-123"); MDC.clear(); logger.detachAppender(listAppender); } } Best Practices
1. Consistent Field Names
// Use consistent naming conventions public static class LogFields { public static final String TRACE_ID = "trace_id"; public static final String SPAN_ID = "span_id"; public static final String USER_ID = "user_id"; public static final String SESSION_ID = "session_id"; public static final String DURATION_MS = "duration_ms"; public static final String ERROR_TYPE = "error_type"; } 2. Sensitive Data Handling
public class LogSanitizer { public static Object sanitize(String fieldName, Object value) { if (value == null) return null; String lowerFieldName = fieldName.toLowerCase(); if (lowerFieldName.contains("password") || lowerFieldName.contains("token") || lowerFieldName.contains("secret")) { return "***REDACTED***"; } if (value instanceof String && ((String) value).length() > 1000) { return ((String) value).substring(0, 1000) + "...[truncated]"; } return value; } } 3. Performance Optimization
// Use parameterized logging to avoid string concatenation public void logWithParameters(String message, Object... args) { if (logger.isInfoEnabled()) { Map<String, Object> structuredData = new HashMap<>(); // Add structured data structuredData.put("message", message); // ... add other fields try { String json = objectMapper.writeValueAsString(structuredData); logger.info("{}", json); // Parameterized to avoid toString calls } catch (JsonProcessingException e) { logger.info(message, args); // Fallback } } } 4. Log Level Management
@Component public class LogLevelManager { @Scheduled(fixedRate = 30000) // Check every 30 seconds public void refreshLogLevels() { // Dynamically adjust log levels based on external configuration // or system conditions } } Conclusion
This comprehensive structured logging implementation provides:
- JSON-formatted logs for machine readability
- Consistent field names and data structures
- MDC integration for contextual logging
- Performance monitoring with metrics
- Error tracking with full context
- External system integration for log aggregation
- Testing support for validation
Key benefits:
- Improved debugging with rich context
- Better analytics through structured data
- Easier filtering and searching in log systems
- Performance insights through timing metrics
- Security compliance with audit trails
- Operational excellence through comprehensive monitoring
The system is production-ready and can be easily integrated with modern log management solutions like ELK Stack, Splunk, Datadog, or cloud-native logging services.