Skip to content

How to Add Observability to Spring Boot with Micrometer: A Beginner's Guide

Problem

I deployed my Spring Boot application to production. Then a user reported that the API was slow. I opened my logs and saw nothing useful. No metrics. No traces. No idea what was happening.

I realized I was flying blind.

User: "The checkout page is slow."
Me: "Let me check the logs..."
Logs: [Nothing useful - just INFO messages]
Me: "I have no idea what's wrong."

This is the problem observability solves. It gives you visibility into your running application.

Environment

  • Spring Boot 3.x
  • Java 17+
  • Maven or Gradle

What is Observability?

Observability means you can understand what’s happening inside your application from the outside. It has three parts:

The Three Pillars
┌─────────────────────────────────────────────────────────────┐
│ OBSERVABILITY │
├───────────────┬───────────────┬─────────────────────────────┤
│ METRICS │ LOGS │ TRACES │
│ │ │ │
│ "How many?" │ "What │ "Where did the request │
│ │ happened?" │ go?" │
│ │ │ │
│ Counters │ Error msgs │ Spans across services │
│ Gauges │ Info logs │ Timing per operation │
│ Histograms │ Debug logs │ Request flow visualization │
└───────────────┴───────────────┴─────────────────────────────┘

Spring Boot uses Micrometer for metrics and OpenTelemetry for tracing. Let me show you how to set them up.

Step 1: Add Actuator and Micrometer

First, add the dependencies. I use Maven:

pom.xml
<dependencies>
<!-- Spring Boot Actuator - provides /actuator endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Prometheus - exposes metrics in Prometheus format -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>

If you use Gradle:

build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

Step 2: Configure Actuator Endpoints

By default, Actuator exposes only /actuator/health and /actuator/info. I need to enable more endpoints.

src/main/resources/application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
prometheus:
enabled: true
metrics:
tags:
application: ${spring.application.name}

Now I can access:

  • /actuator/health - Application health status
  • /actuator/metrics - List of available metrics
  • /actuator/prometheus - Metrics in Prometheus format

Step 3: Check Default Metrics

Start the application and visit /actuator/metrics:

Terminal window
curl http://localhost:8080/actuator/metrics

You’ll see Spring Boot provides many metrics by default:

{
"names": [
"jvm.memory.used",
"jvm.memory.max",
"process.cpu.usage",
"http.server.requests",
"system.cpu.usage",
"tomcat.sessions.active.current"
]
}

Let me check one metric:

Terminal window
curl http://localhost:8080/actuator/metrics/jvm.memory.used
{
"name": "jvm.memory.used",
"measurements": [
{
"statistic": "VALUE",
"value": 1.2345678E8
}
]
}

This tells me the JVM is using about 123 MB of memory. Useful!

Step 4: Add Custom Metrics

Default metrics are nice, but I need business metrics too. Let’s say I want to track orders.

Counter: Count Things

Counters only go up. Use them for things like order count, request count, error count.

src/main/java/com/example/OrderService.java
@Service
public class OrderService {
private final Counter orderCounter;
public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("orders.created")
.description("Total number of orders created")
.tag("type", "standard")
.register(registry);
}
public void createOrder(Order order) {
// ... business logic ...
orderCounter.increment(); // +1 to the counter
}
}

Timer: Measure Duration

Timers measure how long things take. Perfect for tracking API response times.

src/main/java/com/example/OrderService.java
@Service
public class OrderService {
private final Timer orderTimer;
public OrderService(MeterRegistry registry) {
this.orderTimer = Timer.builder("orders.processing.time")
.description("Time taken to process orders")
.publishPercentiles(0.5, 0.95, 0.99) // p50, p95, p99
.register(registry);
}
public void createOrder(Order order) {
orderTimer.record(() -> {
// ... business logic ...
});
}
}

Gauge: Track Current Value

Gauges can go up or down. Use them for things like queue size, active connections.

src/main/java/com/example/OrderService.java
@Service
public class OrderService {
private final Queue&lt;Order&gt; pendingOrders = new LinkedList&lt;&gt;();
public OrderService(MeterRegistry registry) {
registry.gauge("orders.pending.size", pendingOrders, Queue::size);
}
public void addToQueue(Order order) {
pendingOrders.add(order);
}
}

Step 5: Expose Metrics to Prometheus

