All Downloads are FREE. Search and download functionalities are using the official Maven repository.

reactor.netty.http.client.MicrometerHttpClientMetricsHandler Maven / Gradle / Ivy

There is a newer version: 1.2.1
Show newest version
/*
 * Copyright (c) 2022-2024 VMware, Inc. or its affiliates, All Rights Reserved.
 *
 * 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 reactor.netty.http.client;

import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.Timer;
import io.micrometer.observation.Observation;
import io.micrometer.observation.transport.RequestReplySenderContext;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import reactor.netty.observability.ReactorNettyHandlerContext;
import reactor.util.annotation.Nullable;
import reactor.util.context.ContextView;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Supplier;

import static reactor.netty.Metrics.OBSERVATION_REGISTRY;
import static reactor.netty.Metrics.RESPONSE_TIME;
import static reactor.netty.Metrics.UNKNOWN;
import static reactor.netty.Metrics.formatSocketAddress;
import static reactor.netty.Metrics.updateChannelContext;
import static reactor.netty.ReactorNetty.setChannelContext;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeHighCardinalityTags.HTTP_STATUS_CODE;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeHighCardinalityTags.HTTP_URL;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeHighCardinalityTags.NET_PEER_NAME;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeHighCardinalityTags.NET_PEER_PORT;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeHighCardinalityTags.REACTOR_NETTY_TYPE;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeLowCardinalityTags.METHOD;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeLowCardinalityTags.PROXY_ADDRESS;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeLowCardinalityTags.REMOTE_ADDRESS;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeLowCardinalityTags.STATUS;
import static reactor.netty.http.client.HttpClientObservations.ResponseTimeLowCardinalityTags.URI;

/**
 * {@link AbstractHttpClientMetricsHandler} for Reactor Netty built-in integration with Micrometer.
 *
 * @author Marcin Grzejszczak
 * @author Violeta Georgieva
 * @since 1.1.0
 */
final class MicrometerHttpClientMetricsHandler extends AbstractHttpClientMetricsHandler {
	final MicrometerHttpClientMetricsRecorder recorder;

	ResponseTimeHandlerContext responseTimeHandlerContext;
	Observation responseTimeObservation;
	ContextView parentContextView;

	MicrometerHttpClientMetricsHandler(MicrometerHttpClientMetricsRecorder recorder,
			SocketAddress remoteAddress,
			@Nullable SocketAddress proxyAddress,
			@Nullable Function uriTagValue) {
		super(remoteAddress, proxyAddress, uriTagValue);
		this.recorder = recorder;
	}

	MicrometerHttpClientMetricsHandler(MicrometerHttpClientMetricsHandler copy) {
		super(copy);
		this.recorder = copy.recorder;

		this.responseTimeHandlerContext = copy.responseTimeHandlerContext;
		this.responseTimeObservation = copy.responseTimeObservation;
		this.parentContextView = copy.parentContextView;
	}

	@Override
	protected HttpClientMetricsRecorder recorder() {
		return recorder;
	}

	@Override
	protected void recordRead(Channel channel, SocketAddress address) {
		if (proxyAddress == null) {
			recorder().recordDataReceivedTime(address, path, method, status,
					Duration.ofNanos(System.nanoTime() - dataReceivedTime));

			recorder().recordDataReceived(address, path, dataReceived);
		}
		else {
			recorder().recordDataReceivedTime(address, proxyAddress, path, method, status,
					Duration.ofNanos(System.nanoTime() - dataReceivedTime));

			recorder().recordDataReceived(address, proxyAddress, path, dataReceived);
		}

		// Cannot invoke the recorder anymore:
		// 1. The recorder is one instance only, it is invoked for all requests that can happen
		// 2. The recorder does not have knowledge about request lifecycle
		//
		// Move the implementation from the recorder here
		responseTimeObservation.stop();

		setChannelContext(channel, parentContextView);
	}

	@Override
	protected void reset() {
		super.reset();
		responseTimeHandlerContext = null;
		responseTimeObservation = null;
		parentContextView = null;
	}

	// reading the response
	@Override
	protected void startRead(HttpResponse msg) {
		super.startRead(msg);

		responseTimeHandlerContext.setResponse(msg);
		responseTimeHandlerContext.status = status;
	}

	// writing the request
	@Override
	protected void startWrite(HttpRequest msg, Channel channel, SocketAddress address) {
		super.startWrite(msg, channel, address);

		responseTimeHandlerContext = new ResponseTimeHandlerContext(recorder, msg, path, address, proxyAddress);
		responseTimeObservation = Observation.createNotStarted(recorder.name() + RESPONSE_TIME, responseTimeHandlerContext, OBSERVATION_REGISTRY);
		parentContextView = updateChannelContext(channel, responseTimeObservation);
		responseTimeObservation.start();
	}

	/*
	 * Requirements for HTTP clients
	 * 

https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#name * The contextual name must be in the format 'HTTP <method>' *

https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#common-attributes *

https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client */ static final class ResponseTimeHandlerContext extends RequestReplySenderContext implements ReactorNettyHandlerContext, Supplier { static final String HTTP_PREFIX = "http "; static final String TYPE = "client"; final String method; final String netPeerName; final String netPeerPort; final String path; final String proxyAddress; final MicrometerHttpClientMetricsRecorder recorder; // status might not be known beforehand String status = UNKNOWN; ResponseTimeHandlerContext(MicrometerHttpClientMetricsRecorder recorder, HttpRequest request, String path, SocketAddress remoteAddress, SocketAddress proxyAddress) { super((carrier, key, value) -> Objects.requireNonNull(carrier).headers().set(key, value)); this.recorder = recorder; this.method = request.method().name(); if (remoteAddress instanceof InetSocketAddress) { InetSocketAddress address = (InetSocketAddress) remoteAddress; this.netPeerName = address.getHostString(); this.netPeerPort = address.getPort() + ""; } else { this.netPeerName = remoteAddress.toString(); this.netPeerPort = ""; } this.path = path; this.proxyAddress = formatSocketAddress(proxyAddress); setCarrier(request); setContextualName(HTTP_PREFIX + this.method); } @Override public Observation.Context get() { return this; } @Override public Timer getTimer() { if (proxyAddress == null) { return recorder.getResponseTimeTimer(getName(), netPeerName + ":" + netPeerPort, path, method, status); } else { return recorder.getResponseTimeTimer(getName(), netPeerName + ":" + netPeerPort, proxyAddress, path, method, status); } } @Override public KeyValues getHighCardinalityKeyValues() { return KeyValues.of(REACTOR_NETTY_TYPE.asString(), TYPE, HTTP_URL.asString(), path, HTTP_STATUS_CODE.asString(), status, NET_PEER_NAME.asString(), netPeerName, NET_PEER_PORT.asString(), netPeerPort); } @Override public KeyValues getLowCardinalityKeyValues() { if (proxyAddress == null) { return KeyValues.of(METHOD.asString(), method, REMOTE_ADDRESS.asString(), netPeerName + ":" + netPeerPort, STATUS.asString(), status, URI.asString(), path); } else { return KeyValues.of(METHOD.asString(), method, REMOTE_ADDRESS.asString(), netPeerName + ":" + netPeerPort, PROXY_ADDRESS.asString(), proxyAddress, STATUS.asString(), status, URI.asString(), path); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy