Skip to content

Commit

Permalink
Use display-name or context path as the default service name
Browse files Browse the repository at this point in the history
Enables to have multiple service names per JVM,
one per class loader.

closes elastic#136
  • Loading branch information
felixbarny committed Mar 5, 2019
1 parent 9e64b19 commit ecc2233
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

class ServiceNameUtil {
public class ServiceNameUtil {
private static final String JAR_VERSION_SUFFIX = "-(\\d+\\.)+(\\d+)(-.*)?$";

static String getDefaultServiceName() {
Expand Down Expand Up @@ -70,7 +70,7 @@ private static String getSpecialServiceName(String command) {
return null;
}

private static String replaceDisallowedChars(String serviceName) {
public static String replaceDisallowedChars(String serviceName) {
return serviceName.replaceAll("[^a-zA-Z0-9 _-]", "-");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package co.elastic.apm.agent.impl;

import co.elastic.apm.agent.configuration.CoreConfiguration;
import co.elastic.apm.agent.configuration.ServiceNameUtil;
import co.elastic.apm.agent.context.LifecycleListener;
import co.elastic.apm.agent.impl.async.ContextInScopeCallableWrapper;
import co.elastic.apm.agent.impl.async.ContextInScopeRunnableWrapper;
Expand All @@ -40,6 +41,7 @@
import co.elastic.apm.agent.objectpool.impl.QueueBasedObjectPool;
import co.elastic.apm.agent.report.Reporter;
import co.elastic.apm.agent.report.ReporterConfiguration;
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
import org.jctools.queues.atomic.AtomicQueueFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -97,6 +99,7 @@ protected Deque<TraceContextHolder<?>> initialValue() {
private final MetricRegistry metricRegistry;
private Sampler sampler;
boolean assertionsEnabled = false;
private static final WeakConcurrentMap<ClassLoader, String> serviceNameByClassLoader = new WeakConcurrentMap.WithInlinedExpunction<>();

ElasticApmTracer(ConfigurationRegistry configurationRegistry, Reporter reporter, Iterable<LifecycleListener> lifecycleListeners, List<ActivationListener> activationListeners) {
this.metricRegistry = new MetricRegistry(configurationRegistry.getConfig(ReporterConfiguration.class));
Expand Down Expand Up @@ -178,15 +181,68 @@ public void onChange(ConfigurationOption<?> configurationOption, Double oldValue
assert assertionsEnabled = true;
}

/**
* Starts a root transaction
*
* @return a root transaction
*/
public Transaction startTransaction() {
return startTransaction(TraceContext.asRoot(), null);
}

/**
* Starts a transaction as a child of the provided parent
*
* @param childContextCreator used to make the transaction a child of the provided parent
* @param parent the parent of the transaction. May be a traceparent header.
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
* @return a transaction which is a child of the provided parent
*/
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent) {
return startTransaction(childContextCreator, parent, sampler, -1);
return startTransaction(childContextCreator, parent, null);
}

/**
* Starts a transaction as a child of the provided parent
*
* @param childContextCreator used to make the transaction a child of the provided parent
* @param parent the parent of the transaction. May be a traceparent header.
* @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction.
* Used to determine the service name.
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
* @return a transaction which is a child of the provided parent
*/
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, @Nullable ClassLoader initiatingClassLoader) {
return startTransaction(childContextCreator, parent, sampler, -1, initiatingClassLoader);
}

/**
* Starts a transaction as a child of the provided parent
*
* @param childContextCreator used to make the transaction a child of the provided parent
* @param parent the parent of the transaction. May be a traceparent header.
* @param sampler the {@link Sampler} instance which is responsible for determining the sampling decision if this is a root transaction
* @param epochMicros the start timestamp
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
* @return a transaction which is a child of the provided parent
*/
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros) {
return startTransaction(childContextCreator, parent, sampler, epochMicros, null);
}

/**
* Starts a transaction as a child of the provided parent
*
* @param childContextCreator used to make the transaction a child of the provided parent
* @param parent the parent of the transaction. May be a traceparent header.
* @param sampler the {@link Sampler} instance which is responsible for determining the sampling decision if this is a root transaction
* @param epochMicros the start timestamp
* @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction
* Used to determine the service name.
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
* @return a transaction which is a child of the provided parent
*/
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros, @Nullable ClassLoader initiatingClassLoader) {
Transaction transaction;
if (!coreConfiguration.isActive()) {
transaction = noopTransaction();
Expand All @@ -200,6 +256,10 @@ public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> chil
new RuntimeException("this exception is just used to record where the transaction has been started from"));
}
}
final String serviceName = getServiceName(initiatingClassLoader);
if (serviceName != null) {
transaction.setServiceName(serviceName);
}
return transaction;
}

Expand Down Expand Up @@ -259,7 +319,9 @@ public Span startSpan(AbstractSpan<?> parent, long epochMicros) {
dropped = false;
transaction.getSpanCount().getStarted().incrementAndGet();
}
span.setServiceName(transaction.getServiceName());
} else {
// TODO determine service name
dropped = false;
}
span.start(TraceContext.fromParent(), parent, epochMicros, dropped);
Expand All @@ -283,6 +345,9 @@ public void captureException(long epochMicros, @Nullable Throwable e, @Nullable
if (currentTransaction != null) {
error.setTransactionType(currentTransaction.getType());
error.setTransactionSampled(currentTransaction.isSampled());
error.setServiceName(currentTransaction.getServiceName());
} else {
// TODO determine service name
}
if (parent != null) {
error.asChildOf(parent);
Expand Down Expand Up @@ -464,4 +529,30 @@ private void assertIsActive(Object span, @Nullable Object currentlyActive) {
public MetricRegistry getMetricRegistry() {
return metricRegistry;
}

/**
* Overrides the service name for all {@link Transaction}s,
* {@link Span}s and {@link ErrorCapture}s which are created by the service which corresponds to the provided {@link ClassLoader}.
* <p>
* The main use case is being able to differentiate between multiple services deployed to the same application server.
* </p>
*
* @param classLoader the class loader which corresponds to a particular service
* @param serviceName the service name for this class loader
*/
public void overrideServiceNameForClassLoader(@Nullable ClassLoader classLoader, String serviceName) {
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
if (!serviceNameByClassLoader.containsKey(classLoader)) {
serviceNameByClassLoader.putIfAbsent(classLoader, ServiceNameUtil.replaceDisallowedChars(serviceName));
}
}

private String getServiceName(@Nullable ClassLoader initiatingClassLoader) {
if (initiatingClassLoader == null) {
initiatingClassLoader = ClassLoader.getSystemClassLoader();
}
return serviceNameByClassLoader.get(initiatingClassLoader);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public class ErrorCapture implements Recyclable {

private ElasticApmTracer tracer;
private final StringBuilder culprit = new StringBuilder();
@Nullable
private String serviceName;

public ErrorCapture(ElasticApmTracer tracer) {
this.tracer = tracer;
Expand Down Expand Up @@ -109,6 +111,7 @@ public void resetState() {
transactionInfo.resetState();
traceContext.resetState();
culprit.setLength(0);
serviceName = null;
}

public void recycle() {
Expand Down Expand Up @@ -193,6 +196,15 @@ private void setCulprit(StackTraceElement stackTraceElement) {
culprit.append(')');
}

public void setServiceName(@Nullable String serviceName) {
this.serviceName = serviceName;
}

@Nullable
public String getServiceName() {
return serviceName;
}

public static class TransactionInfo implements Recyclable {
/**
* A hint for UI to be able to show whether a recorded trace for the corresponding transaction is expected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public abstract class AbstractSpan<T extends AbstractSpan> extends TraceContextH
protected double duration;

private volatile boolean finished = true;
@Nullable
private String serviceName;

public AbstractSpan(ElasticApmTracer tracer) {
super(tracer);
Expand Down Expand Up @@ -114,7 +116,7 @@ public void resetState() {
duration = 0;
isLifecycleManagingThreadSwitch = false;
traceContext.resetState();
// don't reset previouslyActive, as deactivate can be called after end
serviceName = null;
}

public boolean isChildOf(AbstractSpan<?> parent) {
Expand Down Expand Up @@ -208,4 +210,18 @@ public <V> Callable<V> withActive(Callable<V> callable) {
return tracer.wrapCallable(callable, traceContext);
}
}

/**
* Overrides the {@link co.elastic.apm.agent.impl.payload.Service#name} property sent via the meta data Intake V2 event.
*
* @param serviceName the service name for this event
*/
public void setServiceName(@Nullable String serviceName) {
this.serviceName = serviceName;
}

@Nullable
public String getServiceName() {
return serviceName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ private void serializeError(ErrorCapture errorCapture) {
jw.writeByte(JsonWriter.OBJECT_START);

writeTimestamp(errorCapture.getTimestamp());
serializeServiceName(errorCapture.getServiceName());
serializeErrorTransactionInfo(errorCapture.getTransactionInfo());
if (errorCapture.getTraceContext().hasContent()) {
serializeTraceContext(errorCapture.getTraceContext(), true);
Expand Down Expand Up @@ -482,6 +483,7 @@ private void serializeTransaction(final Transaction transaction) {
writeField("type", transaction.getType());
writeField("duration", transaction.getDuration());
writeField("result", transaction.getResult());
serializeServiceName(transaction.getServiceName());
serializeContext(transaction.getContext());
serializeSpanCount(transaction.getSpanCount());
writeLastField("sampled", transaction.isSampled());
Expand Down Expand Up @@ -522,6 +524,7 @@ private void serializeSpan(final Span span) {
writeTimestamp(span.getTimestamp());
serializeTraceContext(span.getTraceContext(), true);
writeField("duration", span.getDuration());
serializeServiceName(span.getServiceName());
if (span.getStacktrace() != null) {
serializeStacktrace(span.getStacktrace().getStackTrace());
}
Expand All @@ -532,6 +535,16 @@ private void serializeSpan(final Span span) {
jw.writeByte(OBJECT_END);
}

private void serializeServiceName(@Nullable String serviceName) {
if (serviceName != null) {
writeFieldName("service");
jw.writeByte(OBJECT_START);
writeLastField("name", serviceName);
jw.writeByte(OBJECT_END);
jw.writeByte(COMMA);
}
}

/**
* TODO: remove in 2.0
* To be removed for agents working only with APM server 7.0 or higher, where schema contains span.type, span.subtype and span.action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static void onEnterServletService(@Advice.Argument(0) ServletRequest serv

final HttpServletRequest request = (HttpServletRequest) servletRequest;
transaction = servletTransactionHelper.onBefore(
request.getServletContext().getClassLoader(),
request.getServletPath(), request.getPathInfo(),
request.getHeader("User-Agent"),
request.getHeader(TraceContext.TRACE_PARENT_HEADER));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package co.elastic.apm.agent.servlet;

import co.elastic.apm.agent.bci.ElasticApmInstrumentation;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import javax.annotation.Nullable;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import java.util.Collection;
import java.util.Collections;

import static co.elastic.apm.agent.servlet.ServletInstrumentation.SERVLET_API;
import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
import static net.bytebuddy.matcher.ElementMatchers.isInterface;
import static net.bytebuddy.matcher.ElementMatchers.nameContains;
import static net.bytebuddy.matcher.ElementMatchers.nameContainsIgnoreCase;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

public class ServletContextServiceNameInstrumentation extends ElasticApmInstrumentation {
@Override
public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() {
return nameContains("Servlet").or(nameContainsIgnoreCase("jsp"));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return not(isInterface())
.and(hasSuperType(named("javax.servlet.Servlet")));
}

@Override
public ElementMatcher<? super MethodDescription> getMethodMatcher() {
return named("init")
.and(takesArgument(0, named("javax.servlet.ServletConfig")))
.and(takesArguments(1));
}

@Override
public Class<?> getAdviceClass() {
return ServletContextServiceNameAdvice.class;
}

@Override
public Collection<String> getInstrumentationGroupNames() {
return Collections.singleton(SERVLET_API);
}

public static class ServletContextServiceNameAdvice {

@Advice.OnMethodEnter
public static void onServletInit(@Nullable @Advice.Argument(0) ServletConfig servletConfig) {
if (tracer == null || servletConfig == null) {
return;
}
ServletContext servletContext = servletConfig.getServletContext();
if (servletContext == null) {
return;
}
String serviceName = servletContext.getServletContextName();
final String contextPath = servletContext.getContextPath();
if (serviceName == null && contextPath != null && !contextPath.isEmpty()) {
serviceName = contextPath;
}
if (serviceName != null) {
tracer.overrideServiceNameForClassLoader(servletContext.getClassLoader(), serviceName);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ public void init(ElasticApmTracer tracer) {

@Override
public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() {
return nameContains("Servlet");
return nameContains("Servlet").or(nameContainsIgnoreCase("jsp"));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return not(isInterface())
.and(hasSuperType(named("javax.servlet.Servlet")).or(nameContainsIgnoreCase("jsp")));
.and(hasSuperType(named("javax.servlet.Servlet")));
}

@Override
Expand Down
Loading

0 comments on commit ecc2233

Please sign in to comment.