/* * 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.test; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import com.google.inject.Module; import com.google.inject.util.Modules; import com.navercorp.pinpoint.bootstrap.context.TraceContext; import com.navercorp.pinpoint.common.util.AnnotationKeyUtils; import com.navercorp.pinpoint.common.util.ArrayUtils; import com.navercorp.pinpoint.profiler.context.module.ApplicationContext; import com.navercorp.pinpoint.profiler.context.module.DefaultApplicationContext; import com.navercorp.pinpoint.profiler.interceptor.registry.InterceptorRegistryBinder; import com.google.common.base.Objects; import com.navercorp.pinpoint.bootstrap.AgentOption; import com.navercorp.pinpoint.bootstrap.context.ServiceInfo; import com.navercorp.pinpoint.bootstrap.plugin.test.Expectations; import com.navercorp.pinpoint.bootstrap.plugin.test.ExpectedAnnotation; import com.navercorp.pinpoint.bootstrap.plugin.test.ExpectedSql; import com.navercorp.pinpoint.bootstrap.plugin.test.ExpectedTrace; import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifier; import com.navercorp.pinpoint.bootstrap.plugin.test.PluginTestVerifierHolder; import com.navercorp.pinpoint.bootstrap.plugin.test.TraceType; import com.navercorp.pinpoint.common.service.AnnotationKeyRegistryService; import com.navercorp.pinpoint.common.trace.AnnotationKey; import com.navercorp.pinpoint.common.trace.LoggingInfo; import com.navercorp.pinpoint.common.trace.ServiceType; import com.navercorp.pinpoint.profiler.DefaultAgent; import com.navercorp.pinpoint.profiler.context.Span; import com.navercorp.pinpoint.profiler.context.SpanEvent; import com.navercorp.pinpoint.profiler.interceptor.registry.DefaultInterceptorRegistryBinder; import com.navercorp.pinpoint.profiler.util.JavaAssistUtils; import com.navercorp.pinpoint.thrift.dto.TAnnotation; import com.navercorp.pinpoint.thrift.dto.TIntStringStringValue; import com.navercorp.pinpoint.thrift.dto.TSpan; import com.navercorp.pinpoint.thrift.dto.TSpanEvent; /** * @author emeroad * @author koo.taejin * @author hyungil.jeong * @author jaehong.kim */ public class PluginTestAgent extends DefaultAgent implements PluginTestVerifier { private AnnotationKeyRegistryService annotationKeyRegistryService; private final List<Short> ignoredServiceTypes = new ArrayList<Short>(); private PluginApplicationContextModule pluginApplicationContextModule; public PluginTestAgent(AgentOption agentOption) { super(agentOption, new DefaultInterceptorRegistryBinder()); this.annotationKeyRegistryService = agentOption.getAnnotationKeyRegistryService(); PluginTestVerifierHolder.setInstance(this); } @Override protected ApplicationContext newApplicationContext(AgentOption agentOption, InterceptorRegistryBinder interceptorRegistryBinder) { this.pluginApplicationContextModule = new PluginApplicationContextModule(); ApplicationContext applicationContext = new DefaultApplicationContext(agentOption, interceptorRegistryBinder) { @Override protected Module newApplicationContextModule(AgentOption agentOption, InterceptorRegistryBinder interceptorRegistryBinder) { Module applicationContextModule = super.newApplicationContextModule(agentOption, interceptorRegistryBinder); return Modules.override(applicationContextModule).with(pluginApplicationContextModule); } }; return applicationContext; } @Override public void verifyServerType(String serviceTypeName) { final ApplicationContext applicationContext = getApplicationContext(); ServiceType expectedType = findServiceType(serviceTypeName); ServiceType actualType = applicationContext.getAgentInformation().getServerType(); if (!expectedType.equals(actualType)) { throw new AssertionError("ResolvedExpectedTrace server type: " + expectedType.getName() + "[" + expectedType.getCode() + "] but was " + actualType + "[" + actualType.getCode() + "]"); } } @Override public void verifyServerInfo(String expected) { String actualName = this.pluginApplicationContextModule.getServerMetaDataListener().getServerMetaData().getServerInfo(); if (!actualName.equals(expected)) { throw new AssertionError("ResolvedExpectedTrace server name [" + expected + "] but was [" + actualName + "]"); } } @Override public void verifyConnector(String protocol, int port) { Map<Integer, String> connectorMap = this.pluginApplicationContextModule.getServerMetaDataListener().getServerMetaData().getConnectors(); String actualProtocol = connectorMap.get(port); if (actualProtocol == null || !actualProtocol.equals(protocol)) { throw new AssertionError("ResolvedExpectedTrace protocol [" + protocol + "] at port [" + port + "] but was [" + actualProtocol + "]"); } } @Override public void verifyService(String name, List<String> libs) { List<ServiceInfo> serviceInfos = this.pluginApplicationContextModule.getServerMetaDataListener().getServerMetaData().getServiceInfos(); for (ServiceInfo serviceInfo : serviceInfos) { if (serviceInfo.getServiceName().equals(name)) { List<String> actualLibs = serviceInfo.getServiceLibs(); if (actualLibs.size() != libs.size()) { throw new AssertionError("ResolvedExpectedTrace service [" + name + "] with libraries [" + libs + "] but was [" + actualLibs + "]"); } for (String lib : libs) { if (!actualLibs.contains(lib)) { throw new AssertionError("ResolvedExpectedTrace service [" + name + "] with libraries [" + libs + "] but was [" + actualLibs + "]"); } } // OK return; } } throw new AssertionError("ResolvedExpectedTrace service [" + name + "] with libraries [" + libs + "] but there is no such service"); } private boolean isIgnored(Object obj) { short serviceType = -1; if (obj instanceof TSpan) { serviceType = ((TSpan) obj).getServiceType(); } else if (obj instanceof TSpanEvent) { serviceType = ((TSpanEvent) obj).getServiceType(); } return ignoredServiceTypes.contains(serviceType); } @Override public void verifyTraceCount(int expected) { int actual = 0; for (Object obj : getRecorder()) { if (!isIgnored(obj)) { actual++; } } if (expected != actual) { throw new AssertionError("ResolvedExpectedTrace count: " + expected + ", actual: " + actual); } } private ServiceType findServiceType(String name) { ServiceType serviceType = getServiceTypeRegistryService().findServiceTypeByName(name); if (serviceType == ServiceType.UNDEFINED) { throw new AssertionError("No such service type: " + name); } return serviceType; } private Class<?> resolveSpanClass(TraceType type) { switch (type) { case ROOT: return Span.class; case EVENT: return SpanEvent.class; } throw new IllegalArgumentException(type.toString()); } @Override public void verifyDiscreteTrace(ExpectedTrace... expectations) { verifyDiscreteTraceBlock(expectations, null); } public void verifyDiscreteTraceBlock(ExpectedTrace[] expectations, Integer asyncId) { if (ArrayUtils.isEmpty(expectations)) { throw new IllegalArgumentException("No expectations"); } ExpectedTrace expected = expectations[0]; ResolvedExpectedTrace resolved = resolveExpectedTrace(expected, asyncId); int i = 0; Iterator<?> iterator = getRecorder().iterator(); while (iterator.hasNext()) { ActualTrace actual = wrap(iterator.next()); try { verifySpan(resolved, actual); } catch (AssertionError e) { continue; } iterator.remove(); verifyAsyncTraces(expected, actual); if (++i == expectations.length) { return; } expected = expectations[i]; resolved = resolveExpectedTrace(expected, asyncId); } throw new AssertionError("Failed to match " + i + "th expectation: " + resolved); } @Override public void verifyTrace(ExpectedTrace... expectations) { if (ArrayUtils.isEmpty(expectations)) { throw new IllegalArgumentException("No expectations"); } for (ExpectedTrace expected : expectations) { ResolvedExpectedTrace resolved = resolveExpectedTrace(expected, null); final Object actual = popSpan(); if (actual == null) { throw new AssertionError("Expected a " + resolved.toString() + " but there is no trace"); } ActualTrace wrapped = wrap(actual); verifySpan(resolved, wrapped); verifyAsyncTraces(expected, wrapped); } } private void verifyAsyncTraces(ExpectedTrace expected, ActualTrace wrapped) throws AssertionError { ExpectedTrace[] asyncTraces = expected.getAsyncTraces(); if (asyncTraces != null && asyncTraces.length > 0) { Integer asyncId = wrapped.getNextAsyncId(); if (asyncId == null) { throw new AssertionError("Expected async traces triggered but nextAsyncId is not present: " + wrapped); } verifyDiscreteTraceBlock(asyncTraces, asyncId); } } private ResolvedExpectedTrace resolveExpectedTrace(ExpectedTrace expected, Integer asyncId) throws AssertionError { final ServiceType serviceType = findServiceType(expected.getServiceType()); final Class<?> spanClass = resolveSpanClass(expected.getType()); final int apiId = getApiId(expected); return new ResolvedExpectedTrace(spanClass, serviceType, apiId, expected.getRpc(), expected.getEndPoint(), expected.getRemoteAddr(), expected.getDestinationId(), expected.getAnnotations(), asyncId); } private int getApiId(ExpectedTrace expected) { final Member method = expected.getMethod(); if (method == null) { if (expected.getMethodSignature() == null) { // return null; throw new RuntimeException("Method or MethodSignature is null"); } else { String methodSignature = expected.getMethodSignature(); if (methodSignature.indexOf('(') != -1) { methodSignature = MethodDescriptionUtils.toJavaMethodDescriptor(methodSignature); } return findApiId(methodSignature); } } else { return findApiId(method); } } @Override public void ignoreServiceType(String... serviceTypes) { for (String serviceType : serviceTypes) { ServiceType t = findServiceType(serviceType); ignoredServiceTypes.add(t.getCode()); } } private static void appendAnnotations(StringBuilder builder, List<TAnnotation> annotations) { boolean first = true; if (annotations != null) { for (TAnnotation a : annotations) { if (first) { first = false; } else { builder.append(", "); } builder.append(toString(a)); } } } private static String toString(TAnnotation a) { return a.getKey() + "=" + a.getValue().getFieldValue(); } private interface ActualTrace { Short getServiceType(); Integer getApiId(); Integer getAsyncId(); Integer getNextAsyncId(); String getRpc(); String getEndPoint(); String getRemoteAddr(); String getDestinationId(); List<TAnnotation> getAnnotations(); Class<?> getType(); } private static final class SpanFacade implements ActualTrace { private final Span span; public SpanFacade(Span span) { this.span = span; } @Override public Short getServiceType() { return span.isSetServiceType() ? span.getServiceType() : null; } @Override public Integer getApiId() { return span.isSetApiId() ? span.getApiId() : null; } @Override public Integer getAsyncId() { return null; } @Override public Integer getNextAsyncId() { return null; } @Override public String getRpc() { return span.getRpc(); } @Override public String getEndPoint() { return span.getEndPoint(); } @Override public String getRemoteAddr() { return span.getRemoteAddr(); } @Override public String getDestinationId() { return null; } @Override public List<TAnnotation> getAnnotations() { return span.getAnnotations(); } @Override public Class<?> getType() { return Span.class; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("(serviceType: "); builder.append(getServiceType()); builder.append(", apiId: "); builder.append(getApiId()); builder.append(", rpc: "); builder.append(getRpc()); builder.append(", endPoint: "); builder.append(getEndPoint()); builder.append(", remoteAddr: "); builder.append(getRemoteAddr()); builder.append(", ["); appendAnnotations(builder, getAnnotations()); builder.append("])"); return builder.toString(); } } private static final class SpanEventFacade implements ActualTrace { private final SpanEvent span; public SpanEventFacade(SpanEvent span) { this.span = span; } @Override public Short getServiceType() { return span.isSetServiceType() ? span.getServiceType() : null; } @Override public Integer getApiId() { return span.isSetApiId() ? span.getApiId() : null; } @Override public Integer getAsyncId() { return span.isSetAsyncId() ? span.getAsyncId() : null; } @Override public Integer getNextAsyncId() { return span.isSetNextAsyncId() ? span.getNextAsyncId() : null; } @Override public String getRpc() { return span.getRpc(); } @Override public String getEndPoint() { return span.getEndPoint(); } @Override public String getRemoteAddr() { return null; } @Override public String getDestinationId() { return span.getDestinationId(); } @Override public List<TAnnotation> getAnnotations() { return span.getAnnotations(); } @Override public Class<?> getType() { return SpanEvent.class; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("(serviceType: "); builder.append(getServiceType()); builder.append(", apiId: "); builder.append(getApiId()); builder.append(", rpc: "); builder.append(getRpc()); builder.append(", endPoint: "); builder.append(getEndPoint()); builder.append(", destinationId: "); builder.append(getDestinationId()); builder.append(", ["); appendAnnotations(builder, getAnnotations()); builder.append("], asyncId: "); builder.append(getAsyncId()); builder.append("nextAsyncId: "); builder.append(getNextAsyncId()); builder.append(')'); return builder.toString(); } } private static final class ResolvedExpectedTrace { private final Class<?> type; private final ServiceType serviceType; private final Integer asyncId; private final Integer apiId; private final String rpc; private final String endPoint; private final String remoteAddr; private final String destinationId; private final ExpectedAnnotation[] annotations; public ResolvedExpectedTrace(Class<?> type, ServiceType serviceType, Integer apiId, String rpc, String endPoint, String remoteAddr, String destinationId, ExpectedAnnotation[] annotations, Integer asyncId) { this.type = type; this.serviceType = serviceType; this.apiId = apiId; this.rpc = rpc; this.endPoint = endPoint; this.remoteAddr = remoteAddr; this.destinationId = destinationId; this.annotations = annotations; this.asyncId = asyncId; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(type.getSimpleName()); builder.append("(serviceType: "); builder.append(serviceType.getCode()); builder.append(", apiId: "); builder.append(apiId); builder.append(", rpc: "); builder.append(rpc); builder.append(", endPoint: "); builder.append(endPoint); builder.append(", remoteAddr: "); builder.append(remoteAddr); builder.append(", destinationId: "); builder.append(destinationId); builder.append(", annotations: "); builder.append(Arrays.deepToString(annotations)); builder.append(", asyncId: "); builder.append(asyncId); builder.append(")"); return builder.toString(); } } private ActualTrace wrap(Object obj) { if (obj instanceof Span) { return new SpanFacade((Span) obj); } else if (obj instanceof SpanEvent) { return new SpanEventFacade((SpanEvent) obj); } throw new IllegalArgumentException("Unexpected type: " + obj.getClass()); } private static boolean equals(Object expected, Object actual) { // if expected is null, no need to compare. return expected == null || (expected.equals(actual)); } private void verifySpan(ResolvedExpectedTrace expected, ActualTrace actual) { if (!expected.type.equals(actual.getType())) { throw new AssertionError("Expected an instance of " + expected.type.getSimpleName() + " but was " + actual.getType().getName() + ". expected: " + expected + ", was: " + actual); } if (!equals(expected.serviceType.getCode(), actual.getServiceType())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with serviceType[" + expected.serviceType.getCode() + "] but was [" + actual.getServiceType() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.apiId, actual.getApiId())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with apiId[" + expected.apiId + "] but was [" + actual.getApiId() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.rpc, actual.getRpc())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with rpc[" + expected.rpc + "] but was [" + actual.getRpc() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.endPoint, actual.getEndPoint())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with endPoint[" + expected.endPoint + "] but was [" + actual.getEndPoint() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.remoteAddr, actual.getRemoteAddr())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with remoteAddr[" + expected.remoteAddr + "] but was [" + actual.getRemoteAddr() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.destinationId, actual.getDestinationId())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with destinationId[" + expected.destinationId + "] but was [" + actual.getDestinationId() + "]. expected: " + expected + ", was: " + actual); } if (!equals(expected.asyncId, actual.getAsyncId())) { throw new AssertionError("Expected a " + expected.type.getSimpleName() + " with asyncId[" + expected.asyncId + "] but was [" + actual.getAsyncId() + "]. expected: " + expected + ", was: " + actual); } List<TAnnotation> actualAnnotations = actual.getAnnotations(); int len = expected.annotations == null ? 0 : expected.annotations.length; int actualLen = actualAnnotations == null ? 0 : actualAnnotations.size(); if (actualLen != len) { throw new AssertionError("Expected [" + len + "] annotations but was [" + actualLen + "], expected: " + expected + ", was: " + actual); } for (int i = 0; i < len; i++) { ExpectedAnnotation expect = expected.annotations[i]; AnnotationKey expectedAnnotationKey = annotationKeyRegistryService.findAnnotationKeyByName(expect.getKeyName()); TAnnotation actualAnnotation = actualAnnotations.get(i); if (expectedAnnotationKey.getCode() != actualAnnotation.getKey()) { throw new AssertionError("Expected " + i + "th annotation [" + expectedAnnotationKey.getCode() + "=" + expect.getValue() + "] but was [" + toString(actualAnnotation) + "], expected: " + expected + ", was: " + actual); } if (expectedAnnotationKey == AnnotationKey.SQL_ID && expect instanceof ExpectedSql) { verifySql((ExpectedSql) expect, actualAnnotation); } else { Object expectedValue = expect.getValue(); if (expectedValue == Expectations.anyAnnotationValue()) { continue; } if (AnnotationKeyUtils.isCachedArgsKey(expectedAnnotationKey.getCode())) { expectedValue = getTestTcpDataSender().getStringId(expectedValue.toString()); } if (!Objects.equal(expectedValue, actualAnnotation.getValue().getFieldValue())) { throw new AssertionError("Expected " + i + "th annotation [" + expectedAnnotationKey.getCode() + "=" + expect.getValue() + "] but was [" + toString(actualAnnotation) + "], expected: " + expected + ", was: " + actual); } } } } private void verifySql(ExpectedSql expected, TAnnotation actual) { int id = getTestTcpDataSender().getSqlId(expected.getQuery()); TIntStringStringValue value = actual.getValue().getIntStringStringValue(); if (value.getIntValue() != id) { String actualQuery = getTestTcpDataSender().getSql(value.getIntValue()); throw new AssertionError("Expected sql [" + id + ": " + expected.getQuery() + "] but was [" + value.getIntValue() + ": " + actualQuery + "], expected: " + expected + ", was: " + actual); } if (!Objects.equal(value.getStringValue1(), expected.getOutput())) { throw new AssertionError("Expected sql with output [" + expected.getOutput() + "] but was [" + value.getStringValue1() + "], expected: " + expected + ", was: " + actual); } if (!Objects.equal(value.getStringValue2(), expected.getBindValuesAsString())) { throw new AssertionError("Expected sql with bindValues [" + expected.getBindValuesAsString() + "] but was [" + value.getStringValue2() + "], expected: " + expected + ", was: " + actual); } } private int findApiId(Member method) throws AssertionError { final String desc = getMemberInfo(method); return findApiId(desc); } private String getMemberInfo(Member method) { if (method instanceof Method) { return getMethodInfo((Method) method); } else if (method instanceof Constructor) { return getConstructorInfo((Constructor<?>) method); } else { throw new IllegalArgumentException("method: " + method); } } private String getMethodInfo(Method method) { Class<?>[] parameterTypes = method.getParameterTypes(); String[] parameterTypeNames = JavaAssistUtils.toPinpointParameterType(parameterTypes); return MethodDescriptionUtils.toJavaMethodDescriptor(method.getDeclaringClass().getName(), method.getName(), parameterTypeNames); } private String getConstructorInfo(Constructor<?> constructor) { Class<?>[] parameterTypes = constructor.getParameterTypes(); String[] parameterTypeNames = JavaAssistUtils.getParameterType(parameterTypes); final String constructorSimpleName = MethodDescriptionUtils.getConstructorSimpleName(constructor); return MethodDescriptionUtils.toJavaMethodDescriptor(constructor.getDeclaringClass().getName(), constructorSimpleName , parameterTypeNames); } private int findApiId(String desc) throws AssertionError { try { return getTestTcpDataSender().getApiId(desc); } catch (NoSuchElementException e) { throw new AssertionError("Cannot find apiId of [" + desc + "]"); } } private TestTcpDataSender getTestTcpDataSender() { return this.pluginApplicationContextModule.getTcpDataSender(); } private OrderedSpanRecorder getRecorder() { return this.pluginApplicationContextModule.getOrderedSpanRecorder(); } private Object popSpan() { while (true) { OrderedSpanRecorder recorder = getRecorder(); Object obj = recorder.pop(); if (obj == null) { return null; } if (!isIgnored(obj)) { return obj; } } } @Override public void printCache(PrintStream out) { getRecorder().print(out); getTestTcpDataSender().printDatas(out); } @Override public void printCache() { printCache(System.out); } @Override public void initialize(boolean createTraceObject) { if (createTraceObject) { final TraceContext traceContext = getTraceContext(); traceContext.newTraceObject(); } getRecorder().clear(); getTestTcpDataSender().clear(); ignoredServiceTypes.clear(); } @Override public void cleanUp(boolean detachTraceObject) { if (detachTraceObject) { final TraceContext traceContext = getTraceContext(); traceContext.removeTraceObject(); } getRecorder().clear(); getTestTcpDataSender().clear(); ignoredServiceTypes.clear(); } private TraceContext getTraceContext() { ApplicationContext applicationContext = getApplicationContext(); return applicationContext.getTraceContext(); } @Override public void verifyIsLoggingTransactionInfo(LoggingInfo loggingInfo) { Object actual = popSpan(); Span span = null; if (actual instanceof Span) { span = (Span) actual; } else if (actual instanceof SpanEvent) { span = ((SpanEvent) actual).getSpan(); } else { throw new IllegalArgumentException("Unexpected type: " + getActual(actual)); } if (span.getLoggingTransactionInfo() != loggingInfo.getCode()) { LoggingInfo loggingTransactionInfo = LoggingInfo.searchByCode(span.getLoggingTransactionInfo()); if (loggingTransactionInfo != null) { throw new AssertionError("Expected a Span isLoggingTransactionInfo value with [" + loggingInfo.getName() + "] but was [" + loggingTransactionInfo.getName() + "]. expected: " + loggingInfo.getName() + ", was: " + loggingTransactionInfo.getName()); } else { throw new AssertionError("Expected a Span isLoggingTransactionInfo value with [" + loggingInfo.getName() + "] but loggingTransactionInfo value invalid."); } } } private String getActual(Object actual) { if (actual == null) { return "actual is null"; } return actual.getClass().getName(); } }