/* * 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.common.Charsets; import com.navercorp.pinpoint.common.util.StringUtils; import org.apache.http.HeaderElement; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpHost; import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; import org.apache.http.NameValuePair; import org.apache.http.ParseException; import org.apache.http.concurrent.BasicFuture; import org.apache.http.nio.protocol.HttpAsyncRequestProducer; import org.apache.http.protocol.HTTP; import com.navercorp.pinpoint.bootstrap.async.AsyncTraceIdAccessor; import com.navercorp.pinpoint.bootstrap.config.DumpType; import com.navercorp.pinpoint.bootstrap.context.AsyncTraceId; 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.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; import com.navercorp.pinpoint.plugin.httpclient4.HttpClient4PluginConfig; import com.navercorp.pinpoint.plugin.httpclient4.RequestProducerGetter; import com.navercorp.pinpoint.plugin.httpclient4.ResultFutureGetter; /** * * @author minwoo.jung * @author jaehong.kim * */ public class DefaultClientExchangeHandlerImplStartMethodInterceptor implements AroundInterceptor { private final PLogger logger = PLoggerFactory.getLogger(this.getClass()); private final boolean isDebug = logger.isDebugEnabled(); private TraceContext traceContext; private MethodDescriptor methodDescriptor; private boolean param; protected boolean cookie; protected DumpType cookieDumpType; protected SimpleSampler cookieSampler; protected boolean entity; protected DumpType entityDumpType; protected SimpleSampler entitySampler; protected boolean statusCode; public DefaultClientExchangeHandlerImplStartMethodInterceptor(TraceContext traceContext, MethodDescriptor methodDescriptor) { this.traceContext = traceContext; this.methodDescriptor = methodDescriptor; final HttpClient4PluginConfig config = new HttpClient4PluginConfig(traceContext.getProfilerConfig()); this.param = config.isParam(); this.cookie = config.isCookie(); this.cookieDumpType = config.getCookieDumpType(); if (cookie) { this.cookieSampler = SimpleSamplerFactory.createSampler(cookie, config.getCookieSamplingRate()); } this.entity = config.isEntity(); this.entityDumpType = config.getEntityDumpType(); if (entity) { this.entitySampler = SimpleSamplerFactory.createSampler(entity, config.getEntitySamplingRate()); } } @Override public void before(Object target, Object[] args) { if (isDebug) { logger.beforeInterceptor(target, "", methodDescriptor.getMethodName(), "", args); } final Trace trace = traceContext.currentRawTraceObject(); if (trace == null) { return; } final HttpRequest httpRequest = getHttpRequest(target); 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; } SpanEventRecorder recorder = trace.traceBlockBegin(); // set remote trace final 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(target); if (host != null) { final String endpoint = getEndpoint(host.getName(), host.getValue()); logger.debug("Get host {}", endpoint); httpRequest.setHeader(Header.HTTP_HOST.toString(), endpoint); } } try { if (isAsynchronousInvocation(target, args)) { // set asynchronous trace final AsyncTraceId asyncTraceId = trace.getAsyncTraceId(); recorder.recordNextAsyncId(asyncTraceId.getAsyncId()); // check type isAsynchronousInvocation() ((AsyncTraceIdAccessor)((ResultFutureGetter)target)._$PINPOINT$_getResultFuture())._$PINPOINT$_setAsyncTraceId(asyncTraceId); if (isDebug) { logger.debug("Set asyncTraceId metadata {}", asyncTraceId); } } } catch (Throwable t) { logger.warn("Failed to BEFORE process. {}", t.getMessage(), t); } } private HttpRequest getHttpRequest(final Object target) { try { if (!(target instanceof RequestProducerGetter)) { return null; } final HttpAsyncRequestProducer requestProducer = ((RequestProducerGetter)target)._$PINPOINT$_getRequestProducer(); return requestProducer.generateRequest(); } catch (Exception e) { return null; } } private boolean isAsynchronousInvocation(final Object target, final Object[] args) { if (!(target instanceof ResultFutureGetter)) { logger.debug("Invalid target object. Need field accessor({}).", HttpClient4Constants.FIELD_RESULT_FUTURE); return false; } BasicFuture<?> future = ((ResultFutureGetter)target)._$PINPOINT$_getResultFuture(); if (future == null) { logger.debug("Invalid target object. field is null({}).", HttpClient4Constants.FIELD_RESULT_FUTURE); return false; } if (!(future instanceof AsyncTraceIdAccessor)) { logger.debug("Invalid resultFuture field object. Need metadata accessor({}).", HttpClient4Constants.METADATA_ASYNC_TRACE_ID); return false; } return true; } @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 { SpanEventRecorder recorder = trace.currentSpanEventRecorder(); final HttpRequest httpRequest = getHttpRequest(target); 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(target); if (host != null) { final String endpoint = getEndpoint(host.getName(), host.getValue()); recorder.recordDestinationId(endpoint); } recordHttpRequest(recorder, httpRequest, throwable); } recorder.recordApi(methodDescriptor); recorder.recordException(throwable); } finally { trace.traceBlockEnd(); } } private NameIntValuePair<String> getHost(final Object target) { if (!(target instanceof RequestProducerGetter)) { return null; } final HttpAsyncRequestProducer producer = ((RequestProducerGetter)target)._$PINPOINT$_getRequestProducer(); final HttpHost httpHost = producer.getTarget(); if(httpHost != null) { return new NameIntValuePair<String>(httpHost.getHostName(), httpHost.getPort()); } else { 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(SpanEventRecorder recorder, HttpRequest httpRequest, Throwable throwable) { final boolean isException = InterceptorUtils.isThrowable(throwable); if (cookie) { if (DumpType.ALWAYS == cookieDumpType) { recordCookie(httpRequest, recorder); } else if (DumpType.EXCEPTION == cookieDumpType && isException) { recordCookie(httpRequest, recorder); } } if (entity) { if (DumpType.ALWAYS == entityDumpType) { recordEntity(httpRequest, recorder); } else if (DumpType.EXCEPTION == entityDumpType && isException) { recordEntity(httpRequest, recorder); } } } protected void recordCookie(HttpMessage httpMessage, SpanEventRecorder recorder) { org.apache.http.Header[] cookies = httpMessage.getHeaders("Cookie"); for (org.apache.http.Header header : cookies) { final String value = header.getValue(); if (value != null && !value.isEmpty()) { if (cookieSampler.isSampling()) { 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, SpanEventRecorder recorder) { 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); 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 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; } }