Thanks to visit codestin.com
Credit goes to github.com

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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Previously virtual threads were used when at least two CPU cores were available.
Starting with this version, virtual threads are only used when at least four CPU cores are available.
This change should prevent deadlocks due to pinned virtual threads.

=== HTTP client metrics

The HTTP client used by {project_name} now exposes metrics which allow request latency and connection pool status to be monitored.
These metrics are enabled by default when {project_name} is configured with `--metrics-enabled=true`, but can be disabled
by configuring `--http-client-metrics-enabled=false`.

// ------------------------ Deprecated features ------------------------ //
== Deprecated features

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/observability/configuration-metrics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<@tmpl.guide
title="Gaining insights with metrics"
summary="Collect metrics to gain insights about state and activities of a running instance of {project_name}."
includedOptions="metrics-enabled http-metrics-* cache-metrics-*">
includedOptions="metrics-enabled http-metrics-* cache-metrics-* http-client-metrics-*">

{project_name} has built in support for metrics. This {section} describes how to enable and configure server metrics.

Expand Down
44 changes: 44 additions & 0 deletions docs/guides/observability/metrics-for-troubleshooting-http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ m| http_server_bytes_read_sum

include::partials/histogram_note_http.adoc[]

=== Client

The metrics below helps to monitor the performance of the client used by the {project_name} server to make HTTP requests.

Client metrics are enabled by default when `--metrics-enabled=true`, however you can disable them by specifying `--spi-connections-http-client-default-metrics-enabled=false`.

|===
|Metric |Description

m| httpcomponents_httpclient_request_seconds_count
| The total number HttpClient requests for a given URI

m| httpcomponents_httpclient_request_seconds_max
| Duration of the longest HttpClient request for a given URI

m| httpcomponents_httpclient_request_seconds_sum
| Duration of all HttpClient requests for a given URI

m| httpcomponents_httpclient_pool_total_connections
| The number of persistent and available connections for all routes

m| httpcomponents_httpclient_pool_total_pending
| The number of connection requests being blocked awaiting a free connection for all routes.

m| httpcomponents_httpclient_pool_total_max
| The configured maximum number of allowed persistent connections for all routes

m| httpcomponents_httpclient_pool_route_max_default
| The configured default maximum number of allowed persistent connections per route

|===

A `httpcomponents_httpclient_request_seconds` metric is recorded for each unique `host` and `port`, however in order to
prevent cardinality explosion we define the `uri` tag as "UNKNOWN" unless the `X-Metrics-Template` header is explicitly
configured in the request. This prevents endpoints such as `/users/{id}`, registering a metric for every call to
`/users/1`, `/users/2` and so forth. For such paths, it is recommended that the `X-Metrics-Template` be defined as `/users/{id}`.

Similarly, by default we limit the total number of metrics with unique `host`, `port` and `uri` tags to 100. Users can
define the upper limit by setting `http-client-metrics-tag-limit` with the desired value.

You can enable histograms for the `httpcomponents_httpclient_request_seconds` metric by setting `http-client-metrics-histograms-enabled`
to `true`, and add additional buckets for service level objectives using the option `http-client-metrics-slos`.

include::partials/histogram_note_http.adoc[]

== Next steps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;

import static org.keycloak.connections.httpclient.DefaultHttpClientFactory.METRICS_URI_TEMPLATE_HEADER;

