Spring Boot is a common choice for enterprise applications. As services are split into microservices and deployed in containers, the number of running instances and service calls grows. Understanding what happens at runtime becomes essential.
A growing heap, a long garbage collection pause, or a blocked thread can become a production incident. Monitoring helps us see these problems before users feel their effects.
Micrometer provides a common API for application metrics. Together with Spring Boot Actuator, it exposes JVM and business metrics to monitoring systems such as Prometheus. This guide starts with endpoint configuration, then explains the metrics, queries, and alerts that matter in daily operations.
The examples follow the source document’s baseline: Spring Boot 3.5.3 with embedded Tomcat 10.1. Metric availability and labels depend on the JVM, libraries, and enabled instrumentation. Check your own /actuator/prometheus output before building dashboards.
1. Expose Metrics with Actuator
Add the dependencies
Add these dependencies to pom.xml. The Spring Boot parent manages their versions.
<!-- Production monitoring and management endpoints --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency>
<!-- Prometheus registry for /actuator/prometheus --><dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId></dependency>Configure the endpoints
The following configuration exposes health, application information, and Prometheus metrics. It also enables the Tomcat MBean registry, which is needed for Tomcat thread metrics.
server: tomcat: mbeanregistry: enabled: true
management: endpoints: web: exposure: include: health,info,prometheus endpoint: health: show-details: when-authorized status: order: DOWN,OUT_OF_SERVICE,UNKNOWN,UP http-mapping: UP: 200 DOWN: 503 OUT_OF_SERVICE: 503 UNKNOWN: 503 info: env: enabled: trueThe custom status order treats UNKNOWN as more severe than UP. This is useful if a custom health indicator reports UNKNOWN and you want the overall endpoint to return HTTP 503. Other indicators still take part in the aggregate status; this configuration does not give one indicator exclusive control.
The source document also exposes env and sets both health.show-details and env.show-values to always. Those settings can help during local debugging, but exposing environment values can reveal credentials. Enable them only in a controlled environment. In production, protect management endpoints with authentication and network access controls. Enabling management.info.env.enabled includes configured info.* properties in /actuator/info.
Export component health as a metric
/actuator/health reports the health of dependencies such as databases and Redis. It does not automatically create the custom health_status metric used in this guide.
The following component registers one gauge for each top-level HealthIndicator. It reports 1 for UP and 0 for any other status, timeout, or failure.
import io.micrometer.core.instrument.Gauge;import io.micrometer.core.instrument.MeterRegistry;import jakarta.annotation.PreDestroy;import org.springframework.boot.actuate.health.Health;import org.springframework.boot.actuate.health.HealthContributorRegistry;import org.springframework.boot.actuate.health.HealthIndicator;import org.springframework.boot.actuate.health.Status;import org.springframework.boot.context.event.ApplicationReadyEvent;import org.springframework.context.event.EventListener;import org.springframework.stereotype.Component;
import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.ExecutionException;import java.util.concurrent.Future;import java.util.concurrent.RejectedExecutionException;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;import java.util.concurrent.TimeoutException;
@Componentpublic class HealthMetricsExportBinder { private static final int CHECK_TIMEOUT_MS = 200;
private final ThreadPoolExecutor healthCheckPool = new ThreadPoolExecutor( 6, 6, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(32), new ThreadPoolExecutor.AbortPolicy());
private final HealthContributorRegistry contributors; private final MeterRegistry meterRegistry;
public HealthMetricsExportBinder( HealthContributorRegistry contributors, MeterRegistry meterRegistry) { this.contributors = contributors; this.meterRegistry = meterRegistry; }
@EventListener(ApplicationReadyEvent.class) public void registerHealthGauges() { contributors.stream().forEach(named -> { if (named.getContributor() instanceof HealthIndicator indicator) { Gauge.builder("health_status", indicator, this::healthValue) .description("Component health: 1 for UP, 0 otherwise") .tag("component", named.getName()) .register(meterRegistry); } }); }
private double healthValue(HealthIndicator indicator) { Future<Health> future = null; try { future = healthCheckPool.submit(indicator::health); Health health = future.get(CHECK_TIMEOUT_MS, TimeUnit.MILLISECONDS); return Status.UP.equals(health.getStatus()) ? 1.0 : 0.0; } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return 0.0; } catch (TimeoutException | ExecutionException | RejectedExecutionException ex) { return 0.0; } finally { if (future != null && !future.isDone()) { future.cancel(true); } } }
@PreDestroy public void destroy() { healthCheckPool.shutdownNow(); }}The health checks run in a separate pool, but the scrape still waits for each result. The 200 ms timeout applies to each check, not to the entire scrape. Choose a timeout that suits your dependencies.
This example adds a bounded queue and cancellation to the source’s approach. Cancellation cannot guarantee that a blocked database or network call stops, so configure timeouts in the underlying clients too. For larger systems, refresh health in the background and expose cached values. Composite contributors require traversal of their child indicators; the example above deliberately handles only top-level indicators.
The gauge is independent of management.endpoint.health.show-details.
Verify the output
Start the application and request:
curl http://localhost:8080/actuator/prometheusWith the custom binder installed, the response may include:
# HELP health_status Component health: 1 for UP, 0 otherwise# TYPE health_status gaugehealth_status{component="ping"} 1.0health_status{component="diskSpace"} 1.0Database and other component series appear when their indicators are registered and supported by the binder.
2. Know the Main Metric Groups
| Group | Typical metric names or prefixes | What they describe |
|---|---|---|
| HTTP requests | http_server_requests_seconds_* | Traffic, latency, status codes, methods, and routes |
| Dependency health | health_status | Custom component health gauges |
| JVM | jvm_* | Memory, garbage collection, threads, class loading, and compilation |
| Web container | tomcat_threads_*, jetty_threads_* | Container thread usage and limits |
| Process and system | process_*, system_* | CPU, file descriptors, uptime, and load |
| Database pools | jdbc_connections_*, hikaricp_*, custom druid_* | Active, idle, and waiting connections |
| Logging | logback_events_total | Log events by severity |
| Caches | cache_* | Hits, misses, evictions, and size, where supported |
| Executors | executor_* | Active threads, queued tasks, and completed work |
| Startup | application_started_time_seconds, application_ready_time_seconds | Startup and readiness duration |
| Business activity | Application-defined names | Orders, payments, and other domain events |
In the queries below, job and instance are Prometheus target labels. svc is an optional service label that you must supply through your metrics or scrape configuration. Adjust grouping labels to match your deployment.
3. HTTP Requests
Request volume and requests per second
http_server_requests_seconds_count is a counter of recorded HTTP requests. Use rate() to calculate requests per second over a time window.
# Requests per second by servicesum by (job, svc) (rate(http_server_requests_seconds_count[5m]))
# Requests per second by route and methodsum by (job, svc, uri, method) (rate(http_server_requests_seconds_count[5m]))
# The ten busiest routestopk(10, sum by (job, svc, uri) (rate(http_server_requests_seconds_count[5m])))Error rates
These queries return percentages for error rates and requests per second for the status-code breakdown.
# Server error rate: HTTP 5xxsum by (job) (rate(http_server_requests_seconds_count{status=~"5.."}[5m]))/sum by (job) (rate(http_server_requests_seconds_count[5m])) * 100
# Server error rate by routesum by (job, svc, uri) (rate(http_server_requests_seconds_count{status=~"5.."}[5m]))/sum by (job, svc, uri) (rate(http_server_requests_seconds_count[5m])) * 100
# Client error rate: HTTP 4xxsum by (job) (rate(http_server_requests_seconds_count{status=~"4.."}[5m]))/sum by (job) (rate(http_server_requests_seconds_count[5m])) * 100
# Request rate by status codesum by (job, status) (rate(http_server_requests_seconds_count[5m]))If no matching error series exists, the error-rate query may return no result. If there is no traffic, division by zero can produce NaN. Account for these cases in dashboards and use a minimum traffic condition for percentage-based alerts.
Response time
| Metric | Type | Meaning |
|---|---|---|
http_server_requests_seconds_sum | Counter | Total duration of recorded requests, in seconds |
http_server_requests_seconds_count | Counter | Number of recorded requests |
http_server_requests_seconds_bucket | Histogram bucket counter | Requests within each le duration boundary; requires histogram configuration |
http_server_requests_seconds_max | Gauge | Maximum recorded duration within the recent statistics window |
Count and total duration are available without histogram buckets. To calculate P50, P95, or P99 with histogram_quantile(), enable a histogram:
management: metrics: distribution: percentiles-histogram: http.server.requests: trueMerge this into the existing management section. Histograms increase the number of time series, especially when a metric has many label combinations.
# Average response time in secondssum by (job) (rate(http_server_requests_seconds_sum[5m]))/sum by (job) (rate(http_server_requests_seconds_count[5m]))
# Average response time by route, in millisecondssum by (job, svc, uri) (rate(http_server_requests_seconds_sum[5m]))/sum by (job, svc, uri) (rate(http_server_requests_seconds_count[5m])) * 1000
# Recent maximum response time for each recorded label combinationhttp_server_requests_seconds_max
# P95 response time in seconds; requires histogram bucketshistogram_quantile( 0.95, sum by (le, job) (rate(http_server_requests_seconds_bucket[5m])))The source also discusses http_server_requests_active_seconds_duration_sum and http_server_requests_active_seconds_active_count. Do not assume these exist in a standard Spring MVC application. They require suitable active-request instrumentation. If available, their ratio gives the mean elapsed time of requests still in progress:
http_server_requests_active_seconds_duration_sum/http_server_requests_active_seconds_active_countThe total elapsed duration of currently active requests is a gauge: it can fall when requests finish. It is not a counter of completed request duration.
As initial thresholds, consider a warning above 500 ms average latency and a critical alert above one second. Adjust these values to the service’s requirements. Compare latency with request volume to find busy, slow routes, and use percentiles to reveal delays hidden by averages.
4. Dependency Health
The custom health_status gauge uses a simple convention: 1 means healthy; 0 means the check did not report UP.
# Unhealthy componentshealth_status == 0
# Healthy componentshealth_status == 1
# Number of unhealthy components per instancecount by (job, instance) (health_status == 0)
# Select instances with at least one unhealthy componentmin by (job, instance) (health_status) != 1
# Equivalent selection using a countcount by (job, instance) (health_status == 0) > 0PromQL comparisons filter series unless you add bool. For example, min(...) != 1 retains an unhealthy instance with its value of 0; it does not convert that value to 1. Alert rules activate when a result series is present. See the Prometheus comparison operator documentation.
Missing health data is a separate condition. Monitor scrape failures with up == 0 as well, since an unreachable application cannot report its own health.
5. JVM Metrics
Memory
JVM memory metrics distinguish two areas:
area="heap": memory for ordinary objects and arrays, managed by garbage collection and influenced by-Xmsand-Xmx.area="nonheap": memory used for JVM facilities such as class metadata and compiled code; it is not governed by the heap’s-Xmxlimit.
| Area | Example pool names | Purpose |
|---|---|---|
| Heap | G1 Eden Space, PS Eden Space | Allocation of new objects |
| Heap | G1 Survivor Space, PS Survivor Space | Objects that survive young-generation collection |
| Heap | G1 Old Gen, PS Old Gen, Tenured Gen | Longer-lived objects |
| Non-heap | Metaspace | Class metadata |
| Non-heap | Compressed Class Space | Class metadata associated with compressed class pointers |
| Non-heap | CodeCache, CodeHeap... | Machine code produced by the JIT compiler |
Pool names depend on the collector and JDK. ZGC and Shenandoah may expose different layouts, so inspect the actual id labels before writing pool-specific queries.
| Metric | Type | Meaning |
|---|---|---|
jvm_memory_used_bytes | Gauge | Memory currently used in a pool |
jvm_memory_committed_bytes | Gauge | Memory committed for JVM use; not necessarily resident physical memory |
jvm_memory_max_bytes | Gauge | Reported pool limit; -1 means the limit is undefined |
The source also lists jvm_memory_init_bytes. Verify its availability before using it; it is not part of every standard Micrometer export. Direct buffer memory is reported separately through metrics such as jvm_buffer_memory_used_bytes.
# Heap usage by pooljvm_memory_used_bytes{area="heap"}
# Usage percentage for pools with a defined positive limitjvm_memory_used_bytes{area="heap"}/(jvm_memory_max_bytes{area="heap"} > 0) * 100
# Committed heap memory and reported limitsjvm_memory_committed_bytes{area="heap"}jvm_memory_max_bytes{area="heap"}
# Metaspace usagejvm_memory_used_bytes{area="nonheap", id="Metaspace"}
# Code cache usage; adapt the pool pattern to your JVMjvm_memory_used_bytes{area="nonheap", id=~"Code.*"}
# Old-generation growth in bytes per second over one hourderiv(jvm_memory_used_bytes{area="heap", id=~".*Old.*|Tenured Gen"}[1h])The percentage query is per pool, not a universal whole-heap calculation. Some collectors report undefined limits for individual pools. Validate any whole-heap ratio against the collector’s memory layout.
Sustained usage above 90% deserves attention. An old generation that keeps growing after collection may indicate a leak, while growing Metaspace may point to repeated class generation or retained class loaders. Compare used, committed, and maximum memory before changing JVM settings. A rising trend alone does not prove a leak.
Garbage collection
| Metric | Type | Meaning |
|---|---|---|
jvm_gc_pause_seconds_count | Counter | Recorded GC pause events |
jvm_gc_pause_seconds_sum | Counter | Total recorded pause time in seconds |
jvm_gc_pause_seconds_max | Gauge | Largest pause in the recent statistics window |
jvm_gc_memory_allocated_bytes_total | Counter | Estimated allocated bytes from GC observations |
jvm_gc_memory_promoted_bytes_total | Counter | Estimated bytes promoted to the old generation |
jvm_gc_live_data_size_bytes | Gauge | Reported long-lived data size after relevant collection events |
jvm_gc_max_data_size_bytes | Gauge | Reported maximum size of the long-lived memory pool |
The pause timer can support percentile queries when histogram buckets are enabled. The meaning and availability of GC measurements depend on the collector.
# Pause events per second, retaining collector and cause labelsrate(jvm_gc_pause_seconds_count[5m])
# Average pause duration in secondsrate(jvm_gc_pause_seconds_sum[5m])/rate(jvm_gc_pause_seconds_count[5m])
# Major collection events over the last hour, where this label is usedincrease(jvm_gc_pause_seconds_count{action="end of major GC"}[1h])
# Total major collection pause time over the last hourincrease(jvm_gc_pause_seconds_sum{action="end of major GC"}[1h])
# Minor collection events over the last hourincrease(jvm_gc_pause_seconds_count{action="end of minor GC"}[1h])
# Recorded GC pause time as a percentage of elapsed time, per instancesum by (job, instance) (rate(jvm_gc_pause_seconds_sum[5m])) * 100
# Allocation and promotion rates, in bytes per secondrate(jvm_gc_memory_allocated_bytes_total[5m])rate(jvm_gc_memory_promoted_bytes_total[5m])Subtracting cumulative promoted bytes from cumulative allocated bytes does not measure memory reclaimed by a collection. Use before-and-after heap observations or GC logs for that analysis.
GC labels add context:
| Label | Example value | Interpretation |
|---|---|---|
action | end of minor GC | A young-generation collection event on collectors that use this label |
action | end of major GC | A major collection event; confirm its meaning for your collector |
cause | G1 Evacuation Pause | A G1 evacuation pause |
cause | Allocation Failure | An allocation could not be satisfied without collection |
cause | Metadata GC Threshold | Metadata usage reached a collection threshold |
cause | System.gc() | An explicit GC request |
cause | Heap Dump Initiated GC | Collection initiated while taking a heap dump |
cause | Ergonomics | Collection triggered by JVM adaptive policy |
cause | G1 Humongous Allocation | Allocation of an object larger than half a G1 region |
cause | G1 Mixed GC | A mixed collection, if exposed under this label |
cause | Concurrent Mode Failure | A legacy CMS collector could not finish concurrent collection in time |
cause | Promotion Failed | Objects could not be promoted successfully |
gc | G1 Young Generation, G1 Old Generation | G1 collector names |
gc | PS Scavenge, PS MarkSweep | Parallel collector names |
gc | CMS, ZGC, Shenandoah | Other collector names, depending on JDK and instrumentation |
These are examples across JVM generations, not labels guaranteed in Spring Boot 3.5. CMS belongs to older JDKs.
The source proposes investigating more than one Full GC per hour, a Full GC pause above three seconds, or recorded pause time above 5% of elapsed time. Treat these as starting points. Frequent young collections may reflect allocation pressure; poor old-generation recovery after major collections may indicate retained objects. Tune alerts to the collector and the service’s latency requirements.
Threads
| Metric | Type | Meaning |
|---|---|---|
jvm_threads_live_threads | Gauge | Current live thread count, including daemon threads |
jvm_threads_daemon_threads | Gauge | Current daemon thread count |
jvm_threads_peak_threads | Gauge | Peak live thread count since startup or the last peak reset |
jvm_threads_states_threads | Gauge | Thread count by the state label |
jvm_threads_started_threads_total | Counter | Total threads started |
# Current, peak, and daemon thread countsjvm_threads_live_threadsjvm_threads_peak_threadsjvm_threads_daemon_threads
# Distribution by thread statejvm_threads_states_threads
# Growth in live threads over one hourderiv(jvm_threads_live_threads[1h])
# Deadlocked threads, if a binder exports this metricjvm_threads_deadlocked_threadsLook for sustained thread growth and unexpected changes in state distribution. Many runnable threads can indicate CPU pressure or busy loops. Blocked threads may indicate lock contention, but waiting threads are often normal in idle pools. Use a thread dump to investigate suspected deadlocks. Thread-state label values may be lowercase with hyphens, so copy them from the exported metrics.
Class loading
jvm_classes_loaded_classes measures the number of classes currently loaded. jvm_classes_unloaded_classes_total counts classes unloaded since startup.
# Currently loaded classesjvm_classes_loaded_classes
# Classes unloaded per secondrate(jvm_classes_unloaded_classes_total[5m])The source also uses jvm_classes_loaded_classes_total as a cumulative load counter. Do not assume that metric is exported by the standard binder. If your instrumentation provides it, query its rate:
rate(jvm_classes_loaded_classes_total[5m])A sudden rise in loaded classes can accompany generated proxies, dynamic bytecode, or reloads. Compare it with Metaspace growth. A stable unload counter is normal for many applications; it suggests a class-loader leak only when classes should be unloaded but remain retained.
JIT compilation
Just-in-time compilation converts bytecode into machine code while the application runs. It lets the JVM optimize frequently executed code using runtime information.
jvm_compilation_time_seconds_total measures cumulative compilation time when the JVM supports compilation monitoring. Spring Boot’s JVM metrics include JIT compilation instrumentation; an extra library is not inherently required. See the Spring Boot JVM metrics documentation.
Compilation metrics are especially useful when investigating warmup, dynamic code generation, or changing class-loading behavior.
6. Tomcat Threads
With embedded Tomcat, enable server.tomcat.mbeanregistry.enabled as shown earlier, then restart the application.
| Metric | Type | Meaning |
|---|---|---|
tomcat_threads_busy_threads | Gauge | Threads currently handling work |
tomcat_threads_current_threads | Gauge | Current thread count, including idle threads |
tomcat_threads_config_max_threads | Gauge | Configured maximum thread count |
The source also names tomcat_threads_config_min_spare_threads and tomcat_threads_queue_remaining_capacity. These require additional instrumentation; they are not standard metrics from the built-in binder. A worker task queue and the socket accept backlog are different resources and should be measured separately.
# Tomcat worker utilizationtomcat_threads_busy_threads / tomcat_threads_config_max_threadsA ratio above 0.70 is a useful warning starting point; above 0.90 may warrant urgent investigation. High worker utilization does not necessarily mean high CPU use. Threads may be waiting on a database, an external HTTP call, or a lock.
As the pool fills, request latency and queueing can rise. Check downstream bottlenecks before increasing the maximum thread count.
7. Process and System Metrics
CPU
| Metric | Type | Meaning |
|---|---|---|
process_cpu_usage | Gauge | Recent JVM process CPU utilization, normally expressed from 0 to 1 |
system_cpu_usage | Gauge | Recent system CPU utilization visible to the JVM |
system_cpu_count | Gauge | Available processor count reported to the JVM |
# Recent process CPU utilization as a percentageprocess_cpu_usage * 100
# Recent system CPU utilization as a percentagesystem_cpu_usage * 100
# Processor count visible to the JVMsystem_cpu_countDo not divide process_cpu_usage by system_cpu_count as a general normalization step. The process gauge already comes from the JVM’s CPU-load calculation. Container limits and JDK behavior affect its interpretation.
For cumulative process CPU time, inspect the exported name and unit. The source uses process_cpu_time_seconds_total, but Micrometer’s binder registers process.cpu.time with a nanosecond base unit, so an export may instead use process_cpu_time_ns_total. Apply rate() or increase() to the counter actually present and convert its units as needed. See the Micrometer processor binder.
File descriptors
process_files_open reports open file descriptors, including sockets. process_files_max reports the allowed limit on supported platforms.
# File descriptor utilizationprocess_files_open / process_files_maxA rising ratio may indicate leaked files or connections. Investigate before the process reaches its limit.
Uptime and restarts
| Metric | Type | Meaning |
|---|---|---|
process_uptime_seconds | Gauge | JVM uptime in seconds |
process_start_time_seconds | Gauge | Process start time as a Unix timestamp |
# Process age in secondstime() - process_start_time_seconds
# Select processes started within the last five minutestime() - process_start_time_seconds < 300The start timestamp normally lies in the past. A small process age indicates a recent start, which may be a restart, deployment, or newly added instance.
8. Database Connection Pools
Metric names depend on the connection pool. Spring Boot provides generic jdbc.connections.* instrumentation for supported data sources, while HikariCP has its own hikaricp.* metrics.
The source example uses Druid. The following binder exposes a small set of Druid gauges; it does not reproduce the full Druid monitoring console.
Add Druid
The source uses this dependency version:
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.23</version></dependency>Register the gauges
This version uses Spring bean names as labels so multiple unnamed data sources do not share the same metric identity.
import com.alibaba.druid.pool.DruidDataSource;import io.micrometer.core.instrument.Gauge;import io.micrometer.core.instrument.MeterRegistry;import io.micrometer.core.instrument.binder.MeterBinder;import org.springframework.stereotype.Component;
import javax.sql.DataSource;import java.util.Map;
@Componentpublic class DruidDataSourceMeterBinder implements MeterBinder { private final Map<String, DataSource> dataSources;
public DruidDataSourceMeterBinder(Map<String, DataSource> dataSources) { this.dataSources = dataSources; }
@Override public void bindTo(MeterRegistry registry) { dataSources.forEach((name, dataSource) -> { if (dataSource instanceof DruidDataSource druid) { Gauge.builder("druid.connections.active", druid, DruidDataSource::getActiveCount) .description("Active Druid connections") .tag("datasource", name) .register(registry);
Gauge.builder("druid.connections.idle", druid, DruidDataSource::getPoolingCount) .description("Idle Druid connections") .tag("datasource", name) .register(registry);
Gauge.builder("druid.connections.pending", druid, DruidDataSource::getWaitThreadCount) .description("Threads waiting for a Druid connection") .tag("datasource", name) .register(registry);
Gauge.builder("druid.connections.max", druid, DruidDataSource::getMaxActive) .description("Maximum Druid connections") .tag("datasource", name) .register(registry);
Gauge.builder("druid.connections.min", druid, DruidDataSource::getMinIdle) .description("Minimum idle Druid connections") .tag("datasource", name) .register(registry); } }); }}The example handles direct DruidDataSource beans. Routing data sources and wrappers may need additional handling.
| Prometheus metric | Druid method | Meaning |
|---|---|---|
druid_connections_active | getActiveCount() | Connections currently in use |
druid_connections_idle | getPoolingCount() | Idle connections |
druid_connections_pending | getWaitThreadCount() | Threads waiting for a connection |
druid_connections_max | getMaxActive() | Maximum active connections |
druid_connections_min | getMinIdle() | Configured minimum idle connections |
All five are gauges, with a datasource label to distinguish pools.
# Pool utilizationdruid_connections_active / druid_connections_max
# Ten most heavily used pools, as percentagestopk(10, druid_connections_active / druid_connections_max * 100)
# Threads waiting for connectionsdruid_connections_pendingConsider warning above 80% utilization and escalating above 90%, especially when pending requests also rise. Persistent saturation can result from slow queries, long transactions, leaked connections, or insufficient capacity. Increasing the pool is useful only if the database can handle the extra concurrency.
9. Logging
logback_events_total counts logging events by severity. Use the label values actually exported by your binder; standard Micrometer Logback metrics use lowercase levels.
# Error events per secondrate(logback_events_total{level="error"}[1m])
# Warning events per secondrate(logback_events_total{level="warn"}[1m])
# Error events during the last minuteincrease(logback_events_total{level="error"}[1m])These counters reveal changes in error volume. Use the logs themselves to understand the failures.
10. Caches
The source’s Tomcat example includes these resource-cache counters:
# TYPE tomcat_cache_access_total countertomcat_cache_access_total 0.0# TYPE tomcat_cache_hit_total countertomcat_cache_hit_total 0.0Tomcat’s resource cache is separate from an application’s business cache. Application cache metrics under cache_* depend on the cache provider, enabled statistics, and registered instrumentation. Ehcache, Redis, and other providers need their own interpretation; this guide does not cover them in detail.
11. Executors and Scheduled Tasks
Spring Boot can instrument supported ThreadPoolTaskExecutor and ThreadPoolTaskScheduler beans. Metrics use the executor_ prefix and a name label identifying the bean.
# TYPE executor_pool_max_threads gaugeexecutor_pool_max_threads{name="myTaskExecutor"} 10.0| Metric | Type | Meaning |
|---|---|---|
executor_active_threads | Gauge | Threads executing tasks |
executor_pool_size_threads | Gauge | Current pool size |
executor_pool_core_threads | Gauge | Configured core pool size |
executor_pool_max_threads | Gauge | Configured maximum pool size |
executor_queued_tasks | Gauge | Tasks waiting in the queue |
executor_queue_remaining_tasks | Gauge | Remaining queue capacity |
executor_completed_tasks_total | Counter | Completed tasks |
Inspect the exported set for each executor implementation. Rising queue depth shows that work is arriving faster than it completes. A nearly full bounded queue can lead to rejected tasks. Check task duration, downstream delays, and queue policy before changing pool size. An unbounded queue can accumulate work even when the configured maximum thread count looks generous.
12. Application Startup
| Metric | Meaning |
|---|---|
process_uptime_seconds | Time since the JVM started; grows continuously |
application_started_time_seconds | Application startup duration recorded at ApplicationStartedEvent |
application_ready_time_seconds | Duration until ApplicationReadyEvent, after application runners finish |
The two application metrics are gauges recorded during startup. Their timing is based on application startup, which is distinct from JVM process uptime.
JVM starts -> Process uptime begins -> Spring application startup begins -> Application context is refreshed -> ApplicationStartedEvent records startup duration -> CommandLineRunner and ApplicationRunner execute -> ApplicationReadyEvent records readiness duration# Time between the started and ready milestonesapplication_ready_time_seconds - application_started_time_secondsA large difference often points to warmup tasks, data loading, or remote calls in application runners. It can include other work between the two events, so it is not an exact profiler of runner execution.
Kubernetes routes traffic according to its configured readiness probe. Recording an application-ready metric does not, by itself, configure that probe.
13. Business Metrics
Infrastructure metrics explain how a service runs. Business metrics explain whether it is doing useful work.
Examples include orders created, payments completed, and payment failures. Their design depends on the application, so the source leaves detailed instrumentation outside this guide’s scope.
14. Suggested Alert Thresholds
The following rules are starting points from the source document. Tune them to normal traffic, service objectives, and workload behavior. The for duration specifies how long a condition must remain true before an alert fires.
| Alert | Severity | for | Suggested condition |
|---|---|---|---|
AppHighAvgResponseTime | Warning | 5m | Average response time above 500 ms |
AppCriticalAvgResponseTime | Critical | 5m | Average response time above 1 second |
AppHigh5xxErrorRate | Critical | 5m | Server error rate above 1% |
AppHigh4xxErrorRate | Warning | 5m | Client error rate above 5% |
AppQpsAnomaly | Warning | 5m | Request rate changes by more than 50% against a defined comparable baseline |
AppComponentUnhealthy | Critical | 1m | min by (job, instance) (health_status) != 1 |
AppHeapUsageHigh | Warning | 5m | Validated heap utilization above 80% |
AppHeapUsageCritical | Critical | 5m | Validated heap utilization above 90% |
AppTomcatThreadsHigh | Warning | 5m | Busy threads / maximum threads above 0.70 |
AppTomcatThreadsCritical | Critical | 5m | Busy threads / maximum threads above 0.90 |
AppDruidPoolUsageHigh | Warning | 5m | Active connections / maximum connections above 0.80 |
AppDruidPoolUsageCritical | Critical | 5m | Active connections / maximum connections above 0.90 |
Keep instance and data-source labels where they help identify the failing resource. Avoid treating normal low-traffic variation as an incident, and handle missing data separately from healthy values.
15. Build a Useful Monitoring Practice
Start with a small set of signals: request volume, 5xx error rate, heap usage, major GC activity, and database pool activity and waiters. Together, they cover traffic, availability, memory pressure, and database access. Problems in one area often spread to the others.
Expose only the Actuator endpoints you need, protect them in production, and use Micrometer and Prometheus to collect metrics. Build dashboards and alerts in Grafana or your chosen monitoring tools. Set thresholds early enough to act before users experience a serious failure. Continuous profiling can help explain slow calls and unexpected memory retention.
As the system matures, add business metrics, connect metrics with distributed traces, and keep alert rules in version control. Review those rules as the application changes so the monitoring continues to reflect the service it protects.