Structured Logging with ECS in Java

Overview

Elastic Common Schema (ECS) is a standardized schema for structured logging that enables consistent data analysis across different services and platforms. Structured logging with ECS in Java provides machine-readable logs with consistent field names and data types.

Architecture

ECS Logging Components

  1. ECS Formatter: Converts log events to ECS JSON format
  2. ECS Fields: Standardized field definitions
  3. Context Propagation: Trace context across services
  4. Log Shipping: Transport to Elasticsearch/Logstash

Dependencies

<dependencies>
<!-- Logback with ECS -->
<dependency>
<groupId>co.elastic.logging</groupId>
<artifactId>logback-ecs-encoder</artifactId>
<version>1.5.0</version>
</dependency>
<!-- Logstash Logback Encoder -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<!-- Micrometer Tracing -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>1.1.0</version>
</dependency>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Core Implementation

1. ECS Logging Configuration

@Configuration
@Slf4j
public class ECSLoggingConfig {
@Bean
public ECSFieldService ecsFieldService() {
return new ECSFieldService();
}
@Bean
public TracingContext tracingContext() {
return new TracingContext();
}
@Bean
public StructuredLogger structuredLogger(ECSFieldService ecsFieldService,
TracingContext tracingContext) {
return new StructuredLogger(ecsFieldService, tracingContext);
}
}
// ECS Field Service for managing ECS fields
@Component
@Slf4j
public class ECSFieldService {
private final ObjectMapper objectMapper;
private final Map<String, Object> baseFields;
public ECSFieldService() {
this.objectMapper = new ObjectMapper();
this.baseFields = initializeBaseFields();
}
private Map<String, Object> initializeBaseFields() {
Map<String, Object> fields = new LinkedHashMap<>();
// ECS base fields
fields.put("@timestamp", Instant.now().toString());
fields.put("ecs.version", "1.8.0");
// Service fields
fields.put("service.name", getServiceName());
fields.put("service.version", getServiceVersion());
fields.put("service.environment", getEnvironment());
fields.put("service.node.name", getNodeName());
return fields;
}
public Map<String, Object> createECSLogEvent(LogLevel level, String message) {
Map<String, Object> logEvent = new LinkedHashMap<>(baseFields);
// Log event fields
logEvent.put("log.level", level.toString());
logEvent.put("message", message);
logEvent.put("log.logger", getCallingLogger());
// Add tracing context
addTracingContext(logEvent);
return logEvent;
}
public Map<String, Object> createECSLogEvent(LogLevel level, String message, 
Map<String, Object> customFields) {
Map<String, Object> logEvent = createECSLogEvent(level, message);
if (customFields != null) {
logEvent.putAll(customFields);
}
return logEvent;
}
public void addErrorFields(Map<String, Object> logEvent, Throwable error) {
if (error != null) {
Map<String, Object> errorFields = new HashMap<>();
errorFields.put("message", error.getMessage());
errorFields.put("type", error.getClass().getName());
errorFields.put("stack_trace", getStackTrace(error));
logEvent.put("error", errorFields);
}
}
public void addHttpFields(Map<String, Object> logEvent, HttpServletRequest request, 
HttpServletResponse response, Long durationMs) {
Map<String, Object> httpFields = new HashMap<>();
if (request != null) {
httpFields.put("request.method", request.getMethod());
httpFields.put("request.referrer", request.getHeader("Referer"));
httpFields.put("request.bytes", request.getContentLengthLong());
Map<String, Object> requestHeaders = extractHeaders(request);
if (!requestHeaders.isEmpty()) {
httpFields.put("request.headers", requestHeaders);
}
}
if (response != null) {
httpFields.put("response.status_code", response.getStatus());
httpFields.put("response.bytes", response.getBufferSize());
Map<String, Object> responseHeaders = extractHeaders(response);
if (!responseHeaders.isEmpty()) {
httpFields.put("response.headers", responseHeaders);
}
}
if (durationMs != null) {
httpFields.put("duration", durationMs);
}
logEvent.put("http", httpFields);
}
public void addUserFields(Map<String, Object> logEvent, String userId, 
String username, Map<String, Object> userAttributes) {
Map<String, Object> userFields = new HashMap<>();
userFields.put("id", userId);
userFields.put("name", username);
if (userAttributes != null) {
userFields.putAll(userAttributes);
}
logEvent.put("user", userFields);
}
public void addBusinessFields(Map<String, Object> logEvent, String transactionId,
String operation, Map<String, Object> businessContext) {
Map<String, Object> transactionFields = new HashMap<>();
transactionFields.put("id", transactionId);
transactionFields.put("operation", operation);
if (businessContext != null) {
transactionFields.putAll(businessContext);
}
logEvent.put("transaction", transactionFields);
}
private void addTracingContext(Map<String, Object> logEvent) {
// Get tracing context from MDC or ThreadLocal
String traceId = MDC.get("traceId");
String spanId = MDC.get("spanId");
if (traceId != null || spanId != null) {
Map<String, Object> traceFields = new HashMap<>();
if (traceId != null) {
traceFields.put("id", traceId);
}
if (spanId != null) {
traceFields.put("span_id", spanId);
}
logEvent.put("trace", traceFields);
}
}
private Map<String, Object> extractHeaders(HttpServletRequest request) {
Map<String, Object> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (shouldIncludeHeader(headerName)) {
headers.put(headerName.toLowerCase(), request.getHeader(headerName));
}
}
return headers;
}
private Map<String, Object> extractHeaders(HttpServletResponse response) {
Map<String, Object> headers = new HashMap<>();
Collection<String> headerNames = response.getHeaderNames();
for (String headerName : headerNames) {
if (shouldIncludeHeader(headerName)) {
headers.put(headerName.toLowerCase(), response.getHeader(headerName));
}
}
return headers;
}
private boolean shouldIncludeHeader(String headerName) {
String lowerHeader = headerName.toLowerCase();
return lowerHeader.startsWith("x-") || 
lowerHeader.equals("user-agent") ||
lowerHeader.equals("content-type") ||
lowerHeader.equals("authorization");
}
private String getStackTrace(Throwable error) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
error.printStackTrace(pw);
return sw.toString();
}
private String getServiceName() {
return System.getProperty("service.name", "unknown-service");
}
private String getServiceVersion() {
return System.getProperty("service.version", "1.0.0");
}
private String getEnvironment() {
return System.getProperty("environment", "development");
}
private String getNodeName() {
return System.getProperty("node.name", 
InetAddress.getLocalHost().getHostName());
}
private String getCallingLogger() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTrace) {
String className = element.getClassName();
if (className.contains("Logger") && !className.contains("ECSFieldService")) {
return className;
}
}
return "unknown";
}
}

