/*
* 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.ning.asynchttpclient.interceptor;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.TargetMethod;
import com.navercorp.pinpoint.bootstrap.logging.PLogger;
import com.navercorp.pinpoint.bootstrap.logging.PLoggerFactory;
import com.navercorp.pinpoint.bootstrap.sampler.SamplingFlagUtils;
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.common.util.CollectionUtils;
import com.navercorp.pinpoint.common.util.StringUtils;
import com.navercorp.pinpoint.plugin.ning.asynchttpclient.NingAsyncHttpClientPlugin;
import com.navercorp.pinpoint.plugin.ning.asynchttpclient.NingAsyncHttpClientPluginConfig;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.FluentStringsMap;
import com.ning.http.client.Part;
import com.ning.http.client.Request.EntityWriter;
import com.ning.http.client.cookie.Cookie;
/**
* intercept com.ning.http.client.AsyncHttpClient.executeRequest(Request,
* AsyncHandler<T>)
*
* @author netspider
*
*/
@TargetMethod(name="executeRequest", paramTypes= { "com.ning.http.client.Request", "com.ning.http.client.AsyncHandler" })
public class ExecuteRequestInterceptor implements AroundInterceptor {
private final PLogger logger = PLoggerFactory.getLogger(ExecuteRequestInterceptor.class);
private final boolean isDebug = logger.isDebugEnabled();
private final TraceContext traceContext;
private final MethodDescriptor descriptor;
private final NingAsyncHttpClientPluginConfig config;
private final SimpleSampler cookieSampler;
private final SimpleSampler entitySampler;
private final SimpleSampler paramSampler;
public ExecuteRequestInterceptor(TraceContext traceContext, MethodDescriptor descriptor) {
this.traceContext = traceContext;
this.descriptor = descriptor;
this.config = new NingAsyncHttpClientPluginConfig(traceContext.getProfilerConfig());
this.cookieSampler = config.isProfileCookie() ? SimpleSamplerFactory.createSampler(true, config.getCookieSamplingRate()) : null;
this.entitySampler = config.isProfileEntity() ? SimpleSamplerFactory.createSampler(true, config.getEntitySamplingRate()) : null;
this.paramSampler = config.isProfileParam() ? SimpleSamplerFactory.createSampler(true, config.getParamSamplingRate()) : null;
}
@Override
public void before(Object target, Object[] args) {
if (isDebug) {
logger.beforeInterceptor(target, args);
}
final Trace trace = traceContext.currentRawTraceObject();
if (trace == null) {
return;
}
if (args.length == 0 || !(args[0] instanceof com.ning.http.client.Request)) {
return;
}
final com.ning.http.client.Request httpRequest = (com.ning.http.client.Request) args[0];
final boolean sampling = trace.canSampled();
if (!sampling) {
if (isDebug) {
logger.debug("set Sampling flag=false");
}
if (httpRequest != null) {
final FluentCaseInsensitiveStringsMap httpRequestHeaders = httpRequest.getHeaders();
httpRequestHeaders.add(Header.HTTP_SAMPLED.toString(), SamplingFlagUtils.SAMPLING_RATE_FALSE);
}
return;
}
trace.traceBlockBegin();
SpanEventRecorder recorder = trace.currentSpanEventRecorder();
TraceId nextId = trace.getTraceId().getNextTraceId();
recorder.recordNextSpanId(nextId.getSpanId());
recorder.recordServiceType(NingAsyncHttpClientPlugin.ASYNC_HTTP_CLIENT);
if (httpRequest != null) {
final FluentCaseInsensitiveStringsMap httpRequestHeaders = httpRequest.getHeaders();
putHeader(httpRequestHeaders, Header.HTTP_TRACE_ID.toString(), nextId.getTransactionId());
putHeader(httpRequestHeaders, Header.HTTP_SPAN_ID.toString(), String.valueOf(nextId.getSpanId()));
putHeader(httpRequestHeaders, Header.HTTP_PARENT_SPAN_ID.toString(), String.valueOf(nextId.getParentSpanId()));
putHeader(httpRequestHeaders, Header.HTTP_FLAGS.toString(), String.valueOf(nextId.getFlags()));
putHeader(httpRequestHeaders, Header.HTTP_PARENT_APPLICATION_NAME.toString(), traceContext.getApplicationName());
putHeader(httpRequestHeaders, Header.HTTP_PARENT_APPLICATION_TYPE.toString(), Short.toString(traceContext.getServerTypeCode()));
final String hostString = getEndpoint(httpRequest.getURI().getHost(), httpRequest.getURI().getPort());
if(hostString != null) {
putHeader(httpRequestHeaders, Header.HTTP_HOST.toString(), hostString);
}
}
}
private void putHeader(FluentCaseInsensitiveStringsMap httpRequestHeaders, String key, String value) {
final List<String> valueList = new ArrayList<String>();
valueList.add(value);
httpRequestHeaders.put(key, valueList);
}
@Override
public void after(Object target, Object[] args, Object result, Throwable throwable) {
if (isDebug) {
// Do not log result
logger.afterInterceptor(target, args);
}
final Trace trace = traceContext.currentTraceObject();
if (trace == null) {
return;
}
if (args.length == 0 || !(args[0] instanceof com.ning.http.client.Request)) {
return;
}
try {
SpanEventRecorder recorder = trace.currentSpanEventRecorder();
final com.ning.http.client.Request httpRequest = (com.ning.http.client.Request) args[0];
if (httpRequest != null) {
// Accessing httpRequest here not BEFORE() because it can cause side effect.
recorder.recordAttribute(AnnotationKey.HTTP_URL, InterceptorUtils.getHttpUrl(httpRequest.getUrl(), config.isProfileParam()));
String endpoint = getEndpoint(httpRequest.getURI().getHost(), httpRequest.getURI().getPort());
recorder.recordDestinationId(endpoint);
recordHttpRequest(recorder, httpRequest, throwable);
}
recorder.recordApi(descriptor);
recorder.recordException(throwable);
} finally {
trace.traceBlockEnd();
}
}
private String getEndpoint(String host, int port) {
if (host == null) {
return "UnknownHttpClient";
}
if (port < 0) {
return host;
}
final StringBuilder sb = new StringBuilder(host.length() + 8);
sb.append(host);
sb.append(':');
sb.append(port);
return sb.toString();
}
private void recordHttpRequest(SpanEventRecorder recorder, com.ning.http.client.Request httpRequest, Throwable throwable) {
final boolean isException = InterceptorUtils.isThrowable(throwable);
if (config.isProfileCookie()) {
switch (config.getCookieDumpType()) {
case ALWAYS:
recordCookie(httpRequest, recorder);
break;
case EXCEPTION:
if (isException) {
recordCookie(httpRequest, recorder);
}
break;
}
}
if (config.isProfileEntity()) {
switch (config.getEntityDumpType()) {
case ALWAYS:
recordEntity(httpRequest, recorder);
break;
case EXCEPTION:
if (isException) {
recordEntity(httpRequest, recorder);
}
break;
}
}
if (config.isProfileParam()) {
switch (config.getParamDumpType()) {
case ALWAYS:
recordParam(httpRequest, recorder);
break;
case EXCEPTION:
if (isException) {
recordParam(httpRequest, recorder);
}
break;
}
}
}
protected void recordCookie(com.ning.http.client.Request httpRequest, SpanEventRecorder recorder) {
if (cookieSampler.isSampling()) {
Collection<Cookie> cookies = httpRequest.getCookies();
if (cookies.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder(config.getCookieDumpSize() * 2);
Iterator<Cookie> iterator = cookies.iterator();
while (iterator.hasNext()) {
Cookie cookie = iterator.next();
sb.append(cookie.getName()).append("=").append(cookie.getValue());
if (iterator.hasNext()) {
sb.append(",");
}
}
recorder.recordAttribute(AnnotationKey.HTTP_COOKIE, StringUtils.abbreviate(sb.toString(), config.getCookieDumpSize()));
}
}
protected void recordEntity(final com.ning.http.client.Request httpRequest, final SpanEventRecorder recorder) {
if (entitySampler.isSampling()) {
recordNonMultipartData(httpRequest, recorder);
recordMultipartData(httpRequest, recorder);
}
}
/**
* <pre>
* Body could be String, byte array, Stream or EntityWriter.
* We collect String data only.
* </pre>
*
* @param httpRequest
* @param recorder
*/
protected void recordNonMultipartData(final com.ning.http.client.Request httpRequest, final SpanEventRecorder recorder) {
final String stringData = httpRequest.getStringData();
if (stringData != null) {
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, StringUtils.abbreviate(stringData, config.getEntityDumpSize()));
return;
}
final byte[] byteData = httpRequest.getByteData();
if (byteData != null) {
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, "BYTE_DATA");
return;
}
final InputStream streamData = httpRequest.getStreamData();
if (streamData != null) {
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, "STREAM_DATA");
return;
}
final EntityWriter entityWriter = httpRequest.getEntityWriter();
if (entityWriter != null) {
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, "STREAM_DATA");
return;
}
}
/**
* record http multipart data
*
* @param httpRequest
* @param recorder
*/
protected void recordMultipartData(final com.ning.http.client.Request httpRequest, final SpanEventRecorder recorder) {
List<Part> parts = httpRequest.getParts();
// bug fix : parts != null && ****!parts.isEmpty()
if (CollectionUtils.isNotEmpty(parts)) {
StringBuilder sb = new StringBuilder(config.getEntityDumpSize() * 2);
Iterator<Part> iterator = parts.iterator();
while (iterator.hasNext()) {
Part part = iterator.next();
if (part instanceof com.ning.http.client.ByteArrayPart) {
com.ning.http.client.ByteArrayPart p = (com.ning.http.client.ByteArrayPart) part;
sb.append(part.getName());
sb.append("=BYTE_ARRAY_");
sb.append(p.getData().length);
} else if (part instanceof com.ning.http.client.FilePart) {
com.ning.http.client.FilePart p = (com.ning.http.client.FilePart) part;
sb.append(part.getName());
sb.append("=FILE_");
sb.append(p.getMimeType());
} else if (part instanceof com.ning.http.client.StringPart) {
com.ning.http.client.StringPart p = (com.ning.http.client.StringPart) part;
sb.append(part.getName());
sb.append("=");
sb.append(p.getValue());
} else if (part instanceof com.ning.http.multipart.FilePart) {
com.ning.http.multipart.FilePart p = (com.ning.http.multipart.FilePart) part;
sb.append(part.getName());
sb.append("=FILE_");
sb.append(p.getContentType());
} else if (part instanceof com.ning.http.multipart.StringPart) {
com.ning.http.multipart.StringPart p = (com.ning.http.multipart.StringPart) part;
sb.append(part.getName());
// Ignore value because there's no way to get string value and StringPart is an adaptation class of Apache HTTP client.
sb.append("=STRING");
}
if (sb.length() >= config.getEntityDumpSize()) {
break;
}
if (iterator.hasNext()) {
sb.append(",");
}
}
recorder.recordAttribute(AnnotationKey.HTTP_PARAM_ENTITY, StringUtils.abbreviate(sb.toString(), config.getEntityDumpSize()));
}
}
/**
* record http request parameter
*
* @param httpRequest
* @param recorder
*/
protected void recordParam(final com.ning.http.client.Request httpRequest, final SpanEventRecorder recorder) {
if (paramSampler.isSampling()) {
FluentStringsMap requestParams = httpRequest.getParams();
if (requestParams != null) {
String params = paramsToString(requestParams, config.getParamDumpSize());
recorder.recordAttribute(AnnotationKey.HTTP_PARAM, StringUtils.abbreviate(params, config.getParamDumpSize()));
}
}
}
/**
* Returns string without double quotations marks, spaces, semi-colons from com.ning.http.client.FluentStringsMap.toString()
*
* @param params
* @param limit
* @return
*/
private String paramsToString(FluentStringsMap params, int limit) {
StringBuilder result = new StringBuilder(limit * 2);
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
if (result.length() > 0) {
result.append(",");
}
result.append(entry.getKey());
result.append("=");
boolean needsComma = false;
for (String value : entry.getValue()) {
if (needsComma) {
result.append(", ");
} else {
needsComma = true;
}
result.append(value);
}
if (result.length() >= limit) {
break;
}
}
return result.toString();
}
}