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

Apache HTTP client instrumentation with Observation #3312

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.httpcomponents;

import io.micrometer.observation.transport.RequestReplySenderContext;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.protocol.HttpContext;

import java.util.function.Function;

/**
* {@link io.micrometer.observation.Observation.Context} for use with Apache HTTP client
* {@link io.micrometer.observation.Observation} instrumentation.
*
* @since 1.10.0
*/
public class ApacheHttpClientContext extends RequestReplySenderContext<HttpRequest, HttpResponse> {

private final HttpContext apacheHttpContext;

private final Function<HttpRequest, String> uriMapper;

private final boolean exportTagsForRoute;

public ApacheHttpClientContext(HttpRequest request, HttpContext apacheHttpContext,
Function<HttpRequest, String> uriMapper, boolean exportTagsForRoute) {
super((httpRequest, key, value) -> {
if (httpRequest != null) {
httpRequest.addHeader(key, value);
}
});
this.uriMapper = uriMapper;
this.exportTagsForRoute = exportTagsForRoute;
setCarrier(request);
this.apacheHttpContext = apacheHttpContext;
}

public HttpContext getApacheHttpContext() {
return apacheHttpContext;
}

public Function<HttpRequest, String> getUriMapper() {
return uriMapper;
}

public boolean shouldExportTagsForRoute() {
return exportTagsForRoute;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2017 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.httpcomponents;

import io.micrometer.common.docs.KeyName;
import io.micrometer.observation.Observation;
import io.micrometer.observation.docs.DocumentedObservation;

/**
* {@link DocumentedObservation} for Apache HTTP client instrumentation.
* @since 1.10.0
* @see MicrometerHttpRequestExecutor
*/
public enum ApacheHttpClientDocumentedObservation implements DocumentedObservation {

DEFAULT {
@Override
public Class<? extends Observation.ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultApacheHttpClientObservationConvention.class;
}

@Override
public KeyName[] getLowCardinalityKeyNames() {
return ApacheHttpClientTags.values();
}
};

enum ApacheHttpClientTags implements KeyName {

STATUS {
@Override
public String asString() {
return "status";
}
},
METHOD {
@Override
public String asString() {
return "method";
}
},
URI {
@Override
public String asString() {
return "uri";
}
},
TARGET_SCHEME {
@Override
public String asString() {
return "target.scheme";
}
},
TARGET_HOST {
@Override
public String asString() {
return "target.host";
}
},
TARGET_PORT {
@Override
public String asString() {
return "target.port";
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.httpcomponents;

import io.micrometer.observation.Observation;
import io.micrometer.observation.Observation.ObservationConvention;

/**
* {@link ObservationConvention} for Apache HTTP client instrumentation.
*
* @since 1.10.0
* @see DefaultApacheHttpClientObservationConvention
*/
public interface ApacheHttpClientObservationConvention extends ObservationConvention<ApacheHttpClientContext> {

@Override
default boolean supportsContext(Observation.Context context) {
return context instanceof ApacheHttpClientContext;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.httpcomponents;

import io.micrometer.common.KeyValues;
import io.micrometer.common.lang.Nullable;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;

/**
* Default implementation of {@link ApacheHttpClientObservationConvention}.
*
* @since 1.10.0
* @see ApacheHttpClientDocumentedObservation
*/
public class DefaultApacheHttpClientObservationConvention implements ApacheHttpClientObservationConvention {
shakuzen marked this conversation as resolved.
Show resolved Hide resolved

/**
* Singleton instance of this convention.
*/
public static final DefaultApacheHttpClientObservationConvention INSTANCE = new DefaultApacheHttpClientObservationConvention();

// There is no need to instantiate this class multiple times, but it may be extended,
// hence protected visibility.
protected DefaultApacheHttpClientObservationConvention() {
}

@Override
public String getName() {
return MicrometerHttpRequestExecutor.METER_NAME;
}

@Override
public String getContextualName(ApacheHttpClientContext context) {
return "HTTP " + getMethodString(context.getCarrier());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen this hard coded quite a few times - I wonder if we can extract this somehow, somewhere? 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since how to get the method will be different for each implementation I'm not sure we really can. This is exactly what we were trying to do with the HTTP abstractions for request/response, but we ended up going a different direction.

}

@Override
public KeyValues getLowCardinalityKeyValues(ApacheHttpClientContext context) {
KeyValues keyValues = KeyValues.of(
ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.METHOD
.withValue(getMethodString(context.getCarrier())),
ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.URI
.withValue(context.getUriMapper().apply(context.getCarrier())),
ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.STATUS
.withValue(getStatusValue(context.getResponse())));
if (context.shouldExportTagsForRoute()) {
keyValues = keyValues.and(HttpContextUtils.generateTagStringsForRoute(context.getApacheHttpContext()));
}
return keyValues;
}

String getStatusValue(@Nullable HttpResponse response) {
return response != null ? Integer.toString(response.getStatusLine().getStatusCode()) : "CLIENT_ERROR";
}

String getMethodString(@Nullable HttpRequest request) {
return request != null && request.getRequestLine().getMethod() != null ? request.getRequestLine().getMethod()
: "UNKNOWN";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
class HttpContextUtils {

static Tags generateTagsForRoute(HttpContext context) {
return Tags.of(generateTagStringsForRoute(context));
}

static String[] generateTagStringsForRoute(HttpContext context) {
String targetScheme = "UNKNOWN";
String targetHost = "UNKNOWN";
String targetPort = "UNKNOWN";
Expand All @@ -33,7 +37,10 @@ static Tags generateTagsForRoute(HttpContext context) {
targetHost = host.getHostName();
targetPort = String.valueOf(host.getPort());
}
return Tags.of("target.scheme", targetScheme, "target.host", targetHost, "target.port", targetPort);
return new String[] { ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.TARGET_SCHEME.asString(),
targetScheme, ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.TARGET_HOST.asString(),
targetHost, ApacheHttpClientDocumentedObservation.ApacheHttpClientTags.TARGET_PORT.asString(),
targetPort };
}

}
Loading