Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jetty Client instrumentation with Observation API #3416

Merged
merged 5 commits into from
Feb 15, 2023
Merged
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 @@ -15,6 +15,7 @@
*/
package io.micrometer.core.instrument.binder.http;

import io.micrometer.common.KeyValue;
import io.micrometer.core.instrument.Tag;

/**
Expand Down Expand Up @@ -57,8 +58,11 @@ public enum Outcome {

private final Tag tag;

private final KeyValue keyValue;
shakuzen marked this conversation as resolved.
Show resolved Hide resolved

Outcome() {
this.tag = Tag.of("outcome", name());
this.keyValue = KeyValue.of("outcome", name());
}

/**
Expand All @@ -69,6 +73,15 @@ public Tag asTag() {
return this.tag;
}

/**
* Returns the {@code Outcome} as a {@link KeyValue} named {@code outcome}.
* @return the {@code outcome} {@code KeyValue}
* @since 1.11.0
*/
public KeyValue asKeyValue() {
return this.keyValue;
}

/**
* Return the {@code Outcome} for the given HTTP {@code status} code.
* @param status the HTTP status code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2022 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.core.instrument.binder.jetty;

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

/**
* Default implementation of {@link JettyClientObservationConvention}.
*
* @since 1.11.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,45 @@
/*
* Copyright 2022 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.core.instrument.binder.jetty;

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

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

/**
* Context to use when instrumenting Jetty client metrics with the Observation API.
*
* @since 1.11.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).header(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 2022 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.core.instrument.binder.jetty;

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.api.Request;
import org.eclipse.jetty.client.api.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.11.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");

public 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
* @since 1.7.0
*/
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 result the request result
* @param successfulUriPattern successful URI pattern
* @return the uri KeyValue derived from the request 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