OpenTelemetry Metrics Export in Java

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.

Leave a Reply

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


Macro Nepal Helper