2. Structured Logger

@Component
@Slf4j
public class StructuredLogger {
private final ECSFieldService ecsFieldService;
private final TracingContext tracingContext;
private final ObjectMapper objectMapper;
public StructuredLogger(ECSFieldService ecsFieldService,
TracingContext tracingContext) {
this.ecsFieldService = ecsFieldService;
this.tracingContext = tracingContext;
this.objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public void info(String message) {
log.info(createLogMessage(LogLevel.INFO, message, null, null));
}
public void info(String message, Map<String, Object> customFields) {
log.info(createLogMessage(LogLevel.INFO, message, customFields, null));
}
public void info(String message, String transactionId, String operation) {
Map<String, Object> businessFields = new HashMap<>();
businessFields.put("transaction.id", transactionId);
businessFields.put("operation", operation);
log.info(createLogMessage(LogLevel.INFO, message, businessFields, null));
}
public void warn(String message) {
log.warn(createLogMessage(LogLevel.WARN, message, null, null));
}
public void warn(String message, Map<String, Object> customFields) {
log.warn(createLogMessage(LogLevel.WARN, message, customFields, null));
}
public void warn(String message, Throwable error) {
log.warn(createLogMessage(LogLevel.WARN, message, null, error));
}
public void error(String message, Throwable error) {
log.error(createLogMessage(LogLevel.ERROR, message, null, error));
}
public void error(String message, Map<String, Object> customFields, Throwable error) {
log.error(createLogMessage(LogLevel.ERROR, message, customFields, error));
}
public void error(String message, String transactionId, String operation, Throwable error) {
Map<String, Object> businessFields = new HashMap<>();
businessFields.put("transaction.id", transactionId);
businessFields.put("operation", operation);
log.error(createLogMessage(LogLevel.ERROR, message, businessFields, error));
}
public void debug(String message) {
log.debug(createLogMessage(LogLevel.DEBUG, message, null, null));
}
public void debug(String message, Map<String, Object> customFields) {
log.debug(createLogMessage(LogLevel.DEBUG, message, customFields, null));
}
// HTTP Request Logging
public void httpRequest(HttpServletRequest request, HttpServletResponse response, 
long durationMs, String userId) {
Map<String, Object> logEvent = ecsFieldService.createECSLogEvent(
LogLevel.INFO, 
String.format("HTTP %s %s - %d", 
request.getMethod(), 
request.getRequestURI(), 
response.getStatus())
);
ecsFieldService.addHttpFields(logEvent, request, response, durationMs);
if (userId != null) {
ecsFieldService.addUserFields(logEvent, userId, null, null);
}
log.info(formatLogEvent(logEvent));
}
// Business Transaction Logging
public void businessTransaction(String transactionId, String operation, 
String message, Map<String, Object> context) {
Map<String, Object> logEvent = ecsFieldService.createECSLogEvent(
LogLevel.INFO, message
);
ecsFieldService.addBusinessFields(logEvent, transactionId, operation, context);
log.info(formatLogEvent(logEvent));
}
// Performance Metrics Logging
public void performanceMetric(String operation, long durationMs, 
boolean success, Map<String, Object> tags) {
Map<String, Object> logEvent = ecsFieldService.createECSLogEvent(
LogLevel.INFO, 
String.format("Performance metric: %s took %dms", operation, durationMs)
);
Map<String, Object> metricFields = new HashMap<>();
metricFields.put("operation", operation);
metricFields.put("duration_ms", durationMs);
metricFields.put("success", success);
if (tags != null) {
metricFields.putAll(tags);
}
logEvent.put("metric", metricFields);
log.info(formatLogEvent(logEvent));
}
private String createLogMessage(LogLevel level, String message, 
Map<String, Object> customFields, Throwable error) {
Map<String, Object> logEvent = ecsFieldService.createECSLogEvent(
level, message, customFields
);
if (error != null) {
ecsFieldService.addErrorFields(logEvent, error);
}
return formatLogEvent(logEvent);
}
private String formatLogEvent(Map<String, Object> logEvent) {
try {
return objectMapper.writeValueAsString(logEvent);
} catch (JsonProcessingException e) {
log.error("Failed to serialize log event to JSON", e);
return "{\"error\": \"Failed to serialize log event\"}";
}
}
public enum LogLevel {
TRACE, DEBUG, INFO, WARN, ERROR
}
}

3. Tracing Context Management

@Component
@Slf4j
public class TracingContext {
private static final String TRACE_ID = "traceId";
private static final String SPAN_ID = "spanId";
private static final String PARENT_SPAN_ID = "parentSpanId";
public void initializeTraceContext(String traceId, String spanId, String parentSpanId) {
MDC.put(TRACE_ID, traceId);
MDC.put(SPAN_ID, spanId);
if (parentSpanId != null) {
MDC.put(PARENT_SPAN_ID, parentSpanId);
}
}
public void initializeTraceContext(String traceId, String spanId) {
initializeTraceContext(traceId, spanId, null);
}
public void clearTraceContext() {
MDC.remove(TRACE_ID);
MDC.remove(SPAN_ID);
MDC.remove(PARENT_SPAN_ID);
}
public String getTraceId() {
return MDC.get(TRACE_ID);
}
public String getSpanId() {
return MDC.get(SPAN_ID);
}
public String getParentSpanId() {
return MDC.get(PARENT_SPAN_ID);
}
public boolean hasTraceContext() {
return getTraceId() != null && getSpanId() != null;
}
public Map<String, String> getTraceContext() {
Map<String, String> context = new HashMap<>();
context.put(TRACE_ID, getTraceId());
context.put(SPAN_ID, getSpanId());
context.put(PARENT_SPAN_ID, getParentSpanId());
return context;
}
public String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
public String generateSpanId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 8);
}
}

