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.