google-site-verification: google61fe8ba583a51912.html
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