Logback Configuration

1. logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- ECS JSON Encoder -->
<encoder class="co.elastic.logging.logback.EcsEncoder">
<serviceName>${ECS_SERVICE_NAME:-my-application}</serviceName>
<serviceVersion>${ECS_SERVICE_VERSION:-1.0.0}</serviceVersion>
<serviceEnvironment>${ECS_ENVIRONMENT:-development}</serviceEnvironment>
<serviceNodeName>${ECS_NODE_NAME:-localhost}</serviceNodeName>
<includeMarkers>true</includeMarkers>
<includeMdc>true</includeMdc>
<stackTraceAsArray>true</stackTraceAsArray>
</encoder>
<!-- Console Appender with ECS -->
<appender name="ECS_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<version/>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace>
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>30</maxDepthPerThrowable>
<maxLength>2048</maxLength>
<shortenedClassNameLength>20</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
</throwableConverter>
</stackTrace>
<pattern>
<pattern>
{
"ecs.version": "1.8.0",
"service.name": "${ECS_SERVICE_NAME:-my-application}",
"service.version": "${ECS_SERVICE_VERSION:-1.0.0}",
"service.environment": "${ECS_ENVIRONMENT:-development}",
"service.node.name": "${ECS_NODE_NAME:-localhost}"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<!-- File Appender with ECS -->
<appender name="ECS_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application-ecs.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-ecs.%d{yyyy-MM-dd}.json</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder class="co.elastic.logging.logback.EcsEncoder">
<serviceName>${ECS_SERVICE_NAME:-my-application}</serviceName>
<serviceVersion>${ECS_SERVICE_VERSION:-1.0.0}</serviceVersion>
<serviceEnvironment>${ECS_ENVIRONMENT:-development}</serviceEnvironment>
<serviceNodeName>${ECS_NODE_NAME:-localhost}</serviceNodeName>
<includeMarkers>true</includeMarkers>
<includeMdc>true</includeMdc>
</encoder>
</appender>
<!-- HTTP Access Log Appender -->
<appender name="HTTP_ACCESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/http-access.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/http-access.%d{yyyy-MM-dd}.json</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="co.elastic.logging.logback.EcsEncoder">
<serviceName>${ECS_SERVICE_NAME:-my-application}</serviceName>
<serviceVersion>${ECS_SERVICE_VERSION:-1.0.0}</serviceVersion>
<serviceEnvironment>${ECS_ENVIRONMENT:-development}</serviceEnvironment>
<includeMarkers>true</includeMarkers>
<includeMdc>true</includeMdc>
</encoder>
</appender>
<!-- Logger for HTTP Access -->
<logger name="http.access" level="INFO" additivity="false">
<appender-ref ref="HTTP_ACCESS"/>
<appender-ref ref="ECS_CONSOLE"/>
</logger>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="ECS_CONSOLE"/>
<appender-ref ref="ECS_FILE"/>
</root>
<!-- Application-specific Loggers -->
<logger name="com.example" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
</configuration>

Spring Boot Integration

1. HTTP Request Logging Filter

@Component
@Slf4j
public class ECSLoggingFilter implements Filter {
private final StructuredLogger structuredLogger;
private final TracingContext tracingContext;
public ECSLoggingFilter(StructuredLogger structuredLogger,
TracingContext tracingContext) {
this.structuredLogger = structuredLogger;
this.tracingContext = tracingContext;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Initialize tracing context
initializeTracingContext(httpRequest);
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
// Extract user ID from security context
String userId = extractUserId(httpRequest);
// Log HTTP request
structuredLogger.httpRequest(httpRequest, httpResponse, duration, userId);
// Clear tracing context
tracingContext.clearTraceContext();
}
}
private void initializeTracingContext(HttpServletRequest request) {
String traceId = getHeader(request, "X-Trace-Id");
String spanId = getHeader(request, "X-Span-Id");
String parentSpanId = getHeader(request, "X-Parent-Span-Id");
if (traceId == null) {
traceId = tracingContext.generateTraceId();
}
if (spanId == null) {
spanId = tracingContext.generateSpanId();
}
tracingContext.initializeTraceContext(traceId, spanId, parentSpanId);
// Set response headers for trace propagation
((HttpServletResponse) request).setHeader("X-Trace-Id", traceId);
((HttpServletResponse) request).setHeader("X-Span-Id", spanId);
}
private String getHeader(HttpServletRequest request, String headerName) {
String value = request.getHeader(headerName);
return (value != null && !value.trim().isEmpty()) ? value : null;
}
private String extractUserId(HttpServletRequest request) {
// Extract from Spring Security context or JWT token
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
} else if (principal instanceof String) {
return (String) principal;
}
}
} catch (Exception e) {
// Ignore - user context might not be available
}
return null;
}
}

