Skip to content

Commit

Permalink
JettyClientMetrics compatibility with Jetty 12
Browse files Browse the repository at this point in the history
Add JettyClientMetrics and related classes to a micrometer-jetty12 module and migrate Jetty API accordingly.
  • Loading branch information
shakuzen committed Feb 19, 2024
1 parent c8f6f4c commit ff96e60
Show file tree
Hide file tree
Showing 16 changed files with 1,178 additions and 2 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ subprojects {

check.dependsOn("testModules")

if (!(project.name in ['micrometer-jakarta9', 'micrometer-java11'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp
if (!(project.name in ['micrometer-jakarta9', 'micrometer-java11', 'micrometer-jetty12'])) { // add projects here that do not exist in the previous minor so should be excluded from japicmp
apply plugin: 'me.champeau.gradle.japicmp'
apply plugin: 'de.undercouch.download'

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ javax-inject = "1"
jaxb = "2.3.1"
jetty9 = "9.4.53.v20231009"
jetty11 = "11.0.16"
jetty12 = "12.0.3"
jetty12 = "12.0.6"
jersey2 = "2.41"
jersey3 = "3.0.11"
jmh = "1.37"
Expand Down
28 changes: 28 additions & 0 deletions micrometer-jetty12/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
description 'Micrometer instrumentation for Jetty 12'

// skip this module when building with jdk <17
if (!javaLanguageVersion.canCompileOrRun(17)) {
project.tasks.configureEach { task -> task.enabled = false }
}

dependencies {
api project(":micrometer-core")

optionalApi libs.jetty12Client

testImplementation project(":micrometer-observation-test")
testImplementation project(":micrometer-test")
testImplementation libs.junitJupiter
testImplementation libs.assertj
testImplementation libs.wiremock
}

java {
targetCompatibility = 17
}

compileJava {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
options.release = 17
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024 VMware, Inc.
*
* 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.micrometer.jetty12.client;

import io.micrometer.common.KeyValues;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Result;

/**
* Default implementation of {@link JettyClientObservationConvention}.
*
* @since 1.13.0
*/
public class DefaultJettyClientObservationConvention implements JettyClientObservationConvention {

public static DefaultJettyClientObservationConvention INSTANCE = new DefaultJettyClientObservationConvention();

@Override
public KeyValues getLowCardinalityKeyValues(JettyClientContext context) {
Request request = context.getCarrier();
Result result = context.getResponse();
return KeyValues.of(JettyClientKeyValues.method(request), JettyClientKeyValues.host(request),
JettyClientKeyValues.uri(request, result, context.getUriPatternFunction()),
JettyClientKeyValues.exception(result), JettyClientKeyValues.status(result),
JettyClientKeyValues.outcome(result));
}

@Override
public String getName() {
return JettyClientMetrics.DEFAULT_JETTY_CLIENT_REQUESTS_TIMER_NAME;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024 VMware, Inc.
*
* 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.micrometer.jetty12.client;

import io.micrometer.observation.transport.RequestReplySenderContext;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Result;

import java.util.Objects;
import java.util.function.BiFunction;

/**
* Context to use when instrumenting Jetty client metrics with the Observation API.
*
* @since 1.13.0
* @see JettyClientMetrics
*/
public class JettyClientContext extends RequestReplySenderContext<Request, Result> {

private final BiFunction<Request, Result, String> uriPatternFunction;

public JettyClientContext(Request request, BiFunction<Request, Result, String> uriPatternFunction) {
super((carrier, key, value) -> Objects.requireNonNull(carrier)
.headers(httpFields -> httpFields.add(key, value)));
this.uriPatternFunction = uriPatternFunction;
setCarrier(request);
}

public BiFunction<Request, Result, String> getUriPatternFunction() {
return uriPatternFunction;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Copyright 2024 VMware, Inc.
*
* 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.micrometer.jetty12.client;

import io.micrometer.common.KeyValue;
import io.micrometer.common.lang.Nullable;
import io.micrometer.common.util.StringUtils;
import io.micrometer.core.instrument.binder.http.Outcome;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.http.HttpStatus;

import java.util.function.BiFunction;
import java.util.regex.Pattern;

/**
* Factory methods for {@link KeyValue} associated with a request-response exchange that
* is handled by Jetty {@link org.eclipse.jetty.client.HttpClient}.
*
* @author Jon Schneider
* @since 1.13.0
*/
public final class JettyClientKeyValues {

private static final KeyValue URI_NOT_FOUND = KeyValue.of("uri", "NOT_FOUND");

private static final KeyValue URI_REDIRECTION = KeyValue.of("uri", "REDIRECTION");

private static final KeyValue URI_ROOT = KeyValue.of("uri", "root");

private static final KeyValue EXCEPTION_NONE = KeyValue.of("exception", "None");

private static final KeyValue EXCEPTION_UNKNOWN = KeyValue.of("exception", "UNKNOWN");

private static final KeyValue METHOD_UNKNOWN = KeyValue.of("method", "UNKNOWN");

private static final KeyValue HOST_UNKNOWN = KeyValue.of("host", "UNKNOWN");

private static final KeyValue STATUS_UNKNOWN = KeyValue.of("status", "UNKNOWN");

private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");

private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");

private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of("outcome", "UNKNOWN");

private JettyClientKeyValues() {
}

/**
* Creates a {@code method} KeyValue based on the {@link Request#getMethod() method}
* of the given {@code request}.
* @param request the request
* @return the method KeyValue whose value is a capitalized method (e.g. GET).
*/
public static KeyValue method(Request request) {
return (request != null) ? KeyValue.of("method", request.getMethod()) : METHOD_UNKNOWN;
}

/**
* Creates a {@code host} KeyValue based on the {@link Request#getHost()} of the given
* {@code request}.
* @param request the request
* @return the host KeyValue derived from request
*/
public static KeyValue host(Request request) {
return (request != null) ? KeyValue.of("host", request.getHost()) : HOST_UNKNOWN;
}

/**
* Creates a {@code status} KeyValue based on the status of the given {@code result}.
* @param result the request result
* @return the status KeyValue derived from the status of the response
*/
public static KeyValue status(@Nullable Result result) {
return result != null ? KeyValue.of("status", Integer.toString(result.getResponse().getStatus()))
: STATUS_UNKNOWN;
}

/**
* Creates a {@code uri} KeyValue based on the URI of the given {@code result}.
* {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 responses.
* @param request the request
* @param result the request result
* @param successfulUriPattern successful URI pattern
* @return the uri KeyValue derived from the request and its result
*/
public static KeyValue uri(Request request, @Nullable Result result,
BiFunction<Request, Result, String> successfulUriPattern) {
if (result != null && result.getResponse() != null) {
int status = result.getResponse().getStatus();
if (HttpStatus.isRedirection(status)) {
return URI_REDIRECTION;
}
if (status == 404) {
return URI_NOT_FOUND;
}
}

String matchingPattern = successfulUriPattern.apply(request, result);
matchingPattern = MULTIPLE_SLASH_PATTERN.matcher(matchingPattern).replaceAll("/");
if (matchingPattern.equals("/")) {
return URI_ROOT;
}
matchingPattern = TRAILING_SLASH_PATTERN.matcher(matchingPattern).replaceAll("");
return KeyValue.of("uri", matchingPattern);
}

/**
* Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName()
* simple name} of the class of the given {@code exception}.
* @param result the request result
* @return the exception KeyValue derived from the exception
*/
public static KeyValue exception(@Nullable Result result) {
if (result == null) {
return EXCEPTION_UNKNOWN;
}
Throwable exception = result.getFailure();
if (exception == null) {
return EXCEPTION_NONE;
}
if (result.getResponse() != null) {
int status = result.getResponse().getStatus();
if (status == 404 || HttpStatus.isRedirection(status)) {
return EXCEPTION_NONE;
}
}
if (exception.getCause() != null) {
exception = exception.getCause();
}
String simpleName = exception.getClass().getSimpleName();
return KeyValue.of("exception",
StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName());
}

/**
* Creates an {@code outcome} KeyValue based on the status of the given
* {@code result}.
* @param result the request result
* @return the outcome KeyValue derived from the status of the response
*/
public static KeyValue outcome(@Nullable Result result) {
if (result == null) {
return OUTCOME_UNKNOWN;
}
return Outcome.forStatus(result.getResponse().getStatus()).asKeyValue();
}

}
Loading

0 comments on commit ff96e60

Please sign in to comment.