public class Ipatuura {
private static final Logger logger = Logger.getLogger(Ipatuura.class);

Expand Down Expand Up @@ -81,10 +83,11 @@ public Integer csrfAuthLogin() {
String password = model.getConfig().getFirst("loginpassword");

/* Execute GET to get initial csrftoken */
String url = String.format("https://%s%s", server, "/admin/login/");
String loginPath = "/admin/login";
String url = String.format("https://%s%s", server, loginPath);

try {
response = SimpleHttp.create(session).doGet(url).asResponse();
response = SimpleHttp.create(session).doGet(url).header(METRICS_URI_TEMPLATE_HEADER, loginPath).asResponse();
parseSetCookie(response);
response.close();
} catch (Exception e) {
Expand All @@ -95,8 +98,14 @@ public Integer csrfAuthLogin() {
/* Perform login POST */
try {
/* Here we retrieve the Response sessionid and csrftoken cookie */
response = SimpleHttp.create(session).doPost(url).header("X-CSRFToken", csrf_value).header("Cookie", csrf_cookie)
.header("referer", url).param("username", username).param("password", password).asResponse();
response = SimpleHttp.create(session).doPost(url)
.header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie)
.header("referer", url)
.header(METRICS_URI_TEMPLATE_HEADER, loginPath)
.param("username", username)
.param("password", password)
.asResponse();

parseSetCookie(response);
response.close();
Expand All @@ -117,12 +126,18 @@ public boolean isValid(String username, String password) {

/* Build URL */
String server = model.getConfig().getFirst("scimurl");
String endpointurl = String.format("https://%s/creds/simple_pwd", server);
String path = "/creds/simple_pwd";
String endpointurl = String.format("https://%s%s", server, path);

logger.debugv("Sending POST request to {0}", endpointurl);
SimpleHttpRequest simpleHttp = SimpleHttp.create(session).doPost(endpointurl).header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie).header("SessionId", sessionid_cookie).header("referer", endpointurl)
.param("username", username).param("password", password);
SimpleHttpRequest simpleHttp = SimpleHttp.create(session).doPost(endpointurl)
.header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie)
.header("SessionId", sessionid_cookie)
.header("referer", endpointurl)
.header(METRICS_URI_TEMPLATE_HEADER, path)
.param("username", username)
.param("password", password);
try (SimpleHttpResponse response = simpleHttp.asResponse()){
JsonNode result = response.asJson();
return (result.get("result").get("validated").asBoolean());
Expand All @@ -136,11 +151,14 @@ public boolean isValid(String username, String password) {
public String gssAuth(String spnegoToken) {

String server = model.getConfig().getFirst("scimurl");
String endpointurl = String.format("https://%s/bridge/login_kerberos/", server);
String path = "/bridge/login_kerberos";
String endpointurl = String.format("https://%s%s", server, path);

logger.debugv("Sending POST request to {0}", endpointurl);
SimpleHttpRequest simpleHttp = SimpleHttp.create(session).doPost(endpointurl).header("Authorization", "Negotiate " + spnegoToken)
.param("username", "");
SimpleHttpRequest simpleHttp = SimpleHttp.create(session).doPost(endpointurl)
.header("Authorization", "Negotiate " + spnegoToken)
.header(METRICS_URI_TEMPLATE_HEADER, path)
.param("username", "");
try (SimpleHttpResponse response = simpleHttp.asResponse()) {
logger.debugv("Response status is {0}", response.getStatus());
return response.getFirstHeader("Remote-User");
Expand All @@ -151,6 +169,10 @@ public String gssAuth(String spnegoToken) {
}

public <T> SimpleHttpResponse clientRequest(String endpoint, String method, T entity) throws Exception {
return clientRequest(endpoint, method, entity, null);
}

public <T> SimpleHttpResponse clientRequest(String endpoint, String method, T entity, String template) throws Exception {
SimpleHttpResponse response = null;

if (!this.logged_in) {
Expand All @@ -168,26 +190,45 @@ public <T> SimpleHttpResponse clientRequest(String endpoint, String method, T en

logger.debugv("Sending {0} request to {1}", method, endpointurl);

String uriMetricsTemplate = template != null ? template : endpoint;
try {
switch (method) {
case "GET":
response = SimpleHttp.create(session).doGet(endpointurl).header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie).header("SessionId", sessionid_cookie).asResponse();
response = SimpleHttp.create(session).doGet(endpointurl)
.header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie)
.header("SessionId", sessionid_cookie)
.header(METRICS_URI_TEMPLATE_HEADER, uriMetricsTemplate)
.asResponse();
break;
case "DELETE":
response = SimpleHttp.create(session).doDelete(endpointurl).header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie).header("SessionId", sessionid_cookie).header("referer", endpointurl)
.asResponse();
response = SimpleHttp.create(session).doDelete(endpointurl)
.header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie)
.header("SessionId", sessionid_cookie)
.header("referer", endpointurl)
.header(METRICS_URI_TEMPLATE_HEADER, uriMetricsTemplate)
.asResponse();
break;
case "POST":
/* Header is needed for domains endpoint only, but use it here anyway */
response = SimpleHttp.create(session).doPost(endpointurl).header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie).header("SessionId", sessionid_cookie)
.header("referer", endpointurl).json(entity).asResponse();
response = SimpleHttp.create(session).doPost(endpointurl)
.header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie)
.header("SessionId", sessionid_cookie)
.header("referer", endpointurl)
.header(METRICS_URI_TEMPLATE_HEADER, uriMetricsTemplate)
.json(entity)
.asResponse();
break;
case "PUT":
response = SimpleHttp.create(session).doPut(endpointurl).header("X-CSRFToken", this.csrf_value)
.header("SessionId", sessionid_cookie).header("Cookie", this.csrf_cookie).json(entity).asResponse();
response = SimpleHttp.create(session).doPut(endpointurl)
.header("X-CSRFToken", this.csrf_value)
.header("SessionId", sessionid_cookie)
.header("Cookie", this.csrf_cookie)
.header(METRICS_URI_TEMPLATE_HEADER, uriMetricsTemplate)
.json(entity)
.asResponse();
break;
default:
logger.warn("Unknown HTTP method, skipping");
Expand Down Expand Up @@ -264,7 +305,7 @@ public SimpleHttpResponse deleteUser(String username) {

SimpleHttpResponse response;
try {
response = clientRequest(userIdUrl, "DELETE", null);
response = clientRequest(userIdUrl, "DELETE", null, "/Users/{id}");
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
Expand Down Expand Up @@ -367,7 +408,7 @@ public SimpleHttpResponse updateUser(Ipatuura ipatuura, String username, String

SimpleHttpResponse response;
try {
response = clientRequest(modifyUrl, "PUT", user);
response = clientRequest(modifyUrl, "PUT", user, "/Users/{id}");
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,27 @@ public enum ClientAuth {
.defaultValue(Boolean.FALSE)
.build();

public static final Option<Boolean> HTTP_CLIENT_METRICS_ENABLED = new OptionBuilder<>("http-client-metrics-enabled", Boolean.class)
.category(OptionCategory.HTTP)
.description("Whether to register client metrics when metrics are enabled on the server.")
.defaultValue(Boolean.TRUE)
.build();

public static final Option<Boolean> HTTP_CLIENT_METRICS_HISTOGRAMS_ENABLED = new OptionBuilder<>("http-client-metrics-histograms-enabled", Boolean.class)
.category(OptionCategory.HTTP)
.description("Enables a histogram with default buckets for the duration of client HTTP requests.")
.defaultValue(Boolean.FALSE)
.build();

public static final Option<String> HTTP_CLIENT_METRICS_SLOS = new OptionBuilder<>("http-client-metrics-slos", String.class)
.category(OptionCategory.HTTP)
.description("Service level objectives for client HTTP requests. Use this instead of the default histogram, or use it in combination to add additional buckets. " +
"Specify a list of comma-separated values defined in milliseconds. Example with buckets from 5ms to 10s: 5,10,25,50,250,500,1000,2500,5000,10000")
.build();

public static final Option<Integer> HTTP_CLIENT_METRICS_TAG_LIMIT = new OptionBuilder<>("http-client-metrics-tag-limit", Integer.class)
.category(OptionCategory.HTTP)
.defaultValue(100)
.description("The maximum number of unique client metrics that can be present for the 'host', 'port' and 'uri' tags.")
.build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ public List<PropertyMapper<?>> getPropertyMappers() {
.to("quarkus.rest.jackson.optimization.enable-reflection-free-serializers")
.build(),
fromOption(HttpOptions.HTTP_ACCEPT_NON_NORMALIZED_PATHS)
.build(),
fromOption(HttpOptions.HTTP_CLIENT_METRICS_ENABLED)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.build(),
fromOption(HttpOptions.HTTP_CLIENT_METRICS_HISTOGRAMS_ENABLED)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.build(),
fromOption(HttpOptions.HTTP_CLIENT_METRICS_SLOS)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.paramLabel("list of buckets")
.build(),
fromOption(HttpOptions.HTTP_CLIENT_METRICS_TAG_LIMIT)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.to("kc.spi-connections-http-client--default--metrics-tag-limit")
.build()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,58 @@
@Singleton
public class HistogramMeterFilter implements MeterFilter {

private boolean histogramsEnabled;
private double[] slos;
private final boolean httpClientHistograms;
private final boolean httpServerHistograms;
private final double[] httpClientSLOs;
private final double[] httpServerSLOs;

public HistogramMeterFilter() {
histogramsEnabled = Configuration.isTrue(HttpOptions.HTTP_METRICS_HISTOGRAMS_ENABLED);
Optional<String> slosOption = Configuration.getOptionalKcValue(HttpOptions.HTTP_METRICS_SLOS.getKey());
if (slosOption.isPresent()) {
slos = Arrays.stream(slosOption.get().split(",")).filter(s -> !s.trim().isEmpty()).mapToDouble(s -> TimeUnit.MILLISECONDS.toNanos(Long.parseLong(s))).toArray();
if (slos.length == 0) {
slos = null;
}
}
this.httpClientHistograms = Configuration.isTrue(HttpOptions.HTTP_CLIENT_METRICS_HISTOGRAMS_ENABLED);
this.httpClientSLOs = slo(Configuration.getOptionalKcValue(HttpOptions.HTTP_CLIENT_METRICS_SLOS));
this.httpServerHistograms = Configuration.isTrue(HttpOptions.HTTP_METRICS_HISTOGRAMS_ENABLED);
this.httpServerSLOs = slo(Configuration.getOptionalKcValue(HttpOptions.HTTP_METRICS_SLOS.getKey()));
}

@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if (isHttpClientRequests(id)) {
return histogramConfig(httpClientHistograms, httpClientSLOs).merge(config);
}
if (isHttpServerRequests(id)) {
DistributionStatisticConfig.Builder builder = DistributionStatisticConfig.builder()
.percentilesHistogram(histogramsEnabled);
if (slos != null) {
builder.serviceLevelObjectives(slos);
}
return builder.build().merge(config);
return histogramConfig(httpServerHistograms, httpServerSLOs).merge(config);
}
return config;
}

private double[] slo(Optional<String> sloOptional) {
if (sloOptional.isPresent()) {
double[] sloArray = Arrays.stream(sloOptional.get().split(","))
.filter(s -> !s.trim().isEmpty())
.mapToDouble(s -> TimeUnit.MILLISECONDS.toNanos(Long.parseLong(s)))
.toArray();
if (sloArray.length == 0) {
return null;
}
return sloArray;
} else {
return null;
}
}

private DistributionStatisticConfig histogramConfig(boolean enabled, double... slos) {
DistributionStatisticConfig.Builder builder = DistributionStatisticConfig.builder()
.percentilesHistogram(enabled);
if (slos != null) {
builder.serviceLevelObjectives(slos);
}
return builder.build();
}

private boolean isHttpServerRequests(Meter.Id id) {
return "http.server.requests".equals(id.getName());
}

private boolean isHttpClientRequests(Meter.Id id) {
return "httpcomponents.httpclient.request".equals(id.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ quarkus.log.category."io.quarkus.arc.processor.BeanArchives".level=off
quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".level=off
quarkus.log.category."io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor".level=warn
quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level=error
quarkus.log.category."io.micrometer.core.instrument.binder.httpcomponents.MicrometerHttpRequestExecutor".level=error

# SqlExceptionHelper will log-and-throw error messages.
# As those messages might later be caught and handled, this is an antipattern so we prevent logging them
Expand Down
Loading
Loading