2. REST Controller with Structured Logging

@RestController
@RequestMapping("/api")
@Slf4j
public class UserController {
private final StructuredLogger structuredLogger;
private final UserService userService;
public UserController(StructuredLogger structuredLogger, UserService userService) {
this.structuredLogger = structuredLogger;
this.userService = userService;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
String transactionId = UUID.randomUUID().toString();
try {
structuredLogger.info("Fetching user by ID", 
Map.of("user.id", id, "transaction.id", transactionId));
User user = userService.findUserById(id);
if (user != null) {
structuredLogger.info("User found successfully",
Map.of("user.id", id, "transaction.id", transactionId));
return ResponseEntity.ok(user);
} else {
structuredLogger.warn("User not found",
Map.of("user.id", id, "transaction.id", transactionId));
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
structuredLogger.error("Failed to fetch user", 
Map.of("user.id", id, "transaction.id", transactionId), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
String transactionId = UUID.randomUUID().toString();
try {
structuredLogger.businessTransaction(transactionId, "CREATE_USER", 
"Creating new user", 
Map.of("user.email", request.getEmail(), "user.role", request.getRole()));
long startTime = System.currentTimeMillis();
User user = userService.createUser(request);
long duration = System.currentTimeMillis() - startTime;
structuredLogger.performanceMetric("create_user", duration, true,
Map.of("user.id", user.getId(), "transaction.id", transactionId));
structuredLogger.info("User created successfully",
Map.of("user.id", user.getId(), "transaction.id", transactionId));
return ResponseEntity.status(HttpStatus.CREATED).body(user);
} catch (ValidationException e) {
structuredLogger.warn("User creation validation failed",
Map.of("user.email", request.getEmail(), "transaction.id", transactionId), e);
return ResponseEntity.badRequest().build();
} catch (Exception e) {
structuredLogger.error("User creation failed",
Map.of("user.email", request.getEmail(), "transaction.id", transactionId), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}

Advanced ECS Logging Features

1. Custom ECS Appender for Business Events

@Component
@Slf4j
public class BusinessEventLogger {
private final StructuredLogger structuredLogger;
private final ObjectMapper objectMapper;
public BusinessEventLogger(StructuredLogger structuredLogger) {
this.structuredLogger = structuredLogger;
this.objectMapper = new ObjectMapper();
}
public void logOrderCreated(Order order, String userId) {
Map<String, Object> eventFields = new HashMap<>();
eventFields.put("event.action", "order_created");
eventFields.put("event.kind", "event");
eventFields.put("event.category", "business");
eventFields.put("event.outcome", "success");
Map<String, Object> orderFields = new HashMap<>();
orderFields.put("id", order.getId());
orderFields.put("amount", order.getAmount());
orderFields.put("currency", order.getCurrency());
orderFields.put("items_count", order.getItems().size());
Map<String, Object> customFields = new HashMap<>();
customFields.put("event", eventFields);
customFields.put("order", orderFields);
customFields.put("user.id", userId);
structuredLogger.info("Order created successfully", customFields);
}
public void logPaymentProcessed(Payment payment, String orderId) {
Map<String, Object> eventFields = new HashMap<>();
eventFields.put("event.action", "payment_processed");
eventFields.put("event.kind", "event");
eventFields.put("event.category", "transaction");
eventFields.put("event.outcome", payment.isSuccess() ? "success" : "failure");
Map<String, Object> paymentFields = new HashMap<>();
paymentFields.put("id", payment.getId());
paymentFields.put("amount", payment.getAmount());
paymentFields.put("method", payment.getMethod());
paymentFields.put("status", payment.getStatus());
Map<String, Object> customFields = new HashMap<>();
customFields.put("event", eventFields);
customFields.put("payment", paymentFields);
customFields.put("order.id", orderId);
if (!payment.isSuccess()) {
customFields.put("error.reason", payment.getErrorReason());
}
structuredLogger.info("Payment processed", customFields);
}
public void logSystemEvent(String component, String action, String message, 
Map<String, Object> context) {
Map<String, Object> eventFields = new HashMap<>();
eventFields.put("event.action", action);
eventFields.put("event.kind", "event");
eventFields.put("event.category", "system");
Map<String, Object> customFields = new HashMap<>();
customFields.put("event", eventFields);
customFields.put("component", component);
if (context != null) {
customFields.putAll(context);
}
structuredLogger.info(message, customFields);
}
}

2. ECS Logging Aspect for Method Tracing

@Aspect
@Component
@Slf4j
public class MethodTracingAspect {
private final StructuredLogger structuredLogger;
private final TracingContext tracingContext;
public MethodTracingAspect(StructuredLogger structuredLogger,
TracingContext tracingContext) {
this.structuredLogger = structuredLogger;
this.tracingContext = tracingContext;
}
@Around("@annotation(LogExecution)")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
String transactionId = UUID.randomUUID().toString();
Map<String, Object> context = new HashMap<>();
context.put("method", methodName);
context.put("class", className);
context.put("transaction.id", transactionId);
long startTime = System.currentTimeMillis();
boolean success = false;
try {
structuredLogger.info("Method execution started", context);
Object result = joinPoint.proceed();
success = true;
return result;
} catch (Exception e) {
structuredLogger.error("Method execution failed", context, e);
throw e;
} finally {
long duration = System.currentTimeMillis() - startTime;
context.put("duration_ms", duration);
context.put("success", success);
structuredLogger.performanceMetric(
className + "." + methodName, duration, success, context);
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution {
String value() default "";
}

Testing Structured Logging

1. ECS Logging Tests

@SpringBootTest
@TestPropertySource(properties = {
"ECS_SERVICE_NAME=test-service",
"ECS_ENVIRONMENT=test"
})
class ECSLoggingTest {
@Autowired
private StructuredLogger structuredLogger;
@Autowired
private ECSFieldService ecsFieldService;
@Captor
private ArgumentCaptor<String> logCaptor;
@Test
void testStructuredLogging() throws JsonProcessingException {
// Given
Map<String, Object> customFields = Map.of(
"user.id", "12345",
"action", "login",
"source.ip", "192.168.1.1"
);
// When
structuredLogger.info("User logged in", customFields);
// Then
verify(log).info(logCaptor.capture());
String logMessage = logCaptor.getValue();
ObjectMapper mapper = new ObjectMapper();
JsonNode logEvent = mapper.readTree(logMessage);
assertEquals("User logged in", logEvent.get("message").asText());
assertEquals("INFO", logEvent.get("log.level").asText());
assertEquals("12345", logEvent.get("user.id").asText());
assertEquals("test-service", logEvent.get("service.name").asText());
}
@Test
void testErrorLogging() throws JsonProcessingException {
// Given
Exception testException = new RuntimeException("Test error");
// When
structuredLogger.error("Operation failed", testException);
// Then
verify(log).error(logCaptor.capture());
String logMessage = logCaptor.getValue();
ObjectMapper mapper = new ObjectMapper();
JsonNode logEvent = mapper.readTree(logMessage);
assertTrue(logEvent.has("error"));
assertEquals("RuntimeException", logEvent.get("error").get("type").asText());
assertEquals("Test error", logEvent.get("error").get("message").asText());
}
@Test
void testECSFieldService() {
// When
Map<String, Object> logEvent = ecsFieldService.createECSLogEvent(
StructuredLogger.LogLevel.INFO, "Test message"
);
// Then
assertTrue(logEvent.containsKey("@timestamp"));
assertTrue(logEvent.containsKey("ecs.version"));
assertTrue(logEvent.containsKey("service.name"));
assertEquals("Test message", logEvent.get("message"));
assertEquals("INFO", logEvent.get("log.level"));
}
}

Configuration

application.yml

# ECS Logging Configuration
ecs:
service:
name: user-service
version: 1.0.0
environment: ${ENVIRONMENT:development}
node-name: ${HOSTNAME:localhost}
logging:
config: classpath:logback-spring.xml
level:
com.example: DEBUG
http.access: INFO
org.springframework: WARN
org.hibernate: WARN
pattern:
console: ""
file:
name: logs/application.log
management:
endpoints:
web:
exposure:
include: health,metrics,loggers
endpoint:
health:
show-details: always
loggers:
enabled: true
server:
servlet:
context-path: /api

Best Practices

  1. Consistent Field Names: Use ECS field names consistently across services
  2. Meaningful Messages: Write clear, actionable log messages
  3. Appropriate Log Levels: Use appropriate log levels (DEBUG, INFO, WARN, ERROR)
  4. Structured Data: Log structured data instead of concatenated strings
  5. Sensitive Data: Never log passwords, tokens, or personal data
  6. Performance: Be mindful of log volume and performance impact
  7. Correlation IDs: Include correlation IDs for tracing requests across services
  8. Error Context: Provide sufficient context for error investigation

This implementation provides a comprehensive foundation for structured logging with ECS in Java applications, enabling consistent, machine-readable logs that work seamlessly with the Elastic Stack for monitoring and analysis.

Leave a Reply

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


Macro Nepal Helper