Structured Logging with JSON in Java: Complete Guide

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.

Leave a Reply

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


Macro Nepal Helper