Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/accepted-api-changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,5 +273,15 @@
"type": "io.micronaut.configuration.metrics.micrometer.intercept.TimedInterceptor",
"member": "Constructor io.micronaut.configuration.metrics.micrometer.intercept.TimedInterceptor(io.micrometer.core.instrument.MeterRegistry)",
"reason": "Removed deprecated code for Micronaut Framework 5"
},
{
"type": "io.micronaut.configuration.metrics.binder.web.HttpMetricsTagProvider",
"member": "Class io.micronaut.configuration.metrics.binder.web.HttpMetricsTagProvider",
"reason": "Add request-aware HTTP metrics tag provider extension point"
},
{
"type": "io.micronaut.configuration.metrics.binder.web.HttpMetricsTagProvider",
"member": "Method io.micronaut.configuration.metrics.binder.web.HttpMetricsTagProvider.getTags(io.micronaut.http.HttpRequest,io.micronaut.http.HttpResponse,java.lang.Throwable)",
"reason": "Add request-aware HTTP metrics tag provider extension point"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.micronaut.http.filter.HttpClientFilter;
import jakarta.inject.Provider;

import java.util.List;
import java.util.Optional;

import static io.micronaut.core.util.StringUtils.FALSE;
Expand All @@ -51,12 +52,16 @@ final class ClientMetricsFilter {

private static final String START_ATTRIBUTE = ClientMetricsFilter.class.getName() + ".START_ATTRIBUTE";
private final Provider<MeterRegistry> meterRegistryProvider;
private final List<HttpMetricsTagProvider> tagProviders;

/**
* @param meterRegistryProvider the meter registry provider
* @param meterRegistryProvider The meter registry provider
* @param tagProviders The HTTP metrics tag providers
*/
public ClientMetricsFilter(Provider<MeterRegistry> meterRegistryProvider) {
public ClientMetricsFilter(Provider<MeterRegistry> meterRegistryProvider,
List<HttpMetricsTagProvider> tagProviders) {
this.meterRegistryProvider = meterRegistryProvider;
this.tagProviders = tagProviders;
}

@RequestFilter
Expand All @@ -77,12 +82,14 @@ void doException(HttpRequest<?> request, Throwable throwable) {
private WebMetricsHelper createHelper(HttpRequest<?> request) {
return new WebMetricsHelper(
meterRegistryProvider.get(),
request,
resolvePath(request),
request.getAttribute(START_ATTRIBUTE, Long.class).orElseGet(System::nanoTime),
request.getMethod().toString(),
HttpClientMeterConfig.REQUESTS_METRIC,
resolveServiceID(request),
true
true,
tagProviders
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.configuration.metrics.binder.web;

import io.micrometer.core.instrument.Tag;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import org.jspecify.annotations.Nullable;

/**
* Provides custom tags for HTTP client and server request metrics.
*
* @since 6.0.0
*/
@FunctionalInterface
public interface HttpMetricsTagProvider {

/**
* Resolve additional tags for an HTTP request metric.
*
* @param request The HTTP request
* @param response The HTTP response, if available
* @param throwable The request failure, if available
* @return Additional tags for the request metric
*/
Iterable<Tag> getTags(HttpRequest<?> request, @Nullable HttpResponse<?> response, @Nullable Throwable throwable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.micronaut.web.router.UriRouteMatch;
import jakarta.inject.Provider;

import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

Expand All @@ -57,14 +58,19 @@ final class ServerMetricsFilter {
private final Supplier<MeterRegistry> meterRegistryProvider;

private final boolean reportClientErrorURIs;
private final List<HttpMetricsTagProvider> tagProviders;

/**
* @param meterRegistryProvider the meter registry provider
* @param clientErrorsUrisConfig the client errors
* @param tagProviders The HTTP metrics tag providers
*/
public ServerMetricsFilter(Provider<MeterRegistry> meterRegistryProvider, HttpMetricsConfig.ClientErrorsUrisConfig clientErrorsUrisConfig) {
public ServerMetricsFilter(Provider<MeterRegistry> meterRegistryProvider,
HttpMetricsConfig.ClientErrorsUrisConfig clientErrorsUrisConfig,
List<HttpMetricsTagProvider> tagProviders) {
this.meterRegistryProvider = SupplierUtil.memoized(meterRegistryProvider::get);
this.reportClientErrorURIs = clientErrorsUrisConfig.enabled();
this.tagProviders = tagProviders;
}

private String resolvePath(HttpRequest<?> request) {
Expand All @@ -83,12 +89,14 @@ void onRequest(HttpRequest<?> request) {
void onResponse(HttpRequest<?> request, HttpResponse<?> response) {
WebMetricsHelper httpResponseWebMetricsPublisher = new WebMetricsHelper(
meterRegistryProvider.get(),
request,
resolvePath(request),
request.getAttribute(START_ATTRIBUTE, Long.class).orElseGet(System::nanoTime),
request.getMethod().toString(),
HttpServerMeterConfig.REQUESTS_METRIC,
null,
reportClientErrorURIs
reportClientErrorURIs,
tagProviders
);
httpResponseWebMetricsPublisher.onResponse(response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micronaut.core.annotation.Internal;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.micronaut.http.HttpAttributes;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpResponseProvider;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.web.router.ErrorRouteInfo;
import io.micronaut.web.router.RouteMatch;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static java.util.concurrent.TimeUnit.NANOSECONDS;
Expand Down Expand Up @@ -56,15 +58,18 @@
private static final String SERVICE_ID = "serviceId";

private final MeterRegistry meterRegistry;
private final HttpRequest<?> request;
private final String requestPath;
private final long start;
private final String httpMethod;
private final String metricName;
private final String serviceID;
private final boolean reportClientErrorURIs;
private final List<HttpMetricsTagProvider> tagProviders;

/**
* @param meterRegistry MeterRegistry bean
* @param request The HTTP request
* @param requestPath The request path
* @param start The start time of the request
* @param httpMethod The HTTP method name used
Expand All @@ -72,38 +77,63 @@
* @param serviceID The ID of the service called in the request
* @param reportClientErrorURIs Whether client errors provide uris or not
*/
WebMetricsHelper(MeterRegistry meterRegistry,

Check warning on line 80 in micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 9 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-micrometer&issues=AZ3RIcEOT_z8tbmMW8Kx&open=AZ3RIcEOT_z8tbmMW8Kx&pullRequest=1145
HttpRequest<?> request,
String requestPath,
long start,
String httpMethod,
String metricName,
String serviceID,
boolean reportClientErrorURIs) {
boolean reportClientErrorURIs,
List<HttpMetricsTagProvider> tagProviders) {
this.meterRegistry = meterRegistry;
this.request = request;
this.requestPath = requestPath;
this.start = start;
this.httpMethod = httpMethod;
this.metricName = metricName;
this.serviceID = serviceID;
this.reportClientErrorURIs = reportClientErrorURIs;
this.tagProviders = tagProviders;
}

/**
* @param meterRegistry MeterRegistry bean
* @param request The HTTP request
* @param requestPath The request path
* @param start The start time of the request
* @param httpMethod The HTTP method name used
* @param metricName The metric name
* @param serviceID The ID of the service called in the request
* @param reportClientErrorURIs Whether client errors provide uris or not
*/
WebMetricsHelper(MeterRegistry meterRegistry,

Check warning on line 110 in micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 8 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-micrometer&issues=AZ3RIcEOT_z8tbmMW8Ky&open=AZ3RIcEOT_z8tbmMW8Ky&pullRequest=1145
HttpRequest<?> request,
String requestPath,
long start,
String httpMethod,
String metricName,
String serviceID,
boolean reportClientErrorURIs) {
this(meterRegistry, request, requestPath, start, httpMethod, metricName, serviceID, reportClientErrorURIs, Collections.emptyList());
}

/**
* Get the tags for the metrics based on request shape.
*
* @param httpResponse The HTTP response
* @param httpResponse The HTTP response, if available
* @param httpMethod The name of the HTTP method (GET, POST, etc)
* @param requestPath The request path (/foo, /foo/bar, etc)
* @param throwable The throwable (optional)
* @param serviceId the service ID
* @return A list of Tag objects
*/
private static List<Tag> getTags(HttpResponse<?> httpResponse,
String httpMethod,
String requestPath,
Throwable throwable,
String serviceId,
boolean reportClientErrorURIs) {
private List<Tag> getTags(@Nullable HttpResponse<?> httpResponse,
String httpMethod,
String requestPath,
Throwable throwable,
String serviceId,
boolean reportClientErrorURIs) {
List<@NonNull Tag> tags = new ArrayList<>(5);
Tag t1 = method(httpMethod);
if (t1 != null) {
Expand All @@ -116,6 +146,9 @@
if (t5 != null) {
tags.add(t5);
}
for (HttpMetricsTagProvider tagProvider : tagProviders) {
tagProvider.getTags(request, httpResponse, throwable).forEach(tags::add);
}
return tags;
}

Expand Down Expand Up @@ -154,7 +187,7 @@
* @return Tag of URI
*/
@NonNull
private static Tag uri(HttpResponse<?> httpResponse, String path, boolean reportClientErrorURIs) {
private static Tag uri(@Nullable HttpResponse<?> httpResponse, String path, boolean reportClientErrorURIs) {
if (httpResponse != null) {
int code = httpResponse.code();
if (code >= 300 && code < 400) {
Expand Down Expand Up @@ -280,10 +313,10 @@
/**
* Registers the error timer for a web request when an exception occurs.
*
* @param httpResponse existing response object.
* @param httpResponse existing response object, if available.
* @param throwable exception that occurred
*/
public void error(HttpResponse<?> httpResponse, Throwable throwable) {
public void error(@Nullable HttpResponse<?> httpResponse, Throwable throwable) {
if (httpResponse == null && throwable instanceof HttpResponseProvider httpResponseProvider) {
httpResponse = httpResponseProvider.getResponse();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.micronaut.configuration.metrics.binder.web

import groovy.transform.InheritConstructors
import io.micrometer.core.instrument.Tag
import io.micrometer.common.lang.NonNull
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
Expand All @@ -12,10 +13,12 @@ import io.micronaut.configuration.metrics.binder.web.config.HttpServerMeterConfi
import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Requires
import io.micronaut.core.util.CollectionUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Error
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Header
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.http.uri.UriBuilder
Expand All @@ -29,6 +32,7 @@ import reactor.core.publisher.Flux
import spock.lang.PendingFeature
import spock.lang.Specification

import jakarta.inject.Singleton
import jakarta.validation.constraints.NotBlank

import static io.micronaut.configuration.metrics.micrometer.MeterRegistryFactory.MICRONAUT_METRICS_BINDERS
Expand All @@ -38,6 +42,32 @@ import static io.micronaut.http.HttpStatus.NOT_FOUND

class HttpMetricsSpec extends Specification {

void "test client and server metrics include custom tags"() {
when:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, ['spec.name': getClass().getSimpleName()])
def context = embeddedServer.applicationContext
TestClient client = context.getBean(TestClient)

then:
client.tagged("custom-value") == 'ok custom-value'

when:
MeterRegistry registry = context.getBean(MeterRegistry)

then:
registry.get(HttpServerMeterConfig.REQUESTS_METRIC)
.tags('uri', '/test-http-metrics/tagged', 'custom', 'custom-value')
.timer()
.count() == 1
registry.get(HttpClientMeterConfig.REQUESTS_METRIC)
.tags('uri', '/test-http-metrics/tagged', 'custom', 'custom-value')
.timer()
.count() == 1

cleanup:
embeddedServer.close()
}

void "test client / server metrics with #cfg #setting"() {
when:
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer, [(cfg): setting, 'spec.name': getClass().getSimpleName()])
Expand Down Expand Up @@ -285,6 +315,9 @@ class HttpMetricsSpec extends Specification {
@Get("/test-http-metrics/{id}")
String template(String id)

@Get("/test-http-metrics/tagged")
String tagged(@Header("X-Custom-Tag") String customTag)

@Get("/test-http-metrics/error")
HttpResponse error()

Expand All @@ -310,6 +343,9 @@ class HttpMetricsSpec extends Specification {
@Get("/test-http-metrics/{id}")
String template(String id) { "ok " + id }

@Get("/test-http-metrics/tagged")
String tagged(@Header("X-Custom-Tag") String customTag) { "ok " + customTag }

@Get("/test-http-metrics/error")
HttpResponse error() {
HttpResponse.status(CONFLICT)
Expand Down Expand Up @@ -367,6 +403,16 @@ class HttpMetricsSpec extends Specification {

}

@Requires(property = "spec.name", value = "HttpMetricsSpec")
@Singleton
static class TestHttpMetricsTagProvider implements HttpMetricsTagProvider {

@Override
Iterable<Tag> getTags(HttpRequest<?> request, HttpResponse<?> response, Throwable throwable) {
return [Tag.of("custom", request.headers.get("X-Custom-Tag") ?: "none")]
}
}

@InheritConstructors
static class MyException extends RuntimeException {
}
Expand Down
Loading
Loading