Overview
OpenTelemetry Metrics provides a vendor-agnostic way to collect, process, and export metrics from Java applications. Here's a comprehensive guide to implementing metrics with OpenTelemetry in Java.
Setup and Dependencies
1. Maven Dependencies
<!-- pom.xml --> <properties> <opentelemetry.version>1.32.0</opentelemetry.version> <opentelemetry-alpha.version>1.32.0-alpha</opentelemetry-alpha.version> </properties> <dependencies> <!-- OpenTelemetry API --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- OpenTelemetry SDK --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Metrics API & SDK --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api-metrics</artifactId> <version>${opentelemetry-alpha.version}</version> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk-metrics</artifactId> <version>${opentelemetry-alpha.version}</version> </dependency> <!-- Exporters --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> <version>${opentelemetry.version}</version> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-prometheus</artifactId> <version>${opentelemetry-alpha.version}</version> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-logging</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Auto-configuration --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId> <version>${opentelemetry.version}</version> </dependency> <!-- Spring Boot Starter --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-spring-boot-starter</artifactId> <version>1.0.0-alpha</version> </dependency> </dependencies> 2. Basic Configuration
// OpenTelemetryConfiguration.java import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.concurrent.TimeUnit; public class OpenTelemetryConfiguration { public static void initialize() { // Create resource identifying the service Resource resource = Resource.getDefault() .merge(Resource.builder() .put("service.name", "order-service") .put("service.version", "1.0.0") .put("deployment.environment", "production") .build()); // Configure OTLP exporter OtlpGrpcMetricExporter metricExporter = OtlpGrpcMetricExporter.builder() .setEndpoint("http://collector:4317") .setTimeout(Duration.ofSeconds(10)) .build(); // Create metric reader that exports every 30 seconds PeriodicMetricReader metricReader = PeriodicMetricReader.builder(metricExporter) .setInterval(Duration.ofSeconds(30)) .build(); // Build meter provider SdkMeterProvider meterProvider = SdkMeterProvider.builder() .setResource(resource) .registerMetricReader(metricReader) .build(); // Set as global instance io.opentelemetry.api.metrics.GlobalMeterProvider.set(meterProvider); } public static Meter getMeter(String instrumentationName) { return GlobalOpenTelemetry.getMeter(instrumentationName); } } Core Metrics Implementation
1. Basic Metric Types
// ApplicationMetrics.java import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.ObservableLongGauge; import io.opentelemetry.api.common.Attributes; import java.util.concurrent.atomic.AtomicLong; public class ApplicationMetrics { private final Meter meter; private final AtomicLong activeOrders = new AtomicLong(0); // Counter metrics private final LongCounter ordersCreated; private final LongCounter ordersCompleted; private final LongCounter ordersFailed; // Histogram metrics private final DoubleHistogram orderProcessingDuration; private final DoubleHistogram orderValueHistogram; // UpDownCounter metrics private final LongUpDownCounter inventoryLevel; // Observable Gauge metrics private final ObservableLongGauge activeOrdersGauge; public ApplicationMetrics() { this.meter = OpenTelemetryConfiguration.getMeter("order-service"); // Initialize counters this.ordersCreated = meter.counterBuilder("orders.created") .setDescription("Total number of orders created") .setUnit("1") .build(); this.ordersCompleted = meter.counterBuilder("orders.completed") .setDescription("Total number of orders completed successfully") .setUnit("1") .build(); this.ordersFailed = meter.counterBuilder("orders.failed") .setDescription("Total number of orders that failed") .setUnit("1") .build(); // Initialize histograms this.orderProcessingDuration = meter.histogramBuilder("order.processing.duration") .setDescription("Duration of order processing in milliseconds") .setUnit("ms") .build(); this.orderValueHistogram = meter.histogramBuilder("order.value") .setDescription("Distribution of order values") .setUnit("USD") .build(); // Initialize UpDownCounter this.inventoryLevel = meter.upDownCounterBuilder("inventory.level") .setDescription("Current inventory level") .setUnit("1") .build(); // Initialize Observable Gauge this.activeOrdersGauge = meter.gaugeBuilder("orders.active") .setDescription("Number of currently active orders") .setUnit("1") .ofLongs() .buildWithCallback(measurement -> { measurement.record(activeOrders.get(), Attributes.empty()); }); } public void recordOrderCreated(String orderType, String customerTier) { Attributes attributes = Attributes.builder() .put("order.type", orderType) .put("customer.tier", customerTier) .build(); ordersCreated.add(1, attributes); activeOrders.incrementAndGet(); } public void recordOrderCompleted(String orderType, String customerTier, double processingTimeMs, double orderValue) { Attributes attributes = Attributes.builder() .put("order.type", orderType) .put("customer.tier", customerTier) .put("status", "completed") .build(); ordersCompleted.add(1, attributes); activeOrders.decrementAndGet(); // Record processing duration orderProcessingDuration.record(processingTimeMs, attributes); // Record order value orderValueHistogram.record(orderValue, attributes); } public void recordOrderFailed(String orderType, String customerTier, String errorCode, double processingTimeMs) { Attributes attributes = Attributes.builder() .put("order.type", orderType) .put("customer.tier", customerTier) .put("error.code", errorCode) .put("status", "failed") .build(); ordersFailed.add(1, attributes); activeOrders.decrementAndGet(); // Record processing duration even for failed orders orderProcessingDuration.record(processingTimeMs, attributes); } public void updateInventory(String productId, String warehouse, long change) { Attributes attributes = Attributes.builder() .put("product.id", productId) .put("warehouse", warehouse) .build(); inventoryLevel.add(change, attributes); } // Getters for metrics (useful for testing) public LongCounter getOrdersCreated() { return ordersCreated; } public LongCounter getOrdersCompleted() { return ordersCompleted; } public DoubleHistogram getOrderProcessingDuration() { return orderProcessingDuration; } } 2. Business Service with Metrics
// OrderService.java import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; import java.util.concurrent.TimeUnit; public class OrderService { private final ApplicationMetrics metrics; private final InventoryService inventoryService; private final PaymentService paymentService; public OrderService(ApplicationMetrics metrics, InventoryService inventoryService, PaymentService paymentService) { this.metrics = metrics; this.inventoryService = inventoryService; this.paymentService = paymentService; } public Order processOrder(OrderRequest request) { long startTime = System.nanoTime(); String orderType = request.getOrderType(); String customerTier = request.getCustomerTier(); // Record order creation metrics.recordOrderCreated(orderType, customerTier); try { // Validate order validateOrder(request); // Check inventory if (!inventoryService.reserveInventory(request.getProductId(), request.getQuantity())) { throw new InventoryException("Insufficient inventory"); } // Process payment PaymentResult paymentResult = paymentService.processPayment(request); if (!paymentResult.isSuccess()) { throw new PaymentException("Payment failed: " + paymentResult.getErrorCode()); } // Create order Order order = createOrder(request, paymentResult); // Record successful completion double processingTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); metrics.recordOrderCompleted(orderType, customerTier, processingTimeMs, order.getTotalAmount()); return order; } catch (InventoryException e) { recordOrderFailure(startTime, orderType, customerTier, "INVENTORY_INSUFFICIENT", e); throw e; } catch (PaymentException e) { recordOrderFailure(startTime, orderType, customerTier, "PAYMENT_FAILED", e); throw e; } catch (Exception e) { recordOrderFailure(startTime, orderType, customerTier, "UNKNOWN_ERROR", e); throw new OrderProcessingException("Order processing failed", e); } } private void recordOrderFailure(long startTime, String orderType, String customerTier, String errorCode, Exception e) { double processingTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); metrics.recordOrderFailed(orderType, customerTier, errorCode, processingTimeMs); // Also add as span event if we're in a trace context Span.current().recordException(e, Attributes.of( Attributes.stringKey("error.code"), errorCode, Attributes.stringKey("order.type"), orderType )); } private void validateOrder(OrderRequest request) { if (request.getQuantity() <= 0) { throw new ValidationException("Invalid quantity"); } if (request.getTotalAmount() <= 0) { throw new ValidationException("Invalid amount"); } } private Order createOrder(OrderRequest request, PaymentResult paymentResult) { return new Order( generateOrderId(), request.getProductId(), request.getQuantity(), request.getTotalAmount(), paymentResult.getTransactionId() ); } private String generateOrderId() { return "ORD-" + System.currentTimeMillis() + "-" + (int)(Math.random() * 1000); } } // Supporting classes class OrderRequest { private String productId; private int quantity; private double totalAmount; private String orderType; private String customerTier; private String paymentMethod; // getters and setters } class Order { private String id; private String productId; private int quantity; private double totalAmount; private String transactionId; public Order(String id, String productId, int quantity, double totalAmount, String transactionId) { this.id = id; this.productId = productId; this.quantity = quantity; this.totalAmount = totalAmount; this.transactionId = transactionId; } // getters } Advanced Metrics Patterns
1. Database Metrics
// DatabaseMetrics.java import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.common.Attributes; import java.util.concurrent.ConcurrentHashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class DatabaseMetrics { private final Meter meter; private final LongCounter dbQueriesTotal; private final LongCounter dbErrorsTotal; private final DoubleHistogram dbQueryDuration; private final DoubleHistogram dbConnectionAcquisitionDuration; private final Map<String, LongCounter> queryCounters = new ConcurrentHashMap<>(); private final Map<String, DoubleHistogram> queryDurationHistograms = new ConcurrentHashMap<>(); public DatabaseMetrics(Meter meter) { this.meter = meter; // General database metrics this.dbQueriesTotal = meter.counterBuilder("db.queries.total") .setDescription("Total number of database queries") .setUnit("1") .build(); this.dbErrorsTotal = meter.counterBuilder("db.errors.total") .setDescription("Total number of database errors") .setUnit("1") .build(); this.dbQueryDuration = meter.histogramBuilder("db.query.duration") .setDescription("Database query duration in milliseconds") .setUnit("ms") .build(); this.dbConnectionAcquisitionDuration = meter.histogramBuilder("db.connection.acquisition.duration") .setDescription("Database connection acquisition duration in milliseconds") .setUnit("ms") .build(); } public QueryMetrics createQueryMetrics(String queryName) { return new QueryMetrics(queryName); } public class QueryMetrics { private final String queryName; private final LongCounter queryCounter; private final DoubleHistogram queryDuration; public QueryMetrics(String queryName) { this.queryName = queryName; this.queryCounter = queryCounters.computeIfAbsent(queryName, name -> meter.counterBuilder("db.query." + name + ".total") .setDescription("Total executions for query: " + name) .setUnit("1") .build()); this.queryDuration = queryDurationHistograms.computeIfAbsent(queryName, name -> meter.histogramBuilder("db.query." + name + ".duration") .setDescription("Duration for query: " + name) .setUnit("ms") .build()); } public <T> T measureQueryExecution(QuerySupplier<T> query) throws Exception { long startTime = System.nanoTime(); Attributes attributes = Attributes.of(Attributes.stringKey("query.name"), queryName); try { dbQueriesTotal.add(1, attributes); queryCounter.add(1, attributes); T result = query.execute(); long duration = System.nanoTime() - startTime; double durationMs = TimeUnit.NANOSECONDS.toMillis(duration); dbQueryDuration.record(durationMs, attributes); queryDuration.record(durationMs, attributes); return result; } catch (Exception e) { dbErrorsTotal.add(1, Attributes.of( Attributes.stringKey("query.name"), queryName, Attributes.stringKey("error.type"), e.getClass().getSimpleName() )); throw e; } } } @FunctionalInterface public interface QuerySupplier<T> { T execute() throws Exception; } } // Usage in repository @Repository public class OrderRepository { private final JdbcTemplate jdbcTemplate; private final DatabaseMetrics databaseMetrics; private final DatabaseMetrics.QueryMetrics findOrderByIdMetrics; private final DatabaseMetrics.QueryMetrics saveOrderMetrics; public OrderRepository(JdbcTemplate jdbcTemplate, DatabaseMetrics databaseMetrics) { this.jdbcTemplate = jdbcTemplate; this.databaseMetrics = databaseMetrics; this.findOrderByIdMetrics = databaseMetrics.createQueryMetrics("find_order_by_id"); this.saveOrderMetrics = databaseMetrics.createQueryMetrics("save_order"); } public Order findById(String orderId) { try { return findOrderByIdMetrics.measureQueryExecution(() -> { String sql = "SELECT * FROM orders WHERE id = ?"; return jdbcTemplate.queryForObject(sql, new OrderRowMapper(), orderId); }); } catch (Exception e) { throw new DataAccessException("Failed to find order: " + orderId, e); } } public void save(Order order) { try { saveOrderMetrics.measureQueryExecution(() -> { String sql = "INSERT INTO orders (id, product_id, quantity, total_amount) VALUES (?, ?, ?, ?)"; return jdbcTemplate.update(sql, order.getId(), order.getProductId(), order.getQuantity(), order.getTotalAmount()); }); } catch (Exception e) { throw new DataAccessException("Failed to save order: " + order.getId(), e); } } } 2. HTTP Client Metrics
// HttpClientMetrics.java import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.common.Attributes; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import java.util.concurrent.TimeUnit; public class HttpClientMetrics { private final Meter meter; private final RestTemplate restTemplate; private final LongCounter httpRequestsTotal; private final LongCounter httpErrorsTotal; private final DoubleHistogram httpRequestDuration; private final DoubleHistogram httpResponseSize; public HttpClientMetrics(Meter meter, RestTemplate restTemplate) { this.meter = meter; this.restTemplate = restTemplate; this.httpRequestsTotal = meter.counterBuilder("http.client.requests.total") .setDescription("Total number of HTTP client requests") .setUnit("1") .build(); this.httpErrorsTotal = meter.counterBuilder("http.client.errors.total") .setDescription("Total number of HTTP client errors") .setUnit("1") .build(); this.httpRequestDuration = meter.histogramBuilder("http.client.request.duration") .setDescription("HTTP client request duration in milliseconds") .setUnit("ms") .build(); this.httpResponseSize = meter.histogramBuilder("http.client.response.size") .setDescription("HTTP client response size in bytes") .setUnit("bytes") .build(); } public <T> ResponseEntity<T> executeWithMetrics(String serviceName, String endpoint, HttpMethod method, Class<T> responseType) { long startTime = System.nanoTime(); String url = buildUrl(serviceName, endpoint); Attributes attributes = Attributes.builder() .put("http.method", method.name()) .put("http.url", url) .put("service.name", serviceName) .build(); try { httpRequestsTotal.add(1, attributes); ResponseEntity<T> response = restTemplate.exchange(url, method, null, responseType); long duration = System.nanoTime() - startTime; double durationMs = TimeUnit.NANOSECONDS.toMillis(duration); httpRequestDuration.record(durationMs, attributes); // Record response size if available if (response.getHeaders().getContentLength() > 0) { httpResponseSize.record(response.getHeaders().getContentLength(), attributes); } // Record status code Attributes statusAttributes = attributes.toBuilder() .put("http.status_code", response.getStatusCodeValue()) .build(); httpRequestsTotal.add(1, statusAttributes); return response; } catch (Exception e) { long duration = System.nanoTime() - startTime; double durationMs = TimeUnit.NANOSECONDS.toMillis(duration); httpRequestDuration.record(durationMs, attributes); httpErrorsTotal.add(1, attributes.toBuilder() .put("error.type", e.getClass().getSimpleName()) .build()); throw e; } } private String buildUrl(String serviceName, String endpoint) { // Build URL based on service name and endpoint return String.format("http://%s/%s", serviceName, endpoint); } } 3. JVM and System Metrics
// SystemMetrics.java import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.api.common.Attributes; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; import java.lang.management.ThreadMXBean; import java.lang.management.GarbageCollectorMXBean; import java.util.concurrent.atomic.AtomicLong; public class SystemMetrics { private final Meter meter; private final MemoryMXBean memoryMXBean; private final ThreadMXBean threadMXBean; private final AtomicLong lastGcCount = new AtomicLong(0); private final AtomicLong lastGcTime = new AtomicLong(0); public SystemMetrics(Meter meter) { this.meter = meter; this.memoryMXBean = ManagementFactory.getMemoryMXBean(); this.threadMXBean = ManagementFactory.getThreadMXBean(); initializeSystemMetrics(); } private void initializeSystemMetrics() { // JVM Memory metrics meter.gaugeBuilder("jvm.memory.heap.used") .setDescription("Current heap memory usage in bytes") .setUnit("bytes") .ofLongs() .buildWithCallback(measurement -> { MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); measurement.record(heapUsage.getUsed(), Attributes.of(Attributes.stringKey("area"), "heap")); }); meter.gaugeBuilder("jvm.memory.non_heap.used") .setDescription("Current non-heap memory usage in bytes") .setUnit("bytes") .ofLongs() .buildWithCallback(measurement -> { MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage(); measurement.record(nonHeapUsage.getUsed(), Attributes.of(Attributes.stringKey("area"), "non_heap")); }); // Thread metrics meter.gaugeBuilder("jvm.threads.live") .setDescription("Current number of live threads") .setUnit("1") .ofLongs() .buildWithCallback(measurement -> { measurement.record(threadMXBean.getThreadCount()); }); meter.gaugeBuilder("jvm.threads.daemon") .setDescription("Current number of daemon threads") .setUnit("1") .ofLongs() .buildWithCallback(measurement -> { measurement.record(threadMXBean.getDaemonThreadCount()); }); // GC metrics meter.gaugeBuilder("jvm.gc.collections.count") .setDescription("Total number of garbage collections") .setUnit("1") .ofLongs() .buildWithCallback(measurement -> { long totalGcCount = 0; for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { totalGcCount += gc.getCollectionCount(); } measurement.record(totalGcCount); }); meter.gaugeBuilder("jvm.gc.collections.time") .setDescription("Total time spent in garbage collection in milliseconds") .setUnit("ms") .ofLongs() .buildWithCallback(measurement -> { long totalGcTime = 0; for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { totalGcTime += gc.getCollectionTime(); } measurement.record(totalGcTime); }); // CPU metrics meter.gaugeBuilder("system.cpu.usage") .setDescription("Recent CPU usage for the Java Virtual Machine process") .setUnit("1") .ofDoubles() .buildWithCallback(measurement -> { // This would typically use OperatingSystemMXBean // For simplicity, we're not implementing the actual CPU calculation here double cpuUsage = getProcessCpuUsage(); measurement.record(cpuUsage); }); } private double getProcessCpuUsage() { // Simplified implementation - in real scenario, use OperatingSystemMXBean return Math.random() * 100; // Mock value } } Exporters and Configuration
1. Multiple Exporters Configuration
// MultiExporterConfiguration.java import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; import io.opentelemetry.exporter.prometheus.PrometheusHttpServer; import io.opentelemetry.exporter.logging.LoggingMetricExporter; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; import java.time.Duration; import java.util.Arrays; public class MultiExporterConfiguration { public static SdkMeterProvider createMeterProvider() { Resource resource = Resource.getDefault() .merge(Resource.builder() .put("service.name", "order-service") .put("service.version", "1.0.0") .put("deployment.environment", "production") .build()); // OTLP exporter for centralized collection OtlpGrpcMetricExporter otlpExporter = OtlpGrpcMetricExporter.builder() .setEndpoint(System.getenv().getOrDefault("OTLP_ENDPOINT", "http://collector:4317")) .setTimeout(Duration.ofSeconds(10)) .build(); // Prometheus exporter for direct scraping PrometheusHttpServer prometheusExporter = PrometheusHttpServer.builder() .setPort(9464) .build(); // Logging exporter for development LoggingMetricExporter loggingExporter = LoggingMetricExporter.create(); // Create readers for periodic export PeriodicMetricReader otlpReader = PeriodicMetricReader.builder(otlpExporter) .setInterval(Duration.ofSeconds(30)) .build(); PeriodicMetricReader loggingReader = PeriodicMetricReader.builder(loggingExporter) .setInterval(Duration.ofSeconds(60)) .build(); return SdkMeterProvider.builder() .setResource(resource) .registerMetricReader(otlpReader) .registerMetricReader(loggingReader) .registerMetricReader(prometheusExporter) // Prometheus is already a reader .build(); } } 2. Spring Boot Auto-configuration
# application.yml management: metrics: export: otlp: url: http://collector:4317 step: 30s prometheus: enabled: true step: 30s endpoint: prometheus: enabled: true endpoints: web: exposure: include: health,metrics,prometheus opentelemetry: metrics: exporter: otlp: endpoint: http://collector:4317 resource-attributes: service.name: order-service service.version: 1.0.0 deployment.environment: production
// SpringBootMetricsConfiguration.java import io.opentelemetry.api.metrics.Meter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringBootMetricsConfiguration { @Bean public ApplicationMetrics applicationMetrics(Meter meter) { return new ApplicationMetrics(meter); } @Bean public DatabaseMetrics databaseMetrics(Meter meter) { return new DatabaseMetrics(meter); } @Bean public HttpClientMetrics httpClientMetrics(Meter meter) { return new HttpClientMetrics(meter, new RestTemplate()); } @Bean public SystemMetrics systemMetrics(Meter meter) { return new SystemMetrics(meter); } } Testing Metrics
1. Unit Testing Metrics
// ApplicationMetricsTest.java import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.api.common.Attributes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; class ApplicationMetricsTest { private InMemoryMetricReader metricReader; private ApplicationMetrics metrics; @BeforeEach void setUp() { metricReader = InMemoryMetricReader.create(); SdkMeterProvider meterProvider = SdkMeterProvider.builder() .registerMetricReader(metricReader) .build(); io.opentelemetry.api.metrics.GlobalMeterProvider.set(meterProvider); metrics = new ApplicationMetrics(); } @Test void shouldRecordOrderCreated() { // When metrics.recordOrderCreated("STANDARD", "PREMIUM"); // Then List<MetricData> metricData = metricReader.collectAllMetrics(); Optional<MetricData> ordersCreated = metricData.stream() .filter(data -> data.getName().equals("orders.created")) .findFirst(); assertTrue(ordersCreated.isPresent()); // Additional assertions on the metric data } @Test void shouldRecordOrderProcessingDuration() { // When metrics.recordOrderCompleted("EXPRESS", "STANDARD", 150.5, 99.99); // Then List<MetricData> metricData = metricReader.collectAllMetrics(); Optional<MetricData> processingDuration = metricData.stream() .filter(data -> data.getName().equals("order.processing.duration")) .findFirst(); assertTrue(processingDuration.isPresent()); } @Test void shouldUpdateInventoryLevel() { // When metrics.updateInventory("PROD_123", "WAREHOUSE_A", 10); metrics.updateInventory("PROD_123", "WAREHOUSE_A", -5); // Then List<MetricData> metricData = metricReader.collectAllMetrics(); Optional<MetricData> inventoryLevel = metricData.stream() .filter(data -> data.getName().equals("inventory.level")) .findFirst(); assertTrue(inventoryLevel.isPresent()); } } 2. Integration Testing
// OrderServiceIntegrationTest.java import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @SpringBootTest @TestPropertySource(properties = { "opentelemetry.metrics.exporter.otlp.endpoint=http://localhost:4317", "management.metrics.export.otlp.enabled=true" }) class OrderServiceIntegrationTest { @Autowired private OrderService orderService; @MockBean private InventoryService inventoryService; @MockBean private PaymentService paymentService; @Test void shouldRecordMetricsOnSuccessfulOrder() { // Given OrderRequest request = createOrderRequest(); when(inventoryService.reserveInventory(any(), any())).thenReturn(true); when(paymentService.processPayment(any())).thenReturn(new PaymentResult(true, "TXN_123")); // When Order order = orderService.processOrder(request); // Then - metrics should be recorded (verified via exported metrics) assertNotNull(order); // In real scenario, you would verify metrics via test exporter } @Test void shouldRecordMetricsOnFailedOrder() { // Given OrderRequest request = createOrderRequest(); when(inventoryService.reserveInventory(any(), any())).thenReturn(false); // When & Then assertThrows(InventoryException.class, () -> { orderService.processOrder(request); }); // Then - failure metrics should be recorded } } Best Practices
1. Metric Naming and Organization
// MetricNamingConventions.java public class MetricNamingConventions { /* * Naming conventions: * - Use dots as separators: domain.subsystem.metric * - Use snake_case for multi-word names * - Be consistent across services * - Include units in metric names when appropriate */ // Good examples private static final String ORDERS_CREATED_TOTAL = "orders.created.total"; private static final String ORDER_PROCESSING_DURATION_MS = "order.processing.duration.ms"; private static final String HTTP_CLIENT_REQUESTS_TOTAL = "http.client.requests.total"; private static final String DB_QUERY_DURATION_MS = "db.query.duration.ms"; private static final String JVM_MEMORY_HEAP_USED_BYTES = "jvm.memory.heap.used.bytes"; // Attribute naming conventions public static class Attributes { public static final String HTTP_METHOD = "http.method"; public static final String HTTP_STATUS_CODE = "http.status_code"; public static final String HTTP_URL = "http.url"; public static final String DB_OPERATION = "db.operation"; public static final String ERROR_TYPE = "error.type"; public static final String SERVICE_NAME = "service.name"; public static final String ORDER_TYPE = "order.type"; public static final String CUSTOMER_TIER = "customer.tier"; } } 2. Performance Considerations
// PerformanceOptimizedMetrics.java public class PerformanceOptimizedMetrics { private final Meter meter; // Reuse attribute builders for better performance private final AttributesBuilder orderAttributesBuilder; public PerformanceOptimizedMetrics(Meter meter) { this.meter = meter; this.orderAttributesBuilder = Attributes.builder(); // Pre-build common attributes this.orderAttributesBuilder.put("service.name", "order-service") .put("deployment.environment", "production"); } public void recordOrderWithOptimizedAttributes(String orderType, String customerTier) { // Reuse and update the attribute builder Attributes attributes = orderAttributesBuilder .put("order.type", orderType) .put("customer.tier", customerTier) .build(); // Use the metric // metrics.counter.add(1, attributes); // Reset for next use (remove variable attributes) orderAttributesBuilder.remove("order.type") .remove("customer.tier"); } } This comprehensive OpenTelemetry metrics implementation covers everything from basic setup to advanced patterns, including database monitoring, HTTP client metrics, JVM system metrics, and testing strategies. The key is to instrument your application with meaningful metrics that provide visibility into business processes and system health.