google-site-verification: google61fe8ba583a51912.html
CloudWatch Embedded Metrics in Java: Complete Guide

Introduction to CloudWatch Embedded Metrics

CloudWatch Embedded Metric Format (EMF) allows you to generate custom metrics asynchronously by logging structured JSON documents. These metrics are extracted by CloudWatch and become available for alarms, dashboards, and analysis without requiring explicit PutMetricData API calls.


System Architecture Overview

CloudWatch EMF Pipeline ├── Application Instrumentation │ ├ - Structured JSON Logging │ ├ - Metric Definitions │ ├ - Dimension Configuration │ └ - Custom Properties ├── EMF Logger │ ├ - JSON Formatter │ ├ - Metric Validation │ ├ - Batch Processing │ └ - Error Handling ├── CloudWatch Logs │ ├ - Log Group/Stream │ ├ - Metric Filter (Automatic) │ └ - Log Processing └── CloudWatch Metrics ├ - Custom Namespace ├ - Alarms & Dashboards └ - Insights & Analysis

Core Implementation

1. Maven Dependencies

<properties> <aws.java.sdk.version>2.20.0</aws.java.sdk.version> <jackson.version>2.15.2</jackson.version> <slf4j.version>2.0.9</slf4j.version> <logback.version>1.4.11</logback.version> </properties> <dependencies> <!-- AWS SDK v2 --> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>cloudwatch</artifactId> <version>${aws.java.sdk.version}</version> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>cloudwatchlogs</artifactId> <version>${aws.java.sdk.version}</version> </dependency> <!-- JSON Processing --> <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> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <!-- Spring Boot (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.7.0</version> </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. EMF Model Classes

import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.*; @JsonInclude(JsonInclude.Include.NON_NULL) public class EmbeddedMetric { @JsonProperty("_aws") private AwsMetadata aws; private Map<String, Object> properties; public EmbeddedMetric() { this.aws = new AwsMetadata(); this.properties = new HashMap<>(); } // Static factory method public static EmbeddedMetric create() { return new EmbeddedMetric(); } // Fluent API methods public EmbeddedMetric withNamespace(String namespace) { this.aws.setCloudWatchMetrics(Collections.singletonList( new MetricDirective(namespace) )); return this; } public EmbeddedMetric withMetric(String name, double value) { this.aws.getCloudWatchMetrics().get(0).addMetric(name); this.properties.put(name, value); return this; } public EmbeddedMetric withDimension(String name, String value) { this.aws.getCloudWatchMetrics().get(0).addDimension(name); this.properties.put(name, value); return this; } public EmbeddedMetric withProperty(String key, Object value) { this.properties.put(key, value); return this; } public EmbeddedMetric withTimestamp(long timestamp) { this.aws.setTimestamp(timestamp); return this; } // Getters and setters public AwsMetadata getAws() { return aws; } public void setAws(AwsMetadata aws) { this.aws = aws; } public Map<String, Object> getProperties() { return properties; } public void setProperties(Map<String, Object> properties) { this.properties = properties; } // Inner classes for EMF structure @JsonInclude(JsonInclude.Include.NON_NULL) public static class AwsMetadata { @JsonProperty("CloudWatchMetrics") private List<MetricDirective> cloudWatchMetrics; private Long timestamp; public AwsMetadata() { this.cloudWatchMetrics = new ArrayList<>(); } public List<MetricDirective> getCloudWatchMetrics() { return cloudWatchMetrics; } public void setCloudWatchMetrics(List<MetricDirective> cloudWatchMetrics) { this.cloudWatchMetrics = cloudWatchMetrics; } public Long getTimestamp() { return timestamp; } public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } } public static class MetricDirective { private String namespace; private List<MetricDefinition> metrics; private List<List<String>> dimensions; public MetricDirective(String namespace) { this.namespace = namespace; this.metrics = new ArrayList<>(); this.dimensions = new ArrayList<>(); } public void addMetric(String name) { this.metrics.add(new MetricDefinition(name)); } public void addDimension(String name) { this.dimensions.add(Collections.singletonList(name)); } // Getters and setters public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } public List<MetricDefinition> getMetrics() { return metrics; } public void setMetrics(List<MetricDefinition> metrics) { this.metrics = metrics; } public List<List<String>> getDimensions() { return dimensions; } public void setDimensions(List<List<String>> dimensions) { this.dimensions = dimensions; } } public static class MetricDefinition { private String name; public MetricDefinition(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } }

3. CloudWatch EMF Service

import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; @Service public class CloudWatchEMFService { private static final Logger logger = LoggerFactory.getLogger("CLOUDWATCH-EMF"); private final ObjectMapper objectMapper; private final String namespace; private final boolean enabled; // Metric aggregation for high-volume scenarios private final Map<String, AtomicLong> counterCache = new ConcurrentHashMap<>(); private final Map<String, Double> valueCache = new ConcurrentHashMap<>(); public CloudWatchEMFService( @Value("${cloudwatch.namespace:MyApplication}") String namespace, @Value("${cloudwatch.emf.enabled:true}") boolean enabled) { this.objectMapper = new ObjectMapper(); this.namespace = namespace; this.enabled = enabled; } /** * Log a single metric value */ public void logMetric(String metricName, double value, Map<String, String> dimensions) { if (!enabled) return; EmbeddedMetric metric = EmbeddedMetric.create() .withNamespace(namespace) .withMetric(metricName, value) .withTimestamp(System.currentTimeMillis()); // Add dimensions if (dimensions != null) { dimensions.forEach(metric::withDimension); } logEMF(metric); } /** * Log multiple metrics in a single EMF document */ public void logMetrics(Map<String, Double> metrics, Map<String, String> dimensions) { if (!enabled || metrics == null || metrics.isEmpty()) return; EmbeddedMetric metric = EmbeddedMetric.create() .withNamespace(namespace) .withTimestamp(System.currentTimeMillis()); // Add metrics metrics.forEach(metric::withMetric); // Add dimensions if (dimensions != null) { dimensions.forEach(metric::withDimension); } logEMF(metric); } /** * Log a count metric (increments a counter) */ public void logCount(String metricName, Map<String, String> dimensions) { logCount(metricName, 1, dimensions); } /** * Log a count metric with specific value */ public void logCount(String metricName, long count, Map<String, String> dimensions) { logMetric(metricName, count, dimensions); } /** * Log a timing/duration metric */ public void logDuration(String metricName, long durationMs, Map<String, String> dimensions) { logMetric(metricName, durationMs, dimensions); } /** * Log a business transaction with metrics */ public void logBusinessTransaction(String transactionType, String status, long durationMs, Map<String, Object> properties) { if (!enabled) return; Map<String, Double> metrics = new HashMap<>(); metrics.put("TransactionCount", 1.0); metrics.put("TransactionDuration", (double) durationMs); Map<String, String> dimensions = new HashMap<>(); dimensions.put("TransactionType", transactionType); dimensions.put("TransactionStatus", status); EmbeddedMetric metric = EmbeddedMetric.create() .withNamespace(namespace) .withTimestamp(System.currentTimeMillis()); // Add metrics metrics.forEach(metric::withMetric); // Add dimensions dimensions.forEach(metric::withDimension); // Add properties if (properties != null) { properties.forEach(metric::withProperty); } logEMF(metric); } /** * Log an error with context */ public void logError(String errorType, String errorMessage, Map<String, String> context, Map<String, Object> properties) { if (!enabled) return; Map<String, Double> metrics = new HashMap<>(); metrics.put("ErrorCount", 1.0); Map<String, String> dimensions = new HashMap<>(); dimensions.put("ErrorType", errorType); EmbeddedMetric metric = EmbeddedMetric.create() .withNamespace(namespace) .withTimestamp(System.currentTimeMillis()); // Add metrics and dimensions metrics.forEach(metric::withMetric); dimensions.forEach(metric::withDimension); // Add error context metric.withProperty("error_message", errorMessage); if (context != null) { context.forEach(metric::withProperty); } if (properties != null) { properties.forEach(metric::withProperty); } logEMF(metric); } /** * Aggregate counter for high-volume metrics */ public void incrementCounter(String metricName, String dimensionKey, String dimensionValue) { String cacheKey = metricName + ":" + dimensionKey + ":" + dimensionValue; counterCache.computeIfAbsent(cacheKey, k -> new AtomicLong(0)).incrementAndGet(); } /** * Flush aggregated counters to CloudWatch */ public void flushCounters() { if (!enabled || counterCache.isEmpty()) return; counterCache.forEach((cacheKey, count) -> { if (count.get() > 0) { String[] parts = cacheKey.split(":"); String metricName = parts[0]; String dimensionKey = parts[1]; String dimensionValue = parts[2]; Map<String, String> dimensions = new HashMap<>(); dimensions.put(dimensionKey, dimensionValue); logMetric(metricName, count.get(), dimensions); // Reset counter count.set(0); } }); } /** * Record value for statistical aggregation */ public void recordValue(String metricName, double value, String dimensionKey, String dimensionValue) { String cacheKey = metricName + ":" + dimensionKey + ":" + dimensionValue; valueCache.put(cacheKey, value); } /** * Flush recorded values */ public void flushValues() { if (!enabled || valueCache.isEmpty()) return; valueCache.forEach((cacheKey, value) -> { String[] parts = cacheKey.split(":"); String metricName = parts[0]; String dimensionKey = parts[1]; String dimensionValue = parts[2]; Map<String, String> dimensions = new HashMap<>(); dimensions.put(dimensionKey, dimensionValue); logMetric(metricName, value, dimensions); }); valueCache.clear(); } /** * Log the EMF document */ private void logEMF(EmbeddedMetric metric) { try { String emfJson = objectMapper.writeValueAsString(metric); logger.info(emfJson); } catch (JsonProcessingException e) { logger.error("Failed to serialize EMF document", e); } } /** * Create dimensions map from varargs */ public static Map<String, String> dimensions(String... keyValuePairs) { if (keyValuePairs.length % 2 != 0) { throw new IllegalArgumentException("Key-value pairs must be even"); } Map<String, String> dimensions = new HashMap<>(); for (int i = 0; i < keyValuePairs.length; i += 2) { dimensions.put(keyValuePairs[i], keyValuePairs[i + 1]); } return dimensions; } }

4. Spring Boot Configuration

import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @Configuration @EnableScheduling public class CloudWatchConfig { @Value("${cloudwatch.namespace:MyApplication}") private String namespace; @Value("${cloudwatch.emf.enabled:true}") private boolean emfEnabled; @Bean public CloudWatchEMFService cloudWatchEMFService() { return new CloudWatchEMFService(namespace, emfEnabled); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } // Schedule metric flushing for aggregated counters @Scheduled(fixedRate = 60000) // Every minute public void flushAggregatedMetrics() { CloudWatchEMFService emfService = cloudWatchEMFService(); emfService.flushCounters(); emfService.flushValues(); } }

5. Logback Configuration for EMF

<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="30 seconds"> <!-- EMF-specific appender --> <appender name="EMF" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <!-- File appender for EMF logs --> <appender name="EMF_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/emf-metrics.log</file> <encoder> <pattern>%msg%n</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/emf-metrics.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> </appender> <!-- EMF Logger - only logs EMF JSON documents --> <logger name="CLOUDWATCH-EMF" level="INFO" additivity="false"> <appender-ref ref="EMF"/> <appender-ref ref="EMF_FILE"/> </logger> <!-- Root logger --> <root level="INFO"> <appender-ref ref="EMF"/> </root> </configuration>

6. REST Controller with EMF Instrumentation

import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/orders") public class OrderController { private final OrderService orderService; private final CloudWatchEMFService emfService; public OrderController(OrderService orderService, CloudWatchEMFService emfService) { this.orderService = orderService; this.emfService = emfService; } @PostMapping public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) { long startTime = System.currentTimeMillis(); try { // Log business metric emfService.incrementCounter("OrdersCreated", "Service", "OrderService"); Order order = orderService.createOrder(request); long duration = System.currentTimeMillis() - startTime; // Log successful transaction Map<String, Object> properties = new HashMap<>(); properties.put("order_id", order.getId()); properties.put("customer_id", request.getCustomerId()); properties.put("amount", request.getAmount()); emfService.logBusinessTransaction( "CreateOrder", "SUCCESS", duration, properties ); // Log specific metrics Map<String, Double> metrics = new HashMap<>(); metrics.put("OrderAmount", request.getAmount()); metrics.put("OrderProcessingTime", (double) duration); Map<String, String> dimensions = CloudWatchEMFService.dimensions( "OrderType", "STANDARD", "CustomerTier", getCustomerTier(request.getCustomerId()) ); emfService.logMetrics(metrics, dimensions); return ResponseEntity.ok(order); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; // Log error metric Map<String, String> errorContext = new HashMap<>(); errorContext.put("operation", "create_order"); errorContext.put("customer_id", request.getCustomerId()); Map<String, Object> errorProperties = new HashMap<>(); errorProperties.put("duration_ms", duration); errorProperties.put("error_details", e.getMessage()); emfService.logError( "OrderCreationError", "Failed to create order", errorContext, errorProperties ); throw e; } } @GetMapping("/{orderId}") public ResponseEntity<Order> getOrder(@PathVariable String orderId) { long startTime = System.currentTimeMillis(); try { Order order = orderService.getOrder(orderId); long duration = System.currentTimeMillis() - startTime; if (order == null) { // Log cache miss or not found emfService.logMetric( "OrderNotFound", 1.0, CloudWatchEMFService.dimensions("Operation", "GetOrder") ); return ResponseEntity.notFound().build(); } // Log cache hit and performance emfService.logDuration("GetOrderDuration", duration, CloudWatchEMFService.dimensions("Status", "FOUND")); return ResponseEntity.ok(order); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; emfService.logError( "GetOrderError", "Failed to retrieve order", CloudWatchEMFService.dimensions("OrderId", orderId), Map.of("duration_ms", duration) ); throw e; } } @PostMapping("/{orderId}/payments") public ResponseEntity<PaymentResponse> processPayment(@PathVariable String orderId, @RequestBody PaymentRequest request) { long startTime = System.currentTimeMillis(); try { PaymentResponse response = orderService.processPayment(orderId, request); long duration = System.currentTimeMillis() - startTime; // Log payment metrics Map<String, Double> metrics = new HashMap<>(); metrics.put("PaymentAmount", request.getAmount()); metrics.put("PaymentProcessingTime", (double) duration); metrics.put("PaymentCount", 1.0); Map<String, String> dimensions = CloudWatchEMFService.dimensions( "PaymentMethod", request.getPaymentMethod(), "Currency", request.getCurrency(), "Status", response.getStatus() ); emfService.logMetrics(metrics, dimensions); return ResponseEntity.ok(response); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; emfService.logError( "PaymentProcessingError", "Payment processing failed", CloudWatchEMFService.dimensions( "OrderId", orderId, "PaymentMethod", request.getPaymentMethod() ), Map.of("duration_ms", duration, "amount", request.getAmount()) ); throw e; } } private String getCustomerTier(String customerId) { // Simple tier determination logic return customerId.hashCode() % 3 == 0 ? "PREMIUM" : "STANDARD"; } }

7. Service Layer Instrumentation

import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Map; @Service public class OrderService { private final OrderRepository orderRepository; private final InventoryService inventoryService; private final CloudWatchEMFService emfService; public OrderService(OrderRepository orderRepository, InventoryService inventoryService, CloudWatchEMFService emfService) { this.orderRepository = orderRepository; this.inventoryService = inventoryService; this.emfService = emfService; } @Transactional public Order createOrder(OrderRequest request) { long startTime = System.currentTimeMillis(); try { // Check inventory boolean inventoryAvailable = inventoryService.checkInventory(request.getItems()); emfService.logMetric( "InventoryCheck", inventoryAvailable ? 1.0 : 0.0, CloudWatchEMFService.dimensions("Result", inventoryAvailable ? "AVAILABLE" : "UNAVAILABLE") ); if (!inventoryAvailable) { throw new RuntimeException("Insufficient inventory"); } // Create order Order order = orderRepository.save(convertToOrder(request)); long dbDuration = System.currentTimeMillis() - startTime; // Log database performance emfService.logDuration("DatabaseWriteTime", dbDuration, CloudWatchEMFService.dimensions("Operation", "OrderInsert")); // Log order creation metrics emfService.recordValue("OrderAmount", request.getAmount(), "Currency", "USD"); emfService.incrementCounter("OrdersProcessed", "ProductCategory", "ALL"); return order; } catch (Exception e) { emfService.logError( "OrderServiceError", "Order creation failed in service layer", Map.of("customer_id", request.getCustomerId()), Map.of("item_count", request.getItems().size()) ); throw e; } } public Order getOrder(String orderId) { long startTime = System.currentTimeMillis(); try { Order order = orderRepository.findById(orderId); long duration = System.currentTimeMillis() - startTime; emfService.logDuration("DatabaseReadTime", duration, CloudWatchEMFService.dimensions("Operation", "OrderSelect")); return order; } catch (Exception e) { emfService.logError( "OrderRetrievalError", "Failed to retrieve order from database", Map.of("order_id", orderId), null ); throw e; } } public PaymentResponse processPayment(String orderId, PaymentRequest request) { long startTime = System.currentTimeMillis(); try { // Simulate payment processing PaymentResponse response = processPaymentWithProvider(request); long duration = System.currentTimeMillis() - startTime; // Log payment provider response time emfService.logDuration("PaymentProviderResponseTime", duration, CloudWatchEMFService.dimensions("Provider", getPaymentProvider(request.getPaymentMethod()))); return response; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; emfService.logError( "PaymentServiceError", "Payment service call failed", Map.of( "order_id", orderId, "payment_method", request.getPaymentMethod() ), Map.of("duration_ms", duration) ); throw e; } } private PaymentResponse processPaymentWithProvider(PaymentRequest request) { // Payment processing logic return new PaymentResponse("pay_" + System.currentTimeMillis(), "COMPLETED"); } private String getPaymentProvider(String paymentMethod) { switch (paymentMethod.toUpperCase()) { case "CREDIT_CARD": return "STRIPE"; case "PAYPAL": return "PAYPAL"; case "BANK_TRANSFER": return "PLAID"; default: return "UNKNOWN"; } } private Order convertToOrder(OrderRequest request) { return new Order( java.util.UUID.randomUUID().toString(), request.getCustomerId(), request.getAmount(), "PENDING" ); } }

8. Advanced EMF Features

import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @Component public class AdvancedEMFService { private final CloudWatchEMFService emfService; private final BlockingQueue<MetricBatch> metricBatchQueue; private final int batchSize = 100; private final long batchTimeoutMs = 5000; // 5 seconds public AdvancedEMFService(CloudWatchEMFService emfService) { this.emfService = emfService; this.metricBatchQueue = new LinkedBlockingQueue<>(); startBatchProcessor(); } /** * Batch similar metrics for efficiency */ public void batchMetric(String metricName, double value, Map<String, String> dimensions) { MetricBatch batch = new MetricBatch(metricName, dimensions); batch.addValue(value); try { metricBatchQueue.offer(batch, batchTimeoutMs, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Fallback to immediate logging emfService.logMetric(metricName, value, dimensions); } } /** * Log histogram data */ public void logHistogram(String metricName, List<Double> values, Map<String, String> dimensions) { if (values == null || values.isEmpty()) return; // Calculate statistics double min = values.stream().min(Double::compare).orElse(0.0); double max = values.stream().max(Double::compare).orElse(0.0); double avg = values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); double p95 = calculatePercentile(values, 95); double p99 = calculatePercentile(values, 99); Map<String, Double> metrics = new HashMap<>(); metrics.put(metricName + "Min", min); metrics.put(metricName + "Max", max); metrics.put(metricName + "Average", avg); metrics.put(metricName + "P95", p95); metrics.put(metricName + "P99", p99); metrics.put(metricName + "SampleCount", (double) values.size()); emfService.logMetrics(metrics, dimensions); } /** * Log custom business metrics with complex dimensions */ public void logBusinessMetrics(BusinessMetrics metrics) { EmbeddedMetric emf = EmbeddedMetric.create() .withNamespace("BusinessMetrics") .withTimestamp(System.currentTimeMillis()); // Add metrics metrics.getMetrics().forEach(emf::withMetric); // Add dimensions metrics.getDimensions().forEach(emf::withDimension); // Add properties metrics.getProperties().forEach(emf::withProperty); // Log using the underlying service try { String emfJson = new ObjectMapper().writeValueAsString(emf); LoggerFactory.getLogger("CLOUDWATCH-EMF").info(emfJson); } catch (Exception e) { LoggerFactory.getLogger(AdvancedEMFService.class) .error("Failed to log business metrics", e); } } private void startBatchProcessor() { Thread processor = new Thread(() -> { List<MetricBatch> batch = new ArrayList<>(); while (!Thread.currentThread().isInterrupted()) { try { MetricBatch metricBatch = metricBatchQueue.poll(1, TimeUnit.SECONDS); if (metricBatch != null) { batch.add(metricBatch); } if (batch.size() >= batchSize || (metricBatch == null && !batch.isEmpty())) { processBatch(batch); batch.clear(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); processor.setDaemon(true); processor.setName("EMF-Batch-Processor"); processor.start(); } private void processBatch(List<MetricBatch> batches) { // Aggregate and log batched metrics batches.forEach(batch -> { double sum = batch.getValues().stream().mapToDouble(Double::doubleValue).sum(); double avg = sum / batch.getValues().size(); // Log aggregated metric emfService.logMetric(batch.getMetricName(), avg, batch.getDimensions()); }); } private double calculatePercentile(List<Double> values, double percentile) { if (values.isEmpty()) return 0.0; List<Double> sorted = new ArrayList<>(values); Collections.sort(sorted); int index = (int) Math.ceil(percentile / 100.0 * sorted.size()) - 1; index = Math.max(0, Math.min(index, sorted.size() - 1)); return sorted.get(index); } // Supporting classes public static class BusinessMetrics { private Map<String, Double> metrics = new HashMap<>(); private Map<String, String> dimensions = new HashMap<>(); private Map<String, Object> properties = new HashMap<>(); public BusinessMetrics addMetric(String name, double value) { metrics.put(name, value); return this; } public BusinessMetrics addDimension(String name, String value) { dimensions.put(name, value); return this; } public BusinessMetrics addProperty(String name, Object value) { properties.put(name, value); return this; } // Getters public Map<String, Double> getMetrics() { return metrics; } public Map<String, String> getDimensions() { return dimensions; } public Map<String, Object> getProperties() { return properties; } } private static class MetricBatch { private final String metricName; private final Map<String, String> dimensions; private final List<Double> values = new ArrayList<>(); public MetricBatch(String metricName, Map<String, String> dimensions) { this.metricName = metricName; this.dimensions = dimensions != null ? new HashMap<>(dimensions) : new HashMap<>(); } public void addValue(double value) { values.add(value); } // Getters public String getMetricName() { return metricName; } public Map<String, String> getDimensions() { return dimensions; } public List<Double> getValues() { return values; } } }

9. Application Configuration

# application.yml cloudwatch: namespace: OrderService emf: enabled: true batch: size: 100 timeout-ms: 5000 # Logging configuration logging: level: CLOUDWATCH-EMF: INFO com.example: DEBUG file: name: logs/application.log pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" # AWS Configuration (if using SDK directly) aws: region: us-east-1 access-key-id: ${AWS_ACCESS_KEY_ID:} secret-access-key: ${AWS_SECRET_ACCESS_KEY:} # Spring configuration spring: application: name: order-service

10. Testing EMF Implementation

import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.slf4j.Logger; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @SpringBootTest class CloudWatchEMFTest { @MockBean private Logger emfLogger; @Test void testEMFLogging() { CloudWatchEMFService emfService = new CloudWatchEMFService("TestNamespace", true); // Test metric logging emfService.logMetric("TestMetric", 42.0, CloudWatchEMFService.dimensions("TestDim", "Value")); // Verify EMF JSON was logged verify(emfLogger, times(1)).info(anyString()); } @Test void testBusinessTransactionLogging() { CloudWatchEMFService emfService = new CloudWatchEMFService("TestNamespace", true); Map<String, Object> properties = new HashMap<>(); properties.put("order_id", "12345"); properties.put("amount", 99.99); emfService.logBusinessTransaction("TestTransaction", "SUCCESS", 150L, properties); verify(emfLogger, times(1)).info(anyString()); } }

Best Practices

1. Dimension Design

// Good - meaningful, bounded cardinality Map<String, String> dimensions = CloudWatchEMFService.dimensions( "Environment", "production", "Service", "order-service", "Operation", "create_order", "Status", "success" ); // Avoid - high cardinality dimensions Map<String, String> badDimensions = CloudWatchEMFService.dimensions( "UserId", "user-123456789", // High cardinality! "RequestId", "req-abcdefgh" // Very high cardinality! );

2. Metric Namespacing

// Use consistent naming conventions public class MetricNames { public static final String ORDER_COUNT = "OrderCount"; public static final String ORDER_AMOUNT = "OrderAmount"; public static final String PROCESSING_TIME = "ProcessingTime"; public static final String ERROR_COUNT = "ErrorCount"; }

3. Performance Optimization

@Component public class EfficientEMFService { public void logConditionally(boolean condition) { if (condition) { // Only compute expensive values when needed double expensiveValue = computeExpensiveMetric(); emfService.logMetric("ExpensiveMetric", expensiveValue, null); } } private double computeExpensiveMetric() { // Expensive computation return Math.random() * 100; } }

4. Error Handling

public class SafeEMFService { public void logSafely(String metricName, double value) { try { emfService.logMetric(metricName, value, null); } catch (Exception e) { // Don't let logging failures break application logic logger.warn("Failed to log EMF metric", e); } } }

Conclusion

This comprehensive CloudWatch Embedded Metrics implementation provides:

  • Structured JSON logging following EMF specification
  • Automatic metric extraction by CloudWatch
  • Flexible dimension support for multi-dimensional analysis
  • Batch processing for high-volume scenarios
  • Business transaction tracking with rich context
  • Error monitoring with detailed properties
  • Performance metrics for system observability

Key benefits:

  • Cost-effective - No per-metric API calls
  • Asynchronous - No impact on application performance
  • Rich context - Custom properties for debugging
  • CloudWatch native - Seamless integration with AWS ecosystem
  • Scalable - Handles high-volume metric emission

The implementation enables comprehensive observability of your Java applications while maintaining performance and cost efficiency through CloudWatch's EMF capabilities.

Leave a Reply

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


Macro Nepal Helper