Skip to main content

Micrometer Metrics Bridge (juneau-rest-server-metrics-micrometer)

The juneau-rest-server-metrics-micrometer module bridges every @RestOp call to a Micrometer MeterRegistry, enabling Prometheus / Grafana dashboards, StatsD, Datadog, New Relic, and any other Micrometer-backed backend with a single bean registration.

This page covers the module-specific classes and wiring patterns. The shared opt-in contract, the MetricsRecorder SPI, the per-resource / per-operation annotation overrides, and the custom (non-request) observation API are described in Observability — Micrometer + OpenTelemetry.

Maven dependency​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-metrics-micrometer</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.13.6</version> <!-- consumer-supplied; provided scope on the bridge module -->
</dependency>
<!-- pick the registry you want; example uses Prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.13.6</version>
</dependency>

micrometer-core is declared in provided scope on the bridge module. Consumers pick their own Micrometer version (and the registry that matches their backend); core juneau-rest-server stays Micrometer-free.

Classes in this module​

ClassRole
MicrometerMetricsRecorderMetricsRecorder impl that records to a MeterRegistry.
MetricsMixin@Rest(mixins=...) flavor of a /metrics Prometheus scrape endpoint.
MetricsResource@Rest(children=...) flavor of the same endpoint, routed at /metrics.
MetricsManagerInternal worker used by both endpoint flavors; consumers do not use it directly.

MicrometerMetricsRecorder​

MicrometerMetricsRecorder is the MetricsRecorder bridge. Register one as a @Bean on your resource (or in a Spring @Configuration) to activate per-request metrics:

@Configuration
public class ObservabilityConfig {

@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}

@Bean
public MetricsRecorder metricsRecorder(MeterRegistry r) {
return new MicrometerMetricsRecorder(r);
}
}

Constructors​

// Default timer name "http.server.requests" (matches Spring Boot's WebMvcMetricsFilter convention).
new MicrometerMetricsRecorder(MeterRegistry registry)

// Custom timer name — use when an existing dashboard uses a different name.
new MicrometerMetricsRecorder(MeterRegistry registry, String timerName)

The getRegistry() and getTimerName() accessors are available for testing / introspection.

Metric shape​

Each @RestOp call records one Timer sample. Default timer name: http.server.requests.

TagValue
methodUppercased HTTP method (GET, POST, …).
uriThe @RestOp path template (e.g. /users/{id}), not the raw URI — keeps cardinality bounded. Empty string when the operation has no path pattern.
statusHTTP response status code as a string (200, 404, 500, …).
exceptionThrown exception simple-name, or None on a normal completion.

The tag set matches Spring Boot's WebMvcMetricsFilter convention so existing Prometheus / Grafana dashboards work without modification when a service migrates from Spring Boot to Juneau:

http_server_requests_seconds_count{method="GET",uri="/users/{id}",status="200",exception="None"} 1
http_server_requests_seconds_sum {method="GET",uri="/users/{id}",status="200",exception="None"} 0.007

Custom timer name​

@Bean public MetricsRecorder recorder(MeterRegistry r) {
return new MicrometerMetricsRecorder(r, "myservice.http.timer");
}

Per-operation metric name and tags​

Override the timer name or add extra tags for individual operations using the @RestOp attributes (see Observability — per-resource and per-operation controls):

@RestGet("/orders/{id}")
@RestOp(metricName="orders.get", metricTags="team=payments,region=us-east")
public Order getOrder(@Path String id) { ... }

When metricName is non-empty it overrides the recorder-level timer name for that operation. metricTags are merged into the standard tag set as additional key=value pairs.

Custom (non-request) observations​

MicrometerMetricsRecorder also overrides the non-request record(String metricName, String metricTags, Duration elapsed, Throwable error) default method, so it works with Observations.observe(...) for timing arbitrary code blocks outside the request boundary:

try (Observation o = Observations.observe("order.load", "team=payments")) {
return loadOrder(id);
} catch (RuntimeException e) {
o.setError(e);
throw e;
}

The non-request record emits a Timer named metricName carrying the exception tag plus any caller-supplied metricTags. There are no method/uri/status tags because there is no request.

/metrics endpoint — Prometheus scrape​

The module ships two pre-built endpoint components that expose the MeterRegistry as a Prometheus scrape surface. Both delegate to the same internal MetricsManager worker so they cannot drift. When no PrometheusMeterRegistry bean is registered, both degrade to HTTP 501 (Not Implemented).

MetricsMixin — add /metrics to an existing resource​

Use MetricsMixin when you want the scrape endpoint to live on the same path prefix as your API resource:

@Rest(
path="/api",
mixins=MetricsMixin.class // adds GET /api/metrics
)
public class ApiResource extends BasicRestServlet {
// your @RestOp methods
}

GET /api/metrics returns the Prometheus exposition text.

MetricsResource — routed child under /metrics​

Use MetricsResource when you prefer a dedicated child resource mounted at /metrics:

@Rest(
path="/api",
children=MetricsResource.class // mounts GET /api/metrics/*
)
public class ApiResource extends BasicRestServlet {
// your @RestOp methods
}

The child resource inherits its parent's bean store, so the consumer's MeterRegistry bean is reachable without any extra configuration.

Full wiring example​

@Configuration
public class ObservabilityConfig {

@Bean
public MeterRegistry meterRegistry() {
// PrometheusMeterRegistry — scraped by the MetricsMixin/MetricsResource endpoint.
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}

@Bean
public MetricsRecorder metricsRecorder(MeterRegistry r) {
return new MicrometerMetricsRecorder(r);
}
}

@Rest(
path="/api",
observability="true", // fail-fast if no backend is wired
mixins=MetricsMixin.class // exposes GET /api/metrics
)
public class ApiResource extends BasicRestServlet {

@RestGet("/users/{id}")
public User getUser(@Path String id) { ... }

@RestGet("/health")
@RestOp(observability="false") // exclude health-check from metrics
public String health() { return "ok"; }
}

With a PrometheusMeterRegistry in scope, GET /api/metrics returns:

http_server_requests_seconds_count{method="GET",uri="/users/{id}",status="200",exception="None"} 12
http_server_requests_seconds_sum {method="GET",uri="/users/{id}",status="200",exception="None"} 0.054

See also​