The /actuator/prometheus endpoint outputs metrics in Prometheus format:

Terminal window
curl http://localhost:8080/actuator/prometheus
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{application="my-app",area="heap",} 1.2345678E8
# HELP orders_created_total Total number of orders created
# TYPE orders_created_total counter
orders_created_total{application="my-app",type="standard",} 42.0

Prometheus can scrape this endpoint and store the data. Then Grafana can visualize it.

Step 6: Add Distributed Tracing

Metrics tell you WHAT is slow. Tracing tells you WHERE it’s slow.

Add Tracing Dependencies

pom.xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

Configure Tracing

src/main/resources/application.yml
management:
tracing:
enabled: true
sampling:
probability: 1.0 # 100% sampling (reduce in production!)
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans

Create Custom Spans

Spans represent units of work. Spring can create them automatically with annotations:

src/main/java/com/example/PaymentService.java
@Service
public class PaymentService {
private final Tracer tracer;
public PaymentService(Tracer tracer) {
this.tracer = tracer;
}
@NewSpan("process-payment") // Creates a span automatically
public PaymentResult processPayment(PaymentRequest request) {
Span span = tracer.currentSpan();
span.tag("payment.method", request.getMethod());
span.tag("payment.amount", String.valueOf(request.getAmount()));
// Business logic here
return doProcess(request);
}
}

Now when I call processPayment, it creates a span with timing information. I can see it in Zipkin or Jaeger.

Step 7: Correlate Logs with Traces

Here’s the magic part. I can connect logs to traces:

src/main/resources/application.yml
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

Now my logs include trace IDs:

INFO [my-app,abc123,def456] Processed payment successfully

When something fails, I can search logs by trace ID. No more hunting through thousands of log lines.

Step 8: Add Custom Health Checks

The /actuator/health endpoint shows if your app is healthy. You can add custom checks:

src/main/java/com/example/DatabaseHealthIndicator.java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(1)) {
return Health.up()
.withDetail("database", "PostgreSQL")
.withDetail("validationQuery", "SELECT 1")
.build();
}
} catch (SQLException e) {
return Health.down()
.withException(e)
.build();
}
return Health.unknown().build();
}
}

Now /actuator/health shows:

{
"status": "UP",
"components": {
"database": {
"status": "UP",
"details": {
"database": "PostgreSQL"
}
},
"diskSpace": {
"status": "UP"
}
}
}

Common Mistakes to Avoid

1. Sampling Rate Too High

Don’t trace 100% of requests in production. It’s too much data.

# WRONG for production
management:
tracing:
sampling:
probability: 1.0 # 100%
# BETTER for production
management:
tracing:
sampling:
probability: 0.1 # 10%

2. High Cardinality Tags

Don’t use unique values as tags. This creates too many time series.

// WRONG - user IDs are unique, creates millions of series
Counter.builder("requests")
.tag("user_id", userId) // BAD!
.register(registry);
// BETTER - use bounded values
Counter.builder("requests")
.tag("region", userRegion) // OK - only a few regions
.register(registry);

3. Not Setting Up Alerts

Metrics are useless if nobody looks at them. Set up alerts:

Prometheus Alert Rules
groups:
- name: spring-boot-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) /
sum(rate(http_server_requests_seconds_count[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate above 5%"

Quick Setup Checklist

Here’s what you need for basic observability:

Observability Checklist
[ ] Add spring-boot-starter-actuator dependency
[ ] Add micrometer-registry-prometheus dependency
[ ] Configure application.yml to expose endpoints
[ ] Add custom business metrics (counters, timers)
[ ] Set up Prometheus to scrape /actuator/prometheus
[ ] Create Grafana dashboards
[ ] Add tracing dependencies for distributed tracing
[ ] Configure log pattern with trace IDs
[ ] Set up alerts for critical metrics

Summary

In this post, I showed how to add observability to Spring Boot with Micrometer. The key steps are:

  1. Add Actuator and Micrometer dependencies
  2. Configure which endpoints to expose
  3. Add custom metrics for your business logic
  4. Set up Prometheus to collect metrics
  5. Add distributed tracing to see where requests go
  6. Correlate logs with traces for easier debugging

With these in place, you’ll never be flying blind in production again. When users report slowness, you’ll have the data to find the problem.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments