/*
* Copyright 2014 NAVER Corp.
*
* 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
*
* 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.
*/
package com.navercorp.pinpoint.plugin.httpclient4.interceptor;
import java.io.IOException;
import com.navercorp.pinpoint.bootstrap.interceptor.scope.InterceptorScope;
import com.navercorp.pinpoint.bootstrap.interceptor.scope.InterceptorScopeInvocation;
import com.navercorp.pinpoint.common.Charsets;
import com.navercorp.pinpoint.common.util.StringUtils;
import com.navercorp.pinpoint.plugin.httpclient4.HttpCallContext;
import com.navercorp.pinpoint.plugin.httpclient4.HttpCallContextFactory;
import com.navercorp.pinpoint.plugin.httpclient4.HttpClient4PluginConfig;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.StatusLine;
import org.apache.http.protocol.HTTP;
import com.navercorp.pinpoint.bootstrap.config.DumpType;
import com.navercorp.pinpoint.bootstrap.context.Header;
import com.navercorp.pinpoint.bootstrap.context.MethodDescriptor;
import com.navercorp.pinpoint.bootstrap.context.SpanEventRecorder;
import com.navercorp.pinpoint.bootstrap.context.Trace;
import com.navercorp.pinpoint.bootstrap.context.TraceContext;
import com.navercorp.pinpoint.bootstrap.context.TraceId;
import com.navercorp.pinpoint.bootstrap.interceptor.AroundInterceptor;
import com.navercorp.pinpoint.bootstrap.interceptor.annotation.Scope;
import com.navercorp.pinpoint.bootstrap.interceptor.scope.ExecutionPolicy;
import com.navercorp.pinpoint.bootstrap.logging.PLogger;
import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
import com.navercorp.pinpoint.bootstrap.pair.NameIntValuePair;
import com.navercorp.pinpoint.bootstrap.sampler.SamplingFlagUtils;
import com.navercorp.pinpoint.bootstrap.util.FixedByteArrayOutputStream;
import com.navercorp.pinpoint.bootstrap.util.InterceptorUtils;
import com.navercorp.pinpoint.bootstrap.util.SimpleSampler;
import com.navercorp.pinpoint.bootstrap.util.SimpleSamplerFactory;
import com.navercorp.pinpoint.common.trace.AnnotationKey;
import com.navercorp.pinpoint.plugin.httpclient4.HttpClient4Constants;
/**
* @author minwoo.jung
* @author jaehong.kim
*/
@Scope(value = HttpClient4Constants.HTTP_CLIENT4_SCOPE, executionPolicy = ExecutionPolicy.ALWAYS)
public class HttpRequestExecutorExecuteMethodInterceptor implements AroundInterceptor {
private static final int HTTP_REQUEST_INDEX = 1;
private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
private final boolean isDebug = logger.isDebugEnabled();
private final TraceContext traceContext;
private final MethodDescriptor methodDescriptor;
private final boolean param;
private final boolean cookie;
private final DumpType cookieDumpType;
private final SimpleSampler cookieSampler;
private final boolean entity;
private final DumpType entityDumpType;
private final SimpleSampler entitySampler;
private final boolean statusCode;
private final InterceptorScope interceptorScope;
private final boolean io;
public HttpRequestExecutorExecuteMethodInterceptor(TraceContext traceContext, MethodDescriptor methodDescriptor, InterceptorScope interceptorScope) {
this.traceContext = traceContext;
this.methodDescriptor = methodDescriptor;
this.interceptorScope = interceptorScope;
final HttpClient4PluginConfig profilerConfig = new HttpClient4PluginConfig(traceContext.getProfilerConfig());
this.param = profilerConfig.isParam();
this.cookie = profilerConfig.isCookie();
this.cookieDumpType = profilerConfig.getCookieDumpType();
if (cookie) {
this.cookieSampler = SimpleSamplerFactory.createSampler(cookie, profilerConfig.getCookieSamplingRate());
} else {
this.cookieSampler = null;
}
this.entity = profilerConfig.isEntity();
this.entityDumpType = profilerConfig.getEntityDumpType();
if (entity) {
this.entitySampler = SimpleSamplerFactory.createSampler(entity, profilerConfig.getEntitySamplingRate());
} else {
this.entitySampler = null;
}
this.statusCode = profilerConfig.isStatusCode();
this.io = profilerConfig.isIo();
}
@Override
public void before(Object target, Object[] args) {
if (isDebug) {
logger.beforeInterceptor(target, args);
}
final Trace trace = traceContext.currentRawTraceObject();
if (trace == null) {
return;
}
final HttpRequest httpRequest = getHttpRequest(args);
final boolean sampling = trace.canSampled();
if (!sampling) {
if (isDebug) {
logger.debug("set Sampling flag=false");
}
if (httpRequest != null) {
httpRequest.setHeader(Header.HTTP_SAMPLED.toString(), SamplingFlagUtils.SAMPLING_RATE_FALSE);
}
return;
}
final SpanEventRecorder recorder = trace.traceBlockBegin();
TraceId nextId = trace.getTraceId().getNextTraceId();
recorder.recordNextSpanId(nextId.getSpanId());
recorder.recordServiceType(HttpClient4Constants.HTTP_CLIENT_4);
if (httpRequest != null) {
httpRequest.setHeader(Header.HTTP_TRACE_ID.toString(), nextId.getTransactionId());
httpRequest.setHeader(Header.HTTP_SPAN_ID.toString(), String.valueOf(nextId.getSpanId()));
httpRequest.setHeader(Header.HTTP_PARENT_SPAN_ID.toString(), String.valueOf(nextId.getParentSpanId()));
httpRequest.setHeader(Header.HTTP_FLAGS.toString(), String.valueOf(nextId.getFlags()));
httpRequest.setHeader(Header.HTTP_PARENT_APPLICATION_NAME.toString(), traceContext.getApplicationName());
httpRequest.setHeader(Header.HTTP_PARENT_APPLICATION_TYPE.toString(), Short.toString(traceContext.getServerTypeCode()));
final NameIntValuePair<String> host = getHost();
if (host != null) {
final String endpoint = getEndpoint(host.getName(), host.getValue());
logger.debug("Get host {}", endpoint);
httpRequest.setHeader(Header.HTTP_HOST.toString(), endpoint);
}
}
InterceptorScopeInvocation invocation = interceptorScope.getCurrentInvocation();
if (invocation != null) {
invocation.getOrCreateAttachment(HttpCallContextFactory.HTTPCALL_CONTEXT_FACTORY);
}
}
private HttpRequest getHttpRequest(Object[] args) {
if (args != null && args.length >= 1 && args[0] != null && args[0] instanceof HttpRequest) {
return (HttpRequest) args[0];
}
return null;
}
private NameIntValuePair<String> getHost() {
final InterceptorScopeInvocation transaction = interceptorScope.getCurrentInvocation();
if (transaction != null && transaction.getAttachment() != null && transaction.getAttachment() instanceof HttpCallContext) {
HttpCallContext callContext = (HttpCallContext) transaction.getAttachment();
return new NameIntValuePair<String>(callContext.getHost(), callContext.getPort());
}
return null;
}
@Override
public void after(Object target, Object[] args, Object result, Throwable throwable) {
if (isDebug) {
logger.afterInterceptor(target, args);
}
final Trace trace = traceContext.currentTraceObject();
if (trace == null) {
return;
}
try {
final SpanEventRecorder recorder = trace.currentSpanEventRecorder();
final HttpRequest httpRequest = getHttpRequest(args);
if (httpRequest != null) {
// Accessing httpRequest here not BEFORE() because it can cause side effect.
if(httpRequest.getRequestLine() != null) {
final String httpUrl = InterceptorUtils.getHttpUrl(httpRequest.getRequestLine().getUri(), param);
recorder.recordAttribute(AnnotationKey.HTTP_URL, httpUrl);
}
final NameIntValuePair<String> host = getHost();
if (host != null) {
final String endpoint = getEndpoint(host.getName(), host.getValue());
recorder.recordDestinationId(endpoint);
}
recordHttpRequest(trace, httpRequest, throwable);
}
if (statusCode) {
final Integer statusCodeValue = getStatusCode(result);
if (statusCodeValue != null) {
recorder.recordAttribute(AnnotationKey.HTTP_STATUS_CODE, statusCodeValue);
}
}
recorder.recordApi(methodDescriptor);
recorder.recordException(throwable);
final InterceptorScopeInvocation invocation = interceptorScope.getCurrentInvocation();
if (invocation != null && invocation.getAttachment() != null && invocation.getAttachment() instanceof HttpCallContext) {
final HttpCallContext callContext = (HttpCallContext) invocation.getAttachment();
logger.debug("Check call context {}", callContext);
if (io) {
final StringBuilder sb = new StringBuilder();
sb.append("write=").append(callContext.getWriteElapsedTime());
if (callContext.isWriteFail()) {
sb.append("(fail)");
}
sb.append(", read=").append(callContext.getReadElapsedTime());
if (callContext.isReadFail()) {
sb.append("(fail)");
}
recorder.recordAttribute(AnnotationKey.HTTP_IO, sb.toString());
}
// clear
invocation.removeAttachment();
}
} finally {
trace.traceBlockEnd();
}
}
private Integer getStatusCode(Object result) {
return getStatusCodeFromResponse(result);
}
Integer getStatusCodeFromResponse(Object result) {
if (result != null && result instanceof HttpResponse) {
HttpResponse response = (HttpResponse) result;
final StatusLine statusLine = response.getStatusLine();
if (statusLine != null) {
return statusLine.getStatusCode();
} else {
return null;
}
}
return null;
}
private String getEndpoint(String host, int port) {
if (host == null) {
return "UnknownHttpClient";
}
if (port < 0) {
return host;
}
StringBuilder sb = new StringBuilder(host.length() + 8);
sb.append(host);
sb.append(':');
sb.append(port);
return sb.toString();
}
private void recordHttpRequest(Trace trace, HttpRequest httpRequest, Throwable throwable) {
final boolean isException = InterceptorUtils.isThrowable(throwable);
if (cookie) {
if (DumpType.ALWAYS == cookieDumpType) {
recordCookie(httpRequest, trace);
} else if (DumpType.EXCEPTION == cookieDumpType && isException) {
recordCookie(httpRequest, trace);
}
}
if (entity) {
if (DumpType.ALWAYS == entityDumpType) {
recordEntity(httpRequest, trace);
} else if (DumpType.EXCEPTION == entityDumpType && isException) {
recordEntity(httpRequest, trace);
}
}
}
protected void recordCookie(HttpMessage httpMessage, Trace trace) {
org.apache.http.Header[] cookies = httpMessage.getHeaders("Cookie");
for (org.apache.http.Header header : cookies) {
final String value = header.getValue();
if (StringUtils.isNotEmpty(value)) {
if (cookieSampler.isSampling()) {
final SpanEventRecorder recorder = trace.currentSpanEventRecorder();
recorder.recordAttribute(AnnotationKey.HTTP_COOKIE, StringUtils.abbreviate(value, 1024));
}
// Can a cookie have 2 or more values?
// PMD complains if we use break here
return;
}
}
}
protected void recordEntity(HttpMessage httpMessage, Trace trace) {
if (httpMessage instanceof HttpEntityEnclosingRequest) {
final HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) httpMessage;
try {
final HttpEntity entity = entityRequest.getEntity();
if (entity != null && entity.isRepeatable() && entity.getContentLength() > 0) {
if (entitySampler.isSampling()) {
final String entityString = entityUtilsToString(entity, Charsets.UTF_8_NAME, 1024);
final SpanEventRecorder recorder = trace.currentSpanEventRecorder();
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, entityString);
}
}
} catch (Exception e) {
logger.debug("HttpEntityEnclosingRequest entity record fail. Caused:{}", e.getMessage(), e);
}
}
}
/**
* copy: EntityUtils Get the entity content as a String, using the provided default character set if none is found in the entity. If defaultCharset is null, the default "ISO-8859-1" is used.
*
* @param entity
* must not be null
* @param defaultCharset
* character set to be applied if none found in the entity
* @return the entity content as a String. May be null if {@link HttpEntity#getContent()} is null.
* @throws ParseException
* if header elements cannot be parsed
* @throws IllegalArgumentException
* if entity is null or if content length > Integer.MAX_VALUE
* @throws IOException
* if an error occurs reading the input stream
*/
@SuppressWarnings("deprecation")
public static String entityUtilsToString(final HttpEntity entity, final String defaultCharset, int maxLength) throws Exception {
if (entity == null) {
throw new IllegalArgumentException("HTTP entity may not be null");
}
if (entity.getContentLength() > Integer.MAX_VALUE) {
return "HTTP entity is too large to be buffered in memory length:" + entity.getContentLength();
}
if (entity.getContentType().getValue().startsWith("multipart/form-data")) {
return "content type is multipart/form-data. content length:" + entity.getContentLength();
}
String charset = getContentCharSet(entity);
if (charset == null) {
charset = defaultCharset;
}
if (charset == null) {
charset = HTTP.DEFAULT_CONTENT_CHARSET;
}
FixedByteArrayOutputStream outStream = new FixedByteArrayOutputStream(maxLength);
entity.writeTo(outStream);
String entityValue = outStream.toString(charset);
if (entity.getContentLength() > maxLength) {
StringBuilder sb = new StringBuilder();
sb.append(entityValue);
sb.append(" (HTTP entity is large. length: ");
sb.append(entity.getContentLength());
sb.append(" )");
return sb.toString();
}
return entityValue;
}
/**
* copy: EntityUtils Obtains character set of the entity, if known.
*
* @param entity
* must not be null
* @return the character set, or null if not found
* @throws ParseException
* if header elements cannot be parsed
* @throws IllegalArgumentException
* if entity is null
*/
public static String getContentCharSet(final HttpEntity entity) throws ParseException {
if (entity == null) {
throw new IllegalArgumentException("HTTP entity may not be null");
}
String charset = null;
if (entity.getContentType() != null) {
HeaderElement values[] = entity.getContentType().getElements();
if (values.length > 0) {
NameValuePair param = values[0].getParameterByName("charset");
if (param != null) {
charset = param.getValue();
}
}
}
return charset;
}
}