HttpClientObservationSupport.java
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.observation;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.observation.ObservationRegistry;
import org.apache.hc.client5.http.impl.ChainElement;
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.cache.CachingHttpAsyncClientBuilder;
import org.apache.hc.client5.http.impl.cache.CachingHttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.observation.binder.ConnPoolMeters;
import org.apache.hc.client5.http.observation.binder.ConnPoolMetersAsync;
import org.apache.hc.client5.http.observation.impl.ObservationAsyncExecInterceptor;
import org.apache.hc.client5.http.observation.impl.ObservationClassicExecInterceptor;
import org.apache.hc.client5.http.observation.interceptors.AsyncIoByteCounterExec;
import org.apache.hc.client5.http.observation.interceptors.AsyncTimerExec;
import org.apache.hc.client5.http.observation.interceptors.IoByteCounterExec;
import org.apache.hc.client5.http.observation.interceptors.TimerExec;
import org.apache.hc.core5.util.Args;
/**
* Utility class that wires Micrometer / OpenTelemetry instrumentation into
* the HttpClient execution pipeline(s).
*
* <p>This helper can install:</p>
* <ul>
* <li><b>Observations</b> (via Micrometer {@link ObservationRegistry})
* which can be bridged to OpenTelemetry tracing, and</li>
* <li><b>Metrics</b> (via Micrometer {@link MeterRegistry}) such as
* per-request latency timers, response counters, I/O counters, and
* connection pool gauges.</li>
* </ul>
*
* <h3>Optional dependencies</h3>
* <p>
* Micrometer and OpenTelemetry are <em>optional</em> dependencies. Use the
* overloads that accept explicit registries (recommended) or the convenience
* overloads that use {@link Metrics#globalRegistry}. When Micrometer is not
* on the classpath, only the observation / metric features you actually call
* will be required.
* </p>
*
* <h3>Typical usage (classic client)</h3>
* <pre>{@code
* ObservationRegistry obs = ObservationRegistry.create();
* MeterRegistry meters = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
* HttpClientBuilder b = HttpClients.custom();
*
* HttpClientObservationSupport.enable(
* b, obs, meters,
* ObservingOptions.DEFAULT,
* MetricConfig.DEFAULT);
*
* CloseableHttpClient client = b.build();
* }</pre>
*
* <h3>What gets installed</h3>
* <ul>
* <li>An <em>observation</em> interceptor (if {@code obsReg} is non-null)
* that surrounds each execution with a start/stop span.</li>
* <li>Metric interceptors according to {@link ObservingOptions.MetricSet}:
* BASIC (latency timer + response counter), IO (bytes in/out counters),
* and CONN_POOL (pool gauges; classic and async variants).</li>
* </ul>
*
* <p><strong>Thread safety:</strong> This class is stateless. Methods may be
* called from any thread before the client is built.</p>
*
* @since 5.6
*/
public final class HttpClientObservationSupport {
/**
* Internal ID for the observation interceptor.
*/
private static final String OBS_ID = "observation";
/**
* Internal ID for latency/counter metrics interceptor.
*/
private static final String TIMER_ID = "metric-timer";
/**
* Internal ID for I/O byte counters interceptor.
*/
private static final String IO_ID = "metric-io";
/* ====================== Classic ====================== */
/**
* Enables observations and default metrics on a classic client builder using
* {@link Metrics#globalRegistry} and {@link MetricConfig#DEFAULT}.
*
* @param builder client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @since 5.6
*/
public static void enable(final HttpClientBuilder builder,
final ObservationRegistry obsReg) {
enable(builder, obsReg, Metrics.globalRegistry, ObservingOptions.DEFAULT, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a classic client builder using
* {@link Metrics#globalRegistry} and a custom {@link ObservingOptions}.
*
* @param builder client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpClientBuilder builder,
final ObservationRegistry obsReg,
final ObservingOptions opts) {
enable(builder, obsReg, Metrics.globalRegistry, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a classic client builder with an explicit
* meter registry and default {@link MetricConfig}.
*
* @param builder client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts) {
enable(builder, obsReg, meterReg, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a classic client builder using explicit
* registries and {@link MetricConfig}.
*
* <p>Installs interceptors at the <em>beginning</em> of the execution chain.</p>
*
* @param builder client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with (must not be {@code null})
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(builder, "builder");
Args.notNull(meterReg, "meterRegistry");
final ObservingOptions o = (opts != null) ? opts : ObservingOptions.DEFAULT;
final MetricConfig config = (mc != null) ? mc : MetricConfig.DEFAULT;
// Observations (spans) ��� only if registry provided
if (obsReg != null) {
builder.addExecInterceptorFirst(OBS_ID, new ObservationClassicExecInterceptor(obsReg, opts));
}
// Metrics
if (o.metricSets.contains(ObservingOptions.MetricSet.BASIC)) {
builder.addExecInterceptorFirst(TIMER_ID, new TimerExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorFirst(IO_ID, new IoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMeters.bindTo(builder, meterReg, config);
}
}
/* ============== Classic (with caching) =============== */
/**
* Enables observations and default metrics on a caching classic client builder using
* {@link Metrics#globalRegistry} and {@link MetricConfig#DEFAULT}.
*
* @param builder caching client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @since 5.6
*/
public static void enable(final CachingHttpClientBuilder builder,
final ObservationRegistry obsReg) {
enable(builder, obsReg, Metrics.globalRegistry, ObservingOptions.DEFAULT, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching classic client builder using
* {@link Metrics#globalRegistry} and a custom {@link ObservingOptions}.
*
* @param builder caching client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpClientBuilder builder,
final ObservationRegistry obsReg,
final ObservingOptions opts) {
enable(builder, obsReg, Metrics.globalRegistry, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching classic client builder with an explicit
* meter registry and default {@link MetricConfig}.
*
* @param builder caching client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts) {
enable(builder, obsReg, meterReg, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching classic client builder using explicit
* registries and {@link MetricConfig}.
*
* <p>Interceptors are installed <em>after</em> the caching element so that
* metrics/observations reflect the actual exchange.</p>
*
* @param builder caching client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with (must not be {@code null})
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(builder, "builder");
Args.notNull(meterReg, "meterRegistry");
final ObservingOptions o = (opts != null) ? opts : ObservingOptions.DEFAULT;
final MetricConfig config = (mc != null) ? mc : MetricConfig.DEFAULT;
// Observations (after caching stage so they see the real exchange)
if (obsReg != null) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), OBS_ID,
new ObservationClassicExecInterceptor(obsReg, opts));
}
// Metrics
if (o.metricSets.contains(ObservingOptions.MetricSet.BASIC)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), TIMER_ID, new TimerExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new IoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMeters.bindTo(builder, meterReg, config);
}
}
/* ======================== Async ====================== */
/**
* Enables observations and default metrics on an async client builder using
* {@link Metrics#globalRegistry} and {@link MetricConfig#DEFAULT}.
*
* @param builder async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @since 5.6
*/
public static void enable(final HttpAsyncClientBuilder builder,
final ObservationRegistry obsReg) {
enable(builder, obsReg, Metrics.globalRegistry, ObservingOptions.DEFAULT, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on an async client builder using
* {@link Metrics#globalRegistry} and a custom {@link ObservingOptions}.
*
* @param builder async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final ObservingOptions opts) {
enable(builder, obsReg, Metrics.globalRegistry, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on an async client builder with an explicit
* meter registry and default {@link MetricConfig}.
*
* @param builder async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts) {
enable(builder, obsReg, meterReg, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on an async client builder using explicit
* registries and {@link MetricConfig}.
*
* @param builder async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with (must not be {@code null})
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
* @since 5.6
*/
public static void enable(final HttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(builder, "builder");
Args.notNull(meterReg, "meterRegistry");
final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
// Observations
if (obsReg != null) {
builder.addExecInterceptorFirst(OBS_ID, new ObservationAsyncExecInterceptor(obsReg, o));
}
// Metrics
if (o.metricSets.contains(ObservingOptions.MetricSet.BASIC)) {
builder.addExecInterceptorFirst(TIMER_ID, new AsyncTimerExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorFirst(IO_ID, new AsyncIoByteCounterExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.CONN_POOL)) {
ConnPoolMetersAsync.bindTo(builder, meterReg, config);
}
}
/* ============== Async (with caching) ================= */
/**
* Enables observations and default metrics on a caching async client builder using
* {@link Metrics#globalRegistry} and {@link MetricConfig#DEFAULT}.
*
* @param builder caching async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @since 5.6
*/
public static void enable(final CachingHttpAsyncClientBuilder builder,
final ObservationRegistry obsReg) {
enable(builder, obsReg, Metrics.globalRegistry, ObservingOptions.DEFAULT, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching async client builder using
* {@link Metrics#globalRegistry} and a custom {@link ObservingOptions}.
*
* @param builder caching async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final ObservingOptions opts) {
enable(builder, obsReg, Metrics.globalRegistry, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching async client builder with an explicit
* meter registry and default {@link MetricConfig}.
*
* @param builder caching async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts) {
enable(builder, obsReg, meterReg, opts, MetricConfig.DEFAULT);
}
/**
* Enables observations and metrics on a caching async client builder using explicit
* registries and {@link MetricConfig}.
*
* <p>Interceptors are installed <em>after</em> the caching element.</p>
*
* @param builder caching async client builder to instrument
* @param obsReg observation registry; if {@code null} no observations are installed
* @param meterReg meter registry to register meters with (must not be {@code null})
* @param opts observation/metric options; when {@code null} {@link ObservingOptions#DEFAULT} is used
* @param mc metric configuration; when {@code null} {@link MetricConfig#DEFAULT} is used
* @since 5.6
*/
public static void enable(final CachingHttpAsyncClientBuilder builder,
final ObservationRegistry obsReg,
final MeterRegistry meterReg,
final ObservingOptions opts,
final MetricConfig mc) {
Args.notNull(builder, "builder");
Args.notNull(meterReg, "meterRegistry");
final ObservingOptions o = opts != null ? opts : ObservingOptions.DEFAULT;
final MetricConfig config = mc != null ? mc : MetricConfig.DEFAULT;
// Observations (after caching)
if (obsReg != null) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), OBS_ID,
new ObservationAsyncExecInterceptor(obsReg, o));
}
// Metrics
if (o.metricSets.contains(ObservingOptions.MetricSet.BASIC)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), TIMER_ID, new AsyncTimerExec(meterReg, o, config));
}
if (o.metricSets.contains(ObservingOptions.MetricSet.IO)) {
builder.addExecInterceptorAfter(ChainElement.CACHING.name(), IO_ID, new AsyncIoByteCounterExec(meterReg, o, config));
}
}
/**
* No instantiation.
*
* @since 5.6
*/
private HttpClientObservationSupport() {
}
}