diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8b6a523a..d20b9378ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # next (1.5.0) +## Potentially breaking changes + * If you didn't explicitly set the [`service_name`](https://www.elastic.co/guide/en/apm/agent/java/master/config-core.html#config-service-name) + previously and you are dealing with a servlet-based application (including Spring Boot), + your `service_name` will change. + See the documentation for [`service_name`](https://www.elastic.co/guide/en/apm/agent/java/master/config-core.html#config-service-name) + and the corresponding section in _Features_ for more information. + Note: this requires APM Server 7.0+. If using previous versions, nothing will change. + ## Features * Support for number and boolean labels in the public API (#497). This change also renames `tag` to `label` on the API level to be compliant with the [Elastic Common Schema (ECS)](https://github.com/elastic/ecs#-base-fields). @@ -9,6 +17,11 @@ * Support async queries made by Elasticsearch REST client * Added `setStartTimestamp(long epochMicros)` and `end(long epochMicros)` API methods to `Span` and `Transaction`, allowing to set custom start and end timestamps. + * Auto-detection of the `service_name` based on the `` element of the `web.xml` with a fallback to the servlet context path. + If you are using a spring-based application, the agent will use the setting for `spring.application.name` for its `service_name`. + See the documentation for [`service_name`](https://www.elastic.co/guide/en/apm/agent/java/master/config-core.html#config-service-name) + for more information. + Note: this requires APM Server 7.0+. If using previous versions, nothing will change. ## Bug Fixes diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java index 887d4f83ac..294250ea8e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java @@ -23,6 +23,7 @@ import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory; import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.TraceContextHolder; import co.elastic.apm.agent.matcher.WildcardMatcher; import net.bytebuddy.asm.Advice; @@ -58,12 +59,13 @@ public TraceMethodInstrumentation(MethodMatcher methodMatcher) { } @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onMethodEnter(@SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature, + public static void onMethodEnter(@Advice.Origin Class clazz, + @SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature, @Advice.Local("span") AbstractSpan span) { if (tracer != null) { final TraceContextHolder parent = tracer.getActive(); if (parent == null) { - span = tracer.startTransaction() + span = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader()) .withName(signature) .activate(); } else { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java index b25c57ba51..4f867ff817 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java @@ -73,8 +73,20 @@ public class CoreConfiguration extends ConfigurationOptionProvider { .description("This is used to keep all the errors and transactions of your service together\n" + "and is the primary filter in the Elastic APM user interface.\n" + "\n" + - "NOTE: The service name must conform to this regular expression: ^[a-zA-Z0-9 _-]+$. In less regexy terms: Your service name " + - "must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces.") + "The service name must conform to this regular expression: `^[a-zA-Z0-9 _-]+$`.\n" + + "In less regexy terms:\n" + + "Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces.\n" + + "\n" + + "NOTE: When relying on auto-discovery of the service name in Servlet environments (including Spring Boot),\n" + + "there is currently a caveat related to metrics.\n" + + "The consequence is that the 'Metrics' tab of a service does not show process-global metrics like CPU utilization.\n" + + "The reason is that metrics are reported with the detected default service name for the JVM,\n" + + "for example `tomcat-application`.\n" + + "That is because there may be multiple web applications deployed to a single JVM/servlet container.\n" + + "However, you can view those metrics by selecting the `tomcat-application` service name, for example.\n" + + "Future versions of the Elastic APM stack will have better support for that scenario.\n" + + "A workaround is to explicitly set the `service_name` which means all applications deployed to the same servlet container will have the same name\n" + + "or to disable the corresponding `*-service-name` detecting instrumentations via <>.") .addValidator(RegexValidator.of("^[a-zA-Z0-9 _-]+$", "Your service name \"{0}\" must only contain characters " + "from the ASCII alphabet, numbers, dashes, underscores and spaces")) .buildWithDefault(ServiceNameUtil.getDefaultServiceName()); @@ -303,6 +315,10 @@ public String getServiceName() { return serviceName.get(); } + public ConfigurationOption getServiceNameConfig() { + return serviceName; + } + public String getServiceVersion() { return serviceVersion.get(); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceNameUtil.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceNameUtil.java index a44493ab7b..e4a611836f 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceNameUtil.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceNameUtil.java @@ -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() { @@ -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 _-]", "-"); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java index 1be07ff8a0..a1df8275a2 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java @@ -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; @@ -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; @@ -97,6 +99,7 @@ protected Deque> initialValue() { private final MetricRegistry metricRegistry; private Sampler sampler; boolean assertionsEnabled = false; + private static final WeakConcurrentMap serviceNameByClassLoader = new WeakConcurrentMap.WithInlinedExpunction<>(); ElasticApmTracer(ConfigurationRegistry configurationRegistry, Reporter reporter, Iterable lifecycleListeners, List activationListeners) { this.metricRegistry = new MetricRegistry(configurationRegistry.getConfig(ReporterConfiguration.class)); @@ -178,15 +181,33 @@ public void onChange(ConfigurationOption configurationOption, Double oldValue assert assertionsEnabled = true; } - public Transaction startTransaction() { - return startTransaction(TraceContext.asRoot(), null); - } - - public Transaction startTransaction(TraceContext.ChildContextCreator childContextCreator, @Nullable T parent) { - return startTransaction(childContextCreator, parent, sampler, -1); + /** + * 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 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 Transaction startTransaction(TraceContext.ChildContextCreator childContextCreator, @Nullable T parent, @Nullable ClassLoader initiatingClassLoader) { + return startTransaction(childContextCreator, parent, sampler, -1, initiatingClassLoader); } - public Transaction startTransaction(TraceContext.ChildContextCreator childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros) { + /** + * 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 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 Transaction startTransaction(TraceContext.ChildContextCreator childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros, @Nullable ClassLoader initiatingClassLoader) { Transaction transaction; if (!coreConfiguration.isActive()) { transaction = noopTransaction(); @@ -200,6 +221,10 @@ public Transaction startTransaction(TraceContext.ChildContextCreator 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.getTraceContext().setServiceName(serviceName); + } return transaction; } @@ -269,11 +294,21 @@ private boolean isTransactionSpanLimitReached(Transaction transaction) { return coreConfiguration.getTransactionMaxSpans() <= transaction.getSpanCount().getStarted().get(); } - public void captureException(@Nullable Throwable e) { - captureException(System.currentTimeMillis() * 1000, e, getActive()); + /** + * Captures an exception without providing an explicit reference to a parent {@link TraceContextHolder} + * + * @param e the exception to capture + * @param initiatingClassLoader the class + */ + public void captureException(@Nullable Throwable e, ClassLoader initiatingClassLoader) { + captureException(System.currentTimeMillis() * 1000, e, getActive(), initiatingClassLoader); } - public void captureException(long epochMicros, @Nullable Throwable e, @Nullable TraceContextHolder parent) { + public void captureException(long epochMicros, @Nullable Throwable e, TraceContextHolder parent) { + captureException(epochMicros, e, parent, null); + } + + public void captureException(long epochMicros, @Nullable Throwable e, @Nullable TraceContextHolder parent, @Nullable ClassLoader initiatingClassLoader) { if (e != null) { ErrorCapture error = errorPool.createInstance(); error.withTimestamp(epochMicros); @@ -287,6 +322,7 @@ public void captureException(long epochMicros, @Nullable Throwable e, @Nullable error.asChildOf(parent); } else { error.getTraceContext().getId().setToRandomValue(); + error.getTraceContext().setServiceName(getServiceName(initiatingClassLoader)); } reporter.report(error); } @@ -463,4 +499,40 @@ 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}. + *

+ * The main use case is being able to differentiate between multiple services deployed to the same application server. + *

+ * + * @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, @Nullable String serviceName) { + // overriding the service name for the bootstrap class loader is not an actual use-case + // null may also mean we don't know about the initiating class loader + if (classLoader == null + || serviceName == null || serviceName.isEmpty() + // if the service name is set explicitly, don't override it + || !coreConfiguration.getServiceNameConfig().isDefault()) { + return; + } + if (!serviceNameByClassLoader.containsKey(classLoader)) { + serviceNameByClassLoader.putIfAbsent(classLoader, ServiceNameUtil.replaceDisallowedChars(serviceName)); + } + } + + @Nullable + private String getServiceName(@Nullable ClassLoader initiatingClassLoader) { + if (initiatingClassLoader == null) { + return null; + } + return serviceNameByClassLoader.get(initiatingClassLoader); + } + + public void resetServiceNameOverrides() { + serviceNameByClassLoader.clear(); + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java index dd1a788ebc..a3f587cbb0 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java @@ -114,7 +114,6 @@ public void resetState() { duration = 0; isLifecycleManagingThreadSwitch = false; traceContext.resetState(); - // don't reset previouslyActive, as deactivate can be called after end } public boolean isChildOf(AbstractSpan parent) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java index 95df275b11..09f84a4c12 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import java.util.concurrent.Callable; /** @@ -100,6 +101,8 @@ public boolean asChildOf(TraceContext child, Object ignore) { * @see EpochTickClock */ private EpochTickClock clock = new EpochTickClock(); + @Nullable + private String serviceName; private TraceContext(ElasticApmTracer tracer, Id id) { super(tracer); @@ -214,6 +217,7 @@ public void asChildOf(TraceContext parent) { flags = parent.flags; id.setToRandomValue(); clock.init(parent.clock); + serviceName = parent.serviceName; onMutation(); } @@ -229,6 +233,7 @@ public void resetState() { outgoingHeader.setLength(0); flags = 0; clock.resetState(); + serviceName = null; } /** @@ -335,6 +340,7 @@ public void copyFrom(TraceContext other) { outgoingHeader.append(other.outgoingHeader); flags = other.flags; clock.init(other.clock); + serviceName = other.serviceName; onMutation(); } @@ -351,6 +357,20 @@ public boolean isRoot() { return parentId.isEmpty(); } + @Nullable + public String getServiceName() { + return serviceName; + } + + /** + * 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; + } + @Override public TraceContext getTraceContext() { return this; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java index 10957442f7..3d8ae2150a 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java @@ -237,7 +237,7 @@ private void serializeError(ErrorCapture errorCapture) { if (errorCapture.getTraceContext().hasContent()) { serializeTraceContext(errorCapture.getTraceContext(), true); } - serializeContext(errorCapture.getContext()); + serializeContext(errorCapture.getContext(), errorCapture.getTraceContext()); writeField("culprit", errorCapture.getCulprit()); serializeException(errorCapture.getException()); @@ -482,7 +482,7 @@ private void serializeTransaction(final Transaction transaction) { writeField("type", transaction.getType()); writeField("duration", transaction.getDuration()); writeField("result", transaction.getResult()); - serializeContext(transaction.getContext()); + serializeContext(transaction.getContext(), transaction.getTraceContext()); serializeSpanCount(transaction.getSpanCount()); writeLastField("sampled", transaction.isSampled()); jw.writeByte(OBJECT_END); @@ -525,13 +525,22 @@ private void serializeSpan(final Span span) { if (span.getStacktrace() != null) { serializeStacktrace(span.getStacktrace().getStackTrace()); } - if (span.getContext().hasContent()) { - serializeSpanContext(span.getContext()); - } + serializeSpanContext(span.getContext(), span.getTraceContext()); serializeSpanType(span); jw.writeByte(OBJECT_END); } + private void serializeServiceName(TraceContext traceContext) { + String serviceName = traceContext.getServiceName(); + 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 @@ -634,66 +643,56 @@ private boolean isLibraryFrame(String className) { return true; } - private void serializeSpanContext(SpanContext context) { + private void serializeSpanContext(SpanContext context, TraceContext traceContext) { writeFieldName("context"); jw.writeByte(OBJECT_START); - boolean spanContextWritten = false; - Db db = context.getDb(); - if (db.hasContent()) { - serializeDbContext(db); - spanContextWritten = true; - } - Http http = context.getHttp(); - if (http.hasContent()) { - if (spanContextWritten) { - jw.writeByte(COMMA); - } - serializeHttpContext(http); - spanContextWritten = true; - } + serializeServiceName(traceContext); + serializeDbContext(context.getDb()); + serializeHttpContext(context.getHttp()); - if (context.hasLabels()) { - if (spanContextWritten) { - jw.writeByte(COMMA); - } - writeFieldName("tags"); - serializeLabels(context); - } + writeFieldName("tags"); + serializeLabels(context); jw.writeByte(OBJECT_END); jw.writeByte(COMMA); } private void serializeDbContext(final Db db) { - writeFieldName("db"); - jw.writeByte(OBJECT_START); - writeField("instance", db.getInstance()); - if (db.getStatement() != null) { - writeLongStringField("statement", db.getStatement()); - } else { - final CharBuffer statementBuffer = db.getStatementBuffer(); - if (statementBuffer != null && statementBuffer.length() > 0) { - writeFieldName("statement"); - jw.writeString(statementBuffer); - jw.writeByte(COMMA); + if (db.hasContent()) { + writeFieldName("db"); + jw.writeByte(OBJECT_START); + writeField("instance", db.getInstance()); + if (db.getStatement() != null) { + writeLongStringField("statement", db.getStatement()); + } else { + final CharBuffer statementBuffer = db.getStatementBuffer(); + if (statementBuffer != null && statementBuffer.length() > 0) { + writeFieldName("statement"); + jw.writeString(statementBuffer); + jw.writeByte(COMMA); + } } + writeField("type", db.getType()); + writeLastField("user", db.getUser()); + jw.writeByte(OBJECT_END); + jw.writeByte(COMMA); } - writeField("type", db.getType()); - writeLastField("user", db.getUser()); - jw.writeByte(OBJECT_END); } private void serializeHttpContext(final Http http) { - writeFieldName("http"); - jw.writeByte(OBJECT_START); - writeField("method", http.getMethod()); - int statusCode = http.getStatusCode(); - if (statusCode > 0) { - writeField("status_code", http.getStatusCode()); + if (http.hasContent()) { + writeFieldName("http"); + jw.writeByte(OBJECT_START); + writeField("method", http.getMethod()); + int statusCode = http.getStatusCode(); + if (statusCode > 0) { + writeField("status_code", http.getStatusCode()); + } + writeLastField("url", http.getUrl()); + jw.writeByte(OBJECT_END); + jw.writeByte(COMMA); } - writeLastField("url", http.getUrl()); - jw.writeByte(OBJECT_END); } private void serializeSpanCount(final SpanCount spanCount) { @@ -705,9 +704,10 @@ private void serializeSpanCount(final SpanCount spanCount) { jw.writeByte(COMMA); } - private void serializeContext(final TransactionContext context) { + private void serializeContext(final TransactionContext context, TraceContext traceContext) { writeFieldName("context"); jw.writeByte(OBJECT_START); + serializeServiceName(traceContext); if (context.getUser().hasContent()) { serializeUser(context.getUser()); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java index c8d184f3c8..3c91c772b4 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/AbstractInstrumentationTest.java @@ -85,6 +85,7 @@ public final void resetReporter() { @After @AfterEach public final void cleanUp() { + tracer.resetServiceNameOverrides(); assertThat(tracer.getActive()).isNull(); } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/SpyConfiguration.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/SpyConfiguration.java index fcca3dd896..1f6fbead5a 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/SpyConfiguration.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/SpyConfiguration.java @@ -47,7 +47,7 @@ public static ConfigurationRegistry createSpyConfig() { builder.addOptionProvider(spy(options)); } return builder - .addConfigSource(new SimpleSource(CONFIG_SOURCE_NAME).add("service_name", "elastic-apm-test")) + .addConfigSource(new SimpleSource(CONFIG_SOURCE_NAME)) .addConfigSource(new PropertyFileConfigurationSource("elasticapm.properties")) .build(); } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ElasticApmTracerTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ElasticApmTracerTest.java index 8881a4cdf6..da5bb3067f 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ElasticApmTracerTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ElasticApmTracerTest.java @@ -59,7 +59,7 @@ void setUp() { @Test void testThreadLocalStorage() { - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { assertThat(tracerImpl.currentTransaction()).isSameAs(transaction); Span span = tracerImpl.getActive().createSpan(); @@ -77,7 +77,7 @@ void testThreadLocalStorage() { @Test void testNestedSpan() { - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { assertThat(tracerImpl.currentTransaction()).isSameAs(transaction); Span span = tracerImpl.getActive().createSpan(); @@ -102,7 +102,7 @@ void testNestedSpan() { @Test void testDisableStacktraces() { when(tracerImpl.getConfig(StacktraceConfiguration.class).getSpanFramesMinDurationMs()).thenReturn(0L); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { Span span = tracerImpl.getActive().createSpan(); try (Scope spanScope = span.activateInScope()) { @@ -116,7 +116,7 @@ void testDisableStacktraces() { @Test void testEnableStacktraces() throws InterruptedException { when(tracerImpl.getConfig(StacktraceConfiguration.class).getSpanFramesMinDurationMs()).thenReturn(-1L); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { Span span = tracerImpl.getActive().createSpan(); try (Scope spanScope = span.activateInScope()) { @@ -131,7 +131,7 @@ void testEnableStacktraces() throws InterruptedException { @Test void testDisableStacktracesForFastSpans() { when(tracerImpl.getConfig(StacktraceConfiguration.class).getSpanFramesMinDurationMs()).thenReturn(100L); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { Span span = tracerImpl.getActive().createSpan(); try (Scope spanScope = span.activateInScope()) { @@ -146,7 +146,7 @@ void testDisableStacktracesForFastSpans() { @Test void testEnableStacktracesForSlowSpans() throws InterruptedException { when(tracerImpl.getConfig(StacktraceConfiguration.class).getSpanFramesMinDurationMs()).thenReturn(1L); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { Span span = tracerImpl.getActive().createSpan(); try (Scope spanScope = span.activateInScope()) { @@ -160,7 +160,7 @@ void testEnableStacktracesForSlowSpans() throws InterruptedException { @Test void testRecordException() { - tracerImpl.captureException(new Exception("test")); + tracerImpl.captureException(new Exception("test"), getClass().getClassLoader()); assertThat(reporter.getErrors()).hasSize(1); ErrorCapture error = reporter.getFirstError(); assertThat(error.getException()).isNotNull(); @@ -176,7 +176,7 @@ void testRecordExceptionWithTrace() { private void innerRecordExceptionWithTrace(boolean sampled) { reporter.reset(); - Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, ConstantSampler.of(sampled), -1); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, ConstantSampler.of(sampled), -1, null); transaction.withType("test-type"); try (Scope scope = transaction.activateInScope()) { transaction.getContext().getRequest() @@ -208,7 +208,7 @@ private ErrorCapture validateError(AbstractSpan span, boolean sampled, Transacti @Test void testEnableDropSpans() { when(tracerImpl.getConfig(CoreConfiguration.class).getTransactionMaxSpans()).thenReturn(1); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { Span span = tracerImpl.getActive().createSpan(); try (Scope spanScope = span.activateInScope()) { @@ -231,7 +231,7 @@ void testEnableDropSpans() { @Test void testDisable() { when(config.getConfig(CoreConfiguration.class).isActive()).thenReturn(false); - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { assertThat(tracerImpl.currentTransaction()).isSameAs(transaction); assertThat(transaction.isSampled()).isFalse(); @@ -250,7 +250,7 @@ void testDisable() { @Test void testDisableMidTransaction() { - Transaction transaction = tracerImpl.startTransaction(); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { assertThat(tracerImpl.currentTransaction()).isSameAs(transaction); Span span = tracerImpl.getActive().createSpan(); @@ -282,7 +282,7 @@ void testDisableMidTransaction() { @Test void testSamplingNone() throws IOException { config.getConfig(CoreConfiguration.class).getSampleRate().update(0.0, SpyConfiguration.CONFIG_SOURCE_NAME); - Transaction transaction = tracerImpl.startTransaction().withType("request"); + Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, null).withType("request"); try (Scope scope = transaction.activateInScope()) { transaction.setUser("1", "jon.doe@example.com", "jondoe"); Span span = tracerImpl.getActive().createSpan(); @@ -327,7 +327,7 @@ public void stop() { @Test void testTransactionWithParentReference() { final String traceContextHeader = "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01"; - final Transaction transaction = tracerImpl.startTransaction(TraceContext.fromTraceparentHeader(), traceContextHeader, ConstantSampler.of(false), 0); + final Transaction transaction = tracerImpl.startTransaction(TraceContext.fromTraceparentHeader(), traceContextHeader, ConstantSampler.of(false), 0, null); // the traced flag in the header overrides the sampler assertThat(transaction.isSampled()).isTrue(); assertThat(transaction.getTraceContext().getParentId().toString()).isEqualTo("b9c7c989f97918e1"); @@ -339,7 +339,7 @@ void testTransactionWithParentReference() { @Test void testTimestamps() { - final Transaction transaction = tracerImpl.startTransaction(TraceContext.fromTraceparentHeader(), null, ConstantSampler.of(true), 0); + final Transaction transaction = tracerImpl.startTransaction(TraceContext.fromTraceparentHeader(), null, ConstantSampler.of(true), 0, null); final Span span = transaction.createSpan(10); span.end(20); @@ -353,7 +353,7 @@ void testTimestamps() { @Test void testStartSpanAfterTransactionHasEnded() { - final Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null); + final Transaction transaction = tracerImpl.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); final TraceContext transactionTraceContext = transaction.getTraceContext().copy(); transaction.end(); transaction.resetState(); @@ -374,4 +374,26 @@ void testStartSpanAfterTransactionHasEnded() { } assertThat(tracerImpl.getActive()).isNull(); } + + @Test + void testOverrideServiceNameWithoutExplicitServiceName() { + final ElasticApmTracer tracer = new ElasticApmTracerBuilder() + .reporter(reporter) + .build(); + tracer.overrideServiceNameForClassLoader(getClass().getClassLoader(), "overridden"); + tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()).end(); + assertThat(reporter.getFirstTransaction().getTraceContext().getServiceName()).isEqualTo("overridden"); + } + + @Test + void testOverrideServiceNameExplicitServiceName() { + final ElasticApmTracer tracer = new ElasticApmTracerBuilder() + .withConfig("service_name", "explicit-service-name") + .reporter(reporter) + .build(); + + tracer.overrideServiceNameForClassLoader(getClass().getClassLoader(), "overridden"); + tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()).end(); + assertThat(reporter.getFirstTransaction().getTraceContext().getServiceName()).isNull(); + } } diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ScopeManagementTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ScopeManagementTest.java index 5b9ee86c34..fb69b34efd 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ScopeManagementTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/ScopeManagementTest.java @@ -72,7 +72,7 @@ void runTestWithAssertionsDisabled(Runnable test) { @Test void testWrongDeactivationOrder() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); final Span span = transaction.createSpan().activate(); transaction.deactivate(); span.deactivate(); @@ -84,7 +84,7 @@ void testWrongDeactivationOrder() { @Test void testActivateTwice() { runTestWithAssertionsDisabled(() -> { - tracer.startTransaction() + tracer.startTransaction(TraceContext.asRoot(), null, null) .activate().activate() .deactivate().deactivate(); @@ -95,7 +95,7 @@ void testActivateTwice() { @Test void testMissingDeactivation() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.createSpan().activate(); transaction.deactivate(); @@ -106,7 +106,7 @@ void testMissingDeactivation() { @Test void testContextAndSpanRunnableActivation() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.markLifecycleManagingThreadSwitchExpected(); transaction.withActive(transaction.withActive((Runnable) () -> assertThat(tracer.getActive()).isSameAs(transaction))).run(); @@ -119,7 +119,7 @@ void testContextAndSpanRunnableActivation() { @Test void testContextAndSpanCallableActivation() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.markLifecycleManagingThreadSwitchExpected(); try { assertThat(transaction.withActive(transaction.withActive(() -> tracer.currentTransaction())).call()).isSameAs(transaction); @@ -135,7 +135,7 @@ void testContextAndSpanCallableActivation() { @Test void testSpanAndContextRunnableActivation() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); Runnable runnable = transaction.withActive((Runnable) () -> assertThat(tracer.currentTransaction()).isSameAs(transaction)); transaction.markLifecycleManagingThreadSwitchExpected(); @@ -149,7 +149,7 @@ void testSpanAndContextRunnableActivation() { @Test void testSpanAndContextCallableActivation() { runTestWithAssertionsDisabled(() -> { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); Callable callable = transaction.withActive(() -> tracer.currentTransaction()); transaction.markLifecycleManagingThreadSwitchExpected(); try { @@ -165,7 +165,7 @@ void testSpanAndContextCallableActivation() { @Test void testContextAndSpanRunnableActivationInDifferentThread() throws Exception { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.markLifecycleManagingThreadSwitchExpected(); Executors.newSingleThreadExecutor().submit(transaction.withActive(transaction.withActive(() -> { assertThat(tracer.getActive()).isSameAs(transaction); @@ -178,7 +178,7 @@ void testContextAndSpanRunnableActivationInDifferentThread() throws Exception { @Test void testContextAndSpanCallableActivationInDifferentThread() throws Exception { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.markLifecycleManagingThreadSwitchExpected(); Future transactionFuture = Executors.newSingleThreadExecutor().submit(transaction.withActive(transaction.withActive(() -> { assertThat(tracer.getActive()).isSameAs(transaction); @@ -192,7 +192,7 @@ void testContextAndSpanCallableActivationInDifferentThread() throws Exception { @Test void testSpanAndContextRunnableActivationInDifferentThread() throws Exception { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); Runnable runnable = transaction.withActive(() -> { assertThat(tracer.currentTransaction()).isSameAs(transaction); assertThat(tracer.getActive()).isInstanceOf(TraceContext.class); @@ -206,7 +206,7 @@ void testSpanAndContextRunnableActivationInDifferentThread() throws Exception { @Test void testSpanAndContextCallableActivationInDifferentThread() throws Exception { - final Transaction transaction = tracer.startTransaction().activate(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); Callable callable = transaction.withActive(() -> { assertThat(tracer.getActive()).isInstanceOf(TraceContext.class); return tracer.currentTransaction(); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/payload/TransactionPayloadJsonSchemaTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/payload/TransactionPayloadJsonSchemaTest.java index 638a84c0f4..0f82eb75d9 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/payload/TransactionPayloadJsonSchemaTest.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/impl/payload/TransactionPayloadJsonSchemaTest.java @@ -236,7 +236,7 @@ private void validateDbSpanSchema(JsonNode serializedSpans, boolean shouldContai assertThat(tags.get("framework").textValue()).isEqualTo("some-framework"); } else { - assertThat(tags).isNull(); + assertThat(tags).isNullOrEmpty(); } } } diff --git a/apm-agent-core/src/test/java/org/example/stacktrace/StacktraceSerializationTest.java b/apm-agent-core/src/test/java/org/example/stacktrace/StacktraceSerializationTest.java index a25ee74d79..917faecb19 100644 --- a/apm-agent-core/src/test/java/org/example/stacktrace/StacktraceSerializationTest.java +++ b/apm-agent-core/src/test/java/org/example/stacktrace/StacktraceSerializationTest.java @@ -23,6 +23,7 @@ import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.report.serialize.DslJsonSerializer; import com.fasterxml.jackson.databind.JsonNode; @@ -106,7 +107,7 @@ void testNoInternalStackFrames() { } private List getStackTrace() throws IOException { - final Transaction transaction = tracer.startTransaction(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); final Span span = transaction.createSpan(); span.end(); transaction.end(); diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureExceptionInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureExceptionInstrumentation.java index 3807f3ddd1..852a38b8fa 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureExceptionInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureExceptionInstrumentation.java @@ -24,19 +24,15 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import java.util.Collection; -import java.util.Collections; - -import static co.elastic.apm.agent.plugin.api.ElasticApmApiInstrumentation.PUBLIC_API_INSTRUMENTATION_GROUP; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; public class CaptureExceptionInstrumentation extends ApiInstrumentation { @Advice.OnMethodEnter(suppress = Throwable.class) - public static void captureException(@Advice.Argument(0) Throwable t) { + public static void captureException(@Advice.Origin Class clazz, @Advice.Argument(0) Throwable t) { if (tracer != null) { - tracer.captureException(t); + tracer.captureException(t, clazz.getClassLoader()); } } diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureTransactionInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureTransactionInstrumentation.java index 2c32d3ac4f..b59ff76426 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureTransactionInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/CaptureTransactionInstrumentation.java @@ -25,6 +25,7 @@ import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.NamedElement; @@ -54,14 +55,15 @@ public class CaptureTransactionInstrumentation extends ElasticApmInstrumentation private StacktraceConfiguration config; @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onMethodEnter(@SimpleMethodSignature String signature, + public static void onMethodEnter(@Advice.Origin Class clazz, + @SimpleMethodSignature String signature, @AnnotationValueExtractor(annotationClassName = "co.elastic.apm.api.CaptureTransaction", method = "value") String transactionName, @AnnotationValueExtractor(annotationClassName = "co.elastic.apm.api.CaptureTransaction", method = "type") String type, @Advice.Local("transaction") Transaction transaction) { if (tracer != null) { final Object active = tracer.getActive(); if (active == null) { - transaction = tracer.startTransaction() + transaction = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader()) .withName(transactionName.isEmpty() ? signature : transactionName) .withType(type) .activate(); diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/ElasticApmApiInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/ElasticApmApiInstrumentation.java index 23e7171a0f..ecf6fd98dd 100644 --- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/ElasticApmApiInstrumentation.java +++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/plugin/api/ElasticApmApiInstrumentation.java @@ -61,9 +61,9 @@ public StartTransactionInstrumentation() { @VisibleForAdvice @Advice.OnMethodExit(suppress = Throwable.class) - private static void doStartTransaction(@Advice.Return(readOnly = false) Object transaction) { + private static void doStartTransaction(@Advice.Origin Class clazz, @Advice.Return(readOnly = false) Object transaction) { if (tracer != null) { - transaction = tracer.startTransaction(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader()); } } } @@ -76,7 +76,8 @@ public StartTransactionWithRemoteParentInstrumentation() { @VisibleForAdvice @Advice.OnMethodExit(suppress = Throwable.class) - private static void doStartTransaction(@Advice.Return(readOnly = false) Object transaction, + private static void doStartTransaction(@Advice.Origin Class clazz, + @Advice.Return(readOnly = false) Object transaction, @Advice.Argument(0) MethodHandle getFirstHeader, @Advice.Argument(1) @Nullable Object headerExtractor, @Advice.Argument(2) MethodHandle getAllHeaders, @@ -84,17 +85,17 @@ private static void doStartTransaction(@Advice.Return(readOnly = false) Object t if (tracer != null) { if (headerExtractor != null) { final String traceparentHeader = (String) getFirstHeader.invoke(headerExtractor, TraceContext.TRACE_PARENT_HEADER); - transaction = tracer.startTransaction(TraceContext.fromTraceparentHeader(), traceparentHeader); + transaction = tracer.startTransaction(TraceContext.fromTraceparentHeader(), traceparentHeader, clazz.getClassLoader()); } else if (headersExtractor != null) { final Iterable traceparentHeader = (Iterable) getAllHeaders.invoke(headersExtractor, TraceContext.TRACE_PARENT_HEADER); final Iterator iterator = traceparentHeader.iterator(); if (iterator.hasNext()) { - transaction = tracer.startTransaction(TraceContext.fromTraceparentHeader(), iterator.next()); + transaction = tracer.startTransaction(TraceContext.fromTraceparentHeader(), iterator.next(), clazz.getClassLoader()); } else { - transaction = tracer.startTransaction(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader()); } } else { - transaction = tracer.startTransaction(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader()); } } } @@ -135,9 +136,9 @@ public CaptureExceptionInstrumentation() { @VisibleForAdvice @Advice.OnMethodEnter(suppress = Throwable.class) - private static void captureException(@Advice.Argument(0) @Nullable Throwable e) { + private static void captureException(@Advice.Origin Class clazz, @Advice.Argument(0) @Nullable Throwable e) { if (tracer != null) { - tracer.captureException(e); + tracer.captureException(e, clazz.getClassLoader()); } } } diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java index 92019f2b49..9f99c728d6 100644 --- a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java +++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/api/ElasticApmApiInstrumentationTest.java @@ -87,7 +87,7 @@ void testTransactionWithError() { @Test void testCreateChildSpanOfCurrentTransaction() { - final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction().withType("request").withName("transaction").activate(); + final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("transaction").activate(); final Span span = ElasticApm.currentSpan().startSpan("db", "mysql", "query"); span.setName("span"); span.end(); @@ -103,7 +103,7 @@ void testCreateChildSpanOfCurrentTransaction() { @Test void testLegacySpanCreationAndTyping() { - final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction().withType("request").withName("transaction").activate(); + final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("transaction").activate(); final Span span = ElasticApm.currentSpan().createSpan(); span.setName("span"); span.setType("db.mysql.query.etc"); @@ -121,7 +121,7 @@ void testLegacySpanCreationAndTyping() { // https://github.com/elastic/apm-agent-java/issues/132 @Test void testAutomaticAndManualTransactions() { - final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction().withType("request").withName("transaction").activate(); + final co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("transaction").activate(); final Transaction manualTransaction = ElasticApm.startTransaction(); manualTransaction.setName("manual transaction"); manualTransaction.setType("request"); @@ -132,7 +132,7 @@ void testAutomaticAndManualTransactions() { @Test void testGetId_distributedTracingEnabled() { - co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction().withType(Transaction.TYPE_REQUEST); + co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType(Transaction.TYPE_REQUEST); try (Scope scope = transaction.activateInScope()) { assertThat(ElasticApm.currentTransaction().getId()).isEqualTo(transaction.getTraceContext().getId().toString()); assertThat(ElasticApm.currentTransaction().getTraceId()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); @@ -210,7 +210,7 @@ void testScopes() { @Test void testTraceContextScopes() { - co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(); + co.elastic.apm.agent.impl.transaction.Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); tracer.activate(transaction.getTraceContext()); final Span span = ElasticApm.currentSpan(); assertThat(tracer.getActive()).isInstanceOf(TraceContext.class); @@ -291,4 +291,18 @@ void testManualTimestampsDeactivated() { assertThat(reporter.getTransactions()).hasSize(0); assertThat(reporter.getSpans()).hasSize(0); } + + @Test + void testOverrideServiceNameForClassLoader() { + tracer.overrideServiceNameForClassLoader(Transaction.class.getClassLoader(), "overridden"); + ElasticApm.startTransaction().end(); + assertThat(reporter.getFirstTransaction().getTraceContext().getServiceName()).isEqualTo("overridden"); + } + + @Test + void testOverrideServiceNameForClassLoaderWithRemoteParent() { + tracer.overrideServiceNameForClassLoader(Transaction.class.getClassLoader(), "overridden"); + ElasticApm.startTransactionWithRemoteParent(key -> null).end(); + assertThat(reporter.getFirstTransaction().getTraceContext().getServiceName()).isEqualTo("overridden"); + } } diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/es/restclient/v5_6/ElasticsearchRestClientInstrumentationIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/es/restclient/v5_6/ElasticsearchRestClientInstrumentationIT.java index 9a1464517d..dbfc3bf4fe 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/es/restclient/v5_6/ElasticsearchRestClientInstrumentationIT.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-5_6/src/test/java/co/elastic/apm/agent/es/restclient/v5_6/ElasticsearchRestClientInstrumentationIT.java @@ -24,6 +24,7 @@ import co.elastic.apm.agent.impl.transaction.Db; import co.elastic.apm.agent.impl.transaction.Http; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -132,7 +133,7 @@ public static void stopElasticsearchContainerAndClient() throws IOException { @Before public void startTransaction() { - Transaction transaction = tracer.startTransaction().activate(); + Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.setName("ES Transaction"); transaction.withType("request"); transaction.withResult("success"); diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT.java index 16f109a747..82e9cfb2ca 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT.java @@ -24,6 +24,7 @@ import co.elastic.apm.agent.impl.transaction.Db; import co.elastic.apm.agent.impl.transaction.Http; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -133,7 +134,7 @@ public static void stopElasticsearchContainerAndClient() throws IOException { @Before public void startTransaction() { - Transaction transaction = tracer.startTransaction().activate(); + Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.setName("ES Transaction"); transaction.withType("request"); transaction.withResult("success"); diff --git a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT_RealReporter.java b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT_RealReporter.java index c4f1f0c340..9275540d73 100644 --- a/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT_RealReporter.java +++ b/apm-agent-plugins/apm-es-restclient-plugin/apm-es-restclient-plugin-6_4/src/test/java/co/elastic/apm/agent/es/restclient/v6_4/ElasticsearchRestClientInstrumentationIT_RealReporter.java @@ -29,6 +29,7 @@ import co.elastic.apm.agent.impl.payload.Service; import co.elastic.apm.agent.impl.payload.SystemInfo; import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.report.ApmServerReporter; import co.elastic.apm.agent.report.IntakeV2ReportingEventHandler; @@ -36,7 +37,6 @@ import co.elastic.apm.agent.report.ReporterConfiguration; import co.elastic.apm.agent.report.processor.ProcessorEventHandler; import co.elastic.apm.agent.report.serialize.DslJsonSerializer; -import org.testcontainers.elasticsearch.ElasticsearchContainer; import net.bytebuddy.agent.ByteBuddyAgent; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; @@ -69,6 +69,7 @@ import org.junit.Ignore; import org.junit.Test; import org.stagemonitor.configuration.ConfigurationRegistry; +import org.testcontainers.elasticsearch.ElasticsearchContainer; import java.io.IOException; import java.util.HashMap; @@ -158,7 +159,7 @@ public static void stopElasticsearchContainerAndClient() throws IOException { @Before public void startTransaction() { - Transaction transaction = tracer.startTransaction().activate(); + Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.setName("transaction"); transaction.withType("request"); transaction.withResult("success"); diff --git a/apm-agent-plugins/apm-httpclient-core/src/test/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentationTest.java b/apm-agent-plugins/apm-httpclient-core/src/test/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentationTest.java index 06090b8e8c..224cfd2c6c 100644 --- a/apm-agent-plugins/apm-httpclient-core/src/test/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentationTest.java +++ b/apm-agent-plugins/apm-httpclient-core/src/test/java/co/elastic/apm/agent/httpclient/AbstractHttpClientInstrumentationTest.java @@ -58,7 +58,7 @@ public final void setUpWiremock() { .willReturn(seeOther("/"))); wireMockRule.stubFor(get(urlEqualTo("/circular-redirect")) .willReturn(seeOther("/circular-redirect"))); - final Transaction transaction = tracer.startTransaction(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); transaction.withType("request").activate(); } diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java index a876ba417d..7c5ed1d0b5 100644 --- a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java @@ -20,9 +20,8 @@ package co.elastic.apm.agent.concurrent; import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; -import io.netty.util.concurrent.GlobalEventExecutor; -import net.bytebuddy.asm.Advice; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -34,9 +33,6 @@ import java.util.Arrays; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; @@ -58,7 +54,7 @@ public static Iterable> data() { @Before public void setUp() { - transaction = tracer.startTransaction().withName("Transaction").activate(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withName("Transaction").activate(); } @After diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceInstrumentationTest.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceInstrumentationTest.java index d63d1630da..c19de26649 100644 --- a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceInstrumentationTest.java +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceInstrumentationTest.java @@ -20,6 +20,7 @@ package co.elastic.apm.agent.concurrent; import co.elastic.apm.agent.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import io.netty.util.concurrent.GlobalEventExecutor; import org.junit.After; @@ -27,7 +28,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.core.task.SimpleAsyncTaskExecutor; import java.util.Arrays; import java.util.concurrent.ExecutorService; @@ -58,7 +58,7 @@ public static Iterable> data() { @Before public void setUp() { - transaction = tracer.startTransaction().withName("Transaction").activate(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withName("Transaction").activate(); } @After diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java index 2aa43a4041..e7ca8c2504 100644 --- a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java @@ -22,6 +22,7 @@ import co.elastic.apm.agent.AbstractInstrumentationTest; import co.elastic.apm.agent.impl.async.ContextInScopeCallableWrapper; import co.elastic.apm.agent.impl.async.ContextInScopeRunnableWrapper; +import co.elastic.apm.agent.impl.transaction.TraceContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -72,7 +73,7 @@ public ForkJoinTask submit(Runnable task, T result) { throw new UnsupportedOperationException(); } }); - tracer.startTransaction().activate(); + tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); runCounter = new AtomicInteger(); submitWithWrapperCounter = new AtomicInteger(); } diff --git a/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/agent/jaxrs/JaxRsTransactionNameInstrumentationTest.java b/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/agent/jaxrs/JaxRsTransactionNameInstrumentationTest.java index 8e73fe4f40..8d8dc845b1 100644 --- a/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/agent/jaxrs/JaxRsTransactionNameInstrumentationTest.java +++ b/apm-agent-plugins/apm-jaxrs-plugin/src/test/java/co/elastic/apm/agent/jaxrs/JaxRsTransactionNameInstrumentationTest.java @@ -24,6 +24,7 @@ import co.elastic.apm.agent.configuration.SpyConfiguration; import co.elastic.apm.agent.impl.ElasticApmTracer; import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import net.bytebuddy.agent.ByteBuddyAgent; import org.glassfish.jersey.server.ResourceConfig; @@ -117,7 +118,7 @@ protected Application configure() { * @param path the path to make the get request against */ private void doRequest(String path) { - final Transaction request = tracer.startTransaction().withType("request").activate(); + final Transaction request = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").activate(); try { assertThat(getClient().target(getBaseUri()).path(path).request().buildGet().invoke(String.class)).isEqualTo("ok"); } finally { diff --git a/apm-agent-plugins/apm-jaxws-plugin/src/test/java/co/elastic/apm/agent/jaxws/JaxWsTransactionNameInstrumentationTest.java b/apm-agent-plugins/apm-jaxws-plugin/src/test/java/co/elastic/apm/agent/jaxws/JaxWsTransactionNameInstrumentationTest.java index 7d4ec8326f..d3460ceede 100644 --- a/apm-agent-plugins/apm-jaxws-plugin/src/test/java/co/elastic/apm/agent/jaxws/JaxWsTransactionNameInstrumentationTest.java +++ b/apm-agent-plugins/apm-jaxws-plugin/src/test/java/co/elastic/apm/agent/jaxws/JaxWsTransactionNameInstrumentationTest.java @@ -21,6 +21,7 @@ import co.elastic.apm.agent.AbstractInstrumentationTest; import co.elastic.apm.agent.impl.Scope; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +43,7 @@ void setUp() { @Test void testTransactionName() { - final Transaction transaction = tracer.startTransaction(); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, getClass().getClassLoader()); try (Scope scope = transaction.activateInScope()) { helloWorldService.sayHello(); } finally { diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java index efde222496..c72fb879b4 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/agent/jdbc/AbstractJdbcInstrumentationTest.java @@ -22,6 +22,7 @@ import co.elastic.apm.agent.AbstractInstrumentationTest; import co.elastic.apm.agent.impl.transaction.Db; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import org.junit.After; import org.junit.Before; @@ -60,7 +61,7 @@ public abstract class AbstractJdbcInstrumentationTest extends AbstractInstrument this.expectedDbVendor = expectedDbVendor; connection.createStatement().execute("CREATE TABLE ELASTIC_APM (FOO INT, BAR VARCHAR(255))"); connection.createStatement().execute("INSERT INTO ELASTIC_APM (FOO, BAR) VALUES (1, 'APM')"); - transaction = tracer.startTransaction().activate(); + transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).activate(); transaction.setName("transaction"); transaction.withType("request"); transaction.withResult("success"); diff --git a/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracing/impl/ApmSpanBuilderInstrumentation.java b/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracing/impl/ApmSpanBuilderInstrumentation.java index 2347aa7c31..68aa3ef9be 100644 --- a/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracing/impl/ApmSpanBuilderInstrumentation.java +++ b/apm-agent-plugins/apm-opentracing-plugin/src/main/java/co/elastic/apm/agent/opentracing/impl/ApmSpanBuilderInstrumentation.java @@ -84,7 +84,7 @@ public static AbstractSpan doCreateTransactionOrSpan(@Nullable TraceContext p @Nullable Iterable> baggage) { if (tracer != null) { if (parentContext == null) { - return createTransaction(tags, operationName, microseconds, baggage, tracer); + return createTransaction(tags, operationName, microseconds, baggage, tracer, TraceContext.class.getClassLoader()); } else { if (microseconds >= 0) { return tracer.startSpan(TraceContext.fromParent(), parentContext, microseconds); @@ -98,7 +98,7 @@ public static AbstractSpan doCreateTransactionOrSpan(@Nullable TraceContext p @Nonnull private static AbstractSpan createTransaction(Map tags, String operationName, long microseconds, - @Nullable Iterable> baggage, ElasticApmTracer tracer) { + @Nullable Iterable> baggage, ElasticApmTracer tracer, ClassLoader classLoader) { if ("client".equals(tags.get("span.kind"))) { logger.info("Ignoring transaction '{}', as a span.kind client can never be a transaction. " + "Consider creating a span for the whole request.", operationName); @@ -111,7 +111,7 @@ private static AbstractSpan createTransaction(Map tags, Strin } else { sampler = tracer.getSampler(); } - return tracer.startTransaction(TraceContext.fromTraceparentHeader(), getTraceContextHeader(baggage), sampler, microseconds); + return tracer.startTransaction(TraceContext.fromTraceparentHeader(), getTraceContextHeader(baggage), sampler, microseconds, classLoader); } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java index c08e2a2fd9..ecb856069a 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletApiAdvice.java @@ -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)); diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletContextServiceNameInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletContextServiceNameInstrumentation.java new file mode 100644 index 0000000000..b907fb3fa3 --- /dev/null +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletContextServiceNameInstrumentation.java @@ -0,0 +1,103 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2019 Elastic and contributors + * %% + * 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. + * #L% + */ +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.Arrays; +import java.util.Collection; + +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 getTypeMatcherPreFilter() { + return nameContains("Servlet").or(nameContainsIgnoreCase("jsp")); + } + + @Override + public ElementMatcher getTypeMatcher() { + return not(isInterface()) + .and(hasSuperType(named("javax.servlet.Servlet"))); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("init") + .and(takesArgument(0, named("javax.servlet.ServletConfig"))) + .and(takesArguments(1)); + } + + @Override + public Class getAdviceClass() { + return ServletContextServiceNameAdvice.class; + } + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList(SERVLET_API, "servlet-service-name"); + } + + public static class ServletContextServiceNameAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onServletInit(@Nullable @Advice.Argument(0) ServletConfig servletConfig) { + if (tracer == null || servletConfig == null) { + return; + } + ServletContext servletContext = servletConfig.getServletContext(); + if (servletContext == null) { + return; + } + @Nullable + String serviceName = servletContext.getServletContextName(); + if ("application".equals(serviceName) || "".equals(serviceName)) { + // payara returns an empty string as opposed to null + // spring applications which did not set spring.application.name have application as the default + // this is a worse default than the one we would otherwise choose + serviceName = null; + } + final String contextPath = servletContext.getContextPath(); + if (serviceName == null && contextPath != null && !contextPath.isEmpty()) { + // remove leading slash + serviceName = contextPath.substring(1); + } + if (serviceName != null) { + tracer.overrideServiceNameForClassLoader(servletContext.getClassLoader(), serviceName); + } + } + } +} diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java index 5283c7cbc9..b8bf77b0e9 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletInstrumentation.java @@ -57,13 +57,13 @@ public void init(ElasticApmTracer tracer) { @Override public ElementMatcher getTypeMatcherPreFilter() { - return nameContains("Servlet"); + return nameContains("Servlet").or(nameContainsIgnoreCase("jsp")); } @Override public ElementMatcher getTypeMatcher() { return not(isInterface()) - .and(hasSuperType(named("javax.servlet.Servlet")).or(nameContainsIgnoreCase("jsp"))); + .and(hasSuperType(named("javax.servlet.Servlet"))); } @Override diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java index 13ab5315c6..c731d02576 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletTransactionHelper.java @@ -95,14 +95,14 @@ public ServletTransactionHelper(ElasticApmTracer tracer) { */ @Nullable @VisibleForAdvice - public Transaction onBefore(String servletPath, @Nullable String pathInfo, + public Transaction onBefore(ClassLoader classLoader, String servletPath, @Nullable String pathInfo, @Nullable String userAgentHeader, @Nullable String traceContextHeader) { if (coreConfiguration.isActive() && // only create a transaction if there is not already one tracer.currentTransaction() == null && !isExcluded(servletPath, pathInfo, userAgentHeader)) { - return tracer.startTransaction(TraceContext.fromTraceparentHeader(), traceContextHeader).activate(); + return tracer.startTransaction(TraceContext.fromTraceparentHeader(), traceContextHeader, classLoader).activate(); } else { return null; } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-servlet-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation index 463a6c9640..661ea5b24b 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation @@ -2,3 +2,4 @@ co.elastic.apm.agent.servlet.ServletInstrumentation co.elastic.apm.agent.servlet.FilterChainInstrumentation co.elastic.apm.agent.servlet.AsyncInstrumentation$StartAsyncInstrumentation co.elastic.apm.agent.servlet.AsyncInstrumentation$AsyncContextInstrumentation +co.elastic.apm.agent.servlet.ServletContextServiceNameInstrumentation diff --git a/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/ServletInstrumentationTest.java b/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/ServletInstrumentationTest.java index dc24983c48..61b9e71c1e 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/ServletInstrumentationTest.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/test/java/co/elastic/apm/agent/servlet/ServletInstrumentationTest.java @@ -21,17 +21,19 @@ import co.elastic.apm.agent.AbstractServletTest; import co.elastic.apm.agent.bci.ElasticApmAgent; -import co.elastic.apm.agent.bci.ElasticApmInstrumentation; +import co.elastic.apm.agent.configuration.CoreConfiguration; import co.elastic.apm.agent.configuration.SpyConfiguration; import co.elastic.apm.agent.impl.ElasticApmTracerBuilder; +import co.elastic.apm.agent.matcher.WildcardMatcher; +import co.elastic.apm.agent.web.WebConfiguration; import net.bytebuddy.agent.ByteBuddyAgent; -import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.matcher.ElementMatcher; import okhttp3.Response; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.stagemonitor.configuration.ConfigurationRegistry; import javax.servlet.DispatcherType; import javax.servlet.Filter; @@ -46,78 +48,91 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; +import java.util.List; -import static net.bytebuddy.matcher.ElementMatchers.none; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; class ServletInstrumentationTest extends AbstractServletTest { - @AfterEach - final void afterEach() { + private static ConfigurationRegistry config; + + @BeforeAll + static void initInstrumentation() { + config = SpyConfiguration.createSpyConfig(); + when(config.getConfig(WebConfiguration.class).getIgnoreUrls()).thenReturn(List.of(WildcardMatcher.valueOf("/init"))); + ElasticApmAgent.initInstrumentation(new ElasticApmTracerBuilder() + .configurationRegistry(config) + .reporter(reporter) + .build(), ByteBuddyAgent.install()); + } + + @AfterAll + static void afterAll() { ElasticApmAgent.reset(); } + @BeforeEach + void setUp() { + SpyConfiguration.reset(config); + } + @Override protected void setUpHandler(ServletContextHandler handler) { + handler.setDisplayName(getClass().getSimpleName()); + handler.setClassLoader(getClass().getClassLoader()); + handler.addServlet(TestServlet.class, "/filter/test"); handler.addServlet(TestServlet.class, "/test"); handler.addServlet(BaseTestServlet.class, "/base"); handler.addServlet(ForwardingServlet.class, "/forward"); handler.addServlet(IncludingServlet.class, "/include"); - handler.addFilter(TestFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + handler.addFilter(TestFilter.class, "/filter/*", EnumSet.of(DispatcherType.REQUEST)); } @Test void testServletInstrumentation() throws Exception { - testInstrumentation(new ServletInstrumentation(), 1, "/test"); + callServlet(1, "/test"); } @Test void testBaseServletInstrumentation() throws Exception { - testInstrumentation(new ServletInstrumentation(), 1, "/base"); + callServlet(1, "/base"); } @Test void testFilterChainInstrumentation() throws Exception { - testInstrumentation(new FilterChainInstrumentation(), 1, "/test"); + callServlet(1, "/filter/test"); } @Test void testNoopInstrumentation() throws Exception { - testInstrumentation(new NoopInstrumentation(), 0, "/test"); + when(config.getConfig(CoreConfiguration.class).isActive()).thenReturn(false); + callServlet(0, "/test"); } @Test void testForward() throws Exception { - testInstrumentation(new ServletInstrumentation(), 1, "/forward"); + callServlet(1, "/forward"); } @Test void testInclude() throws Exception { - testInstrumentation(new ServletInstrumentation(), 1, "/include"); + callServlet(1, "/include"); } - private void testInstrumentation(ElasticApmInstrumentation instrumentation, int expectedTransactions, String path) throws IOException, InterruptedException { - initInstrumentation(instrumentation); - + private void callServlet(int expectedTransactions, String path) throws IOException, InterruptedException { final Response response = get(path); assertThat(response.code()).isEqualTo(200); assertThat(response.body().string()).isEqualTo("Hello World!"); if (expectedTransactions > 0) { reporter.getFirstTransaction(500); + assertThat(reporter.getTransactions().stream().map(transaction -> transaction.getTraceContext().getServiceName()).distinct()).containsExactly(getClass().getSimpleName()); } assertThat(reporter.getTransactions()).hasSize(expectedTransactions); } - private void initInstrumentation(ElasticApmInstrumentation instrumentation) { - ElasticApmAgent.initInstrumentation(new ElasticApmTracerBuilder() - .configurationRegistry(SpyConfiguration.createSpyConfig()) - .reporter(reporter) - .build(), ByteBuddyAgent.install(), Collections.singleton(instrumentation)); - } public static class TestServlet extends HttpServlet { @Override @@ -185,21 +200,4 @@ public void destroy() { } } - - private static class NoopInstrumentation extends ElasticApmInstrumentation { - @Override - public ElementMatcher getTypeMatcher() { - return none(); - } - - @Override - public ElementMatcher getMethodMatcher() { - return none(); - } - - @Override - public Collection getInstrumentationGroupNames() { - return Collections.singleton("noop"); - } - } } diff --git a/apm-agent-plugins/apm-slf4j-plugin/src/test/java/co/elastic/apm/agent/slf4j/Slf4JMdcActivationListenerTest.java b/apm-agent-plugins/apm-slf4j-plugin/src/test/java/co/elastic/apm/agent/slf4j/Slf4JMdcActivationListenerTest.java index 5d09ad6851..2cff1ef4ea 100644 --- a/apm-agent-plugins/apm-slf4j-plugin/src/test/java/co/elastic/apm/agent/slf4j/Slf4JMdcActivationListenerTest.java +++ b/apm-agent-plugins/apm-slf4j-plugin/src/test/java/co/elastic/apm/agent/slf4j/Slf4JMdcActivationListenerTest.java @@ -24,6 +24,7 @@ import co.elastic.apm.agent.impl.Scope; import co.elastic.apm.agent.impl.transaction.AbstractSpan; import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.logging.LoggingConfiguration; import org.junit.jupiter.api.BeforeEach; @@ -49,7 +50,7 @@ void setUp() { @Test void testMdcIntegration() { when(loggingConfiguration.isLogCorrelationEnabled()).thenReturn(true); - Transaction transaction = tracer.startTransaction().withType("request").withName("test"); + Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("test"); assertMdcIsEmpty(); try (Scope scope = transaction.activateInScope()) { assertMdcIsSet(transaction); @@ -71,7 +72,7 @@ void testMdcIntegration() { @Test void testMdcIntegrationTransactionScopeInDifferentThread() throws Exception { when(loggingConfiguration.isLogCorrelationEnabled()).thenReturn(true); - Transaction transaction = tracer.startTransaction().withType("request").withName("test"); + Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("test"); assertMdcIsEmpty(); Thread thread = new Thread(() -> { assertMdcIsEmpty(); @@ -89,7 +90,7 @@ void testMdcIntegrationTransactionScopeInDifferentThread() throws Exception { @Test void testMdcIntegrationContextScopeInDifferentThread() throws Exception { when(loggingConfiguration.isLogCorrelationEnabled()).thenReturn(true); - final Transaction transaction = tracer.startTransaction().withType("request").withName("test"); + final Transaction transaction = tracer.startTransaction(TraceContext.asRoot(), null, null).withType("request").withName("test"); assertMdcIsEmpty(); try (Scope scope = transaction.activateInScope()) { assertMdcIsSet(transaction); diff --git a/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/spring/webmvc/SpringServiceNameInstrumentation.java b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/spring/webmvc/SpringServiceNameInstrumentation.java new file mode 100644 index 0000000000..73775ba337 --- /dev/null +++ b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/spring/webmvc/SpringServiceNameInstrumentation.java @@ -0,0 +1,74 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2019 Elastic and contributors + * %% + * 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. + * #L% + */ +package co.elastic.apm.agent.spring.webmvc; + +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 org.springframework.web.context.WebApplicationContext; + +import java.util.Collection; +import java.util.Collections; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.nameEndsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class SpringServiceNameInstrumentation extends ElasticApmInstrumentation { + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameEndsWith("ApplicationContext"); + } + + @Override + public ElementMatcher getTypeMatcher() { + return hasSuperType(named("org.springframework.web.context.WebApplicationContext")); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("initPropertySources").and(takesArguments(0)); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Collections.singletonList("spring-service-name"); + } + + @Override + public Class getAdviceClass() { + return SpringServiceNameAdvice.class; + } + + public static class SpringServiceNameAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void afterInitPropertySources(@Advice.This WebApplicationContext applicationContext) { + if (tracer != null && applicationContext.getServletContext() != null) { + tracer.overrideServiceNameForClassLoader(applicationContext.getServletContext().getClassLoader(), applicationContext.getEnvironment().getProperty("spring.application.name")); + } + } + } +} diff --git a/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation index af09e25cd8..df32105161 100644 --- a/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation +++ b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation @@ -1,2 +1,3 @@ co.elastic.apm.agent.spring.webmvc.SpringTransactionNameInstrumentation co.elastic.apm.agent.spring.webmvc.DispatcherServletRenderInstrumentation +co.elastic.apm.agent.spring.webmvc.SpringServiceNameInstrumentation diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 07d303c8a0..4a348c636a 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -153,13 +153,32 @@ NOTE: Both active and instrument needs to be true for instrumentation to be runn This is used to keep all the errors and transactions of your service together and is the primary filter in the Elastic APM user interface. -NOTE: The service name must conform to this regular expression: ^[a-zA-Z0-9 _-]+$. In less regexy terms: Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces. +The service name must conform to this regular expression: `^[a-zA-Z0-9 _-]+$`. +In less regexy terms: +Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces. + +NOTE: When relying on auto-discovery of the service name in Servlet environments (including Spring Boot), +there is currently a caveat related to metrics. +The consequence is that the 'Metrics' tab of a service does not show process-global metrics like CPU utilization. +The reason is that metrics are reported with the detected default service name for the JVM, +for example `tomcat-application`. +That is because there may be multiple web applications deployed to a single JVM/servlet container. +However, you can view those metrics by selecting the `tomcat-application` service name, for example. +Future versions of the Elastic APM stack will have better support for that scenario. +A workaround is to explicitly set the `service_name` which means all applications deployed to the same servlet container will have the same name +or to disable the corresponding `*-service-name` detecting instrumentations via <>. [options="header"] |============ | Default | Type | Dynamic -| Name of main class or jar | String | false +| For Spring-based application, uses the `spring.application.name` property, if set. +For Servlet-based applications, uses the `display-name` of the `web.xml`, if available. +Falls back to the servlet context path the application is mapped to (unless mapped to the root context). +Note: the above service name auto discovery mechanisms require APM Server 7.0+. +Falls back to the name of the main class or jar file. +If the service name is set explicitly, it overrides all of the above. + | String | false |============ @@ -300,7 +319,7 @@ you should add an additional entry to this list (make sure to also include the d ==== `disable_instrumentations` A list of instrumentations which should be disabled. -Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`, `urlconnection`. +Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`. If you want to try out incubating features, set the value to an empty string. @@ -1054,13 +1073,38 @@ The default unit for this option is `ms` # This is used to keep all the errors and transactions of your service together # and is the primary filter in the Elastic APM user interface. # -# NOTE: The service name must conform to this regular expression: ^[a-zA-Z0-9 _-]+$. In less regexy terms: Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces. +# The service name must conform to this regular expression: `^[a-zA-Z0-9 _-]+$`. +# In less regexy terms: +# Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces. +# +# NOTE: When relying on auto-discovery of the service name in Servlet environments (including Spring Boot), +# there is currently a caveat related to metrics. +# The consequence is that the 'Metrics' tab of a service does not show process-global metrics like CPU utilization. +# The reason is that metrics are reported with the detected default service name for the JVM, +# for example `tomcat-application`. +# That is because there may be multiple web applications deployed to a single JVM/servlet container. +# However, you can view those metrics by selecting the `tomcat-application` service name, for example. +# Future versions of the Elastic APM stack will have better support for that scenario. +# A workaround is to explicitly set the `service_name` which means all applications deployed to the same servlet container will have the same name +# or to disable the corresponding `*-service-name` detecting instrumentations via <>. # # This setting can not be changed at runtime. Changes require a restart of the application. # Type: String -# Default value: Name of main class or jar +# Default value: For Spring-based application, uses the `spring.application.name` property, if set. +For Servlet-based applications, uses the `display-name` of the `web.xml`, if available. +Falls back to the servlet context path the application is mapped to (unless mapped to the root context). +Note: the above service name auto discovery mechanisms require APM Server 7.0+. +Falls back to the name of the main class or jar file. +If the service name is set explicitly, it overrides all of the above. + # -# service_name=Name of main class or jar +# service_name=For Spring-based application, uses the `spring.application.name` property, if set. +For Servlet-based applications, uses the `display-name` of the `web.xml`, if available. +Falls back to the servlet context path the application is mapped to (unless mapped to the root context). +Note: the above service name auto discovery mechanisms require APM Server 7.0+. +Falls back to the name of the main class or jar file. +If the service name is set explicitly, it overrides all of the above. + # A version string for the currently deployed version of the service. If you don’t version your deployments, the recommended value for this field is the commit identifier of the deployed revision, e.g. the output of git rev-parse HEAD. # @@ -1129,7 +1173,7 @@ The default unit for this option is `ms` # sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,authorization,set-cookie # A list of instrumentations which should be disabled. -# Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`, `urlconnection`. +# Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-ws`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `servlet-service-name`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `urlconnection`. # If you want to try out incubating features, # set the value to an empty string. # diff --git a/elastic-apm-agent/src/test/resources/configuration.asciidoc.ftl b/elastic-apm-agent/src/test/resources/configuration.asciidoc.ftl index 2e554379b1..13b435d82b 100644 --- a/elastic-apm-agent/src/test/resources/configuration.asciidoc.ftl +++ b/elastic-apm-agent/src/test/resources/configuration.asciidoc.ftl @@ -48,6 +48,14 @@ ELASTIC_APM_SERVICE_NAME=my-cool-service ELASTIC_APM_APPLICATION_PACKAGES=org.example,org.another.example ELASTIC_APM_SERVER_URLS=http://localhost:8200 ---- +<#assign defaultServiceName> +For Spring-based application, uses the `spring.application.name` property, if set. +For Servlet-based applications, uses the `display-name` of the `web.xml`, if available. +Falls back to the servlet context path the application is mapped to (unless mapped to the root context). +Note: the above service name auto discovery mechanisms require APM Server 7.0+. +Falls back to the name of the main class or jar file. +If the service name is set explicitly, it overrides all of the above. + [float] === Option reference @@ -84,7 +92,7 @@ Valid options: <#list option.validOptionsLabelMap?values as validOption>`${valid [options="header"] |============ | Default | Type | Dynamic -| <#if option.key?matches("service_name")>Name of main class or jar<#else>`<@defaultValue option/>` | ${option.valueType} | ${option.dynamic?c} +| <#if option.key?matches("service_name")>${defaultServiceName}<#else>`<@defaultValue option/>` | ${option.valueType} | ${option.dynamic?c} |============ @@ -128,9 +136,9 @@ Valid options: <#list option.validOptionsLabelMap?values as validOption>`${valid # Supports the duration suffixes ms, s and m. Example: ${option.defaultValueAsString}. # The default unit for this option is ${option.valueConverter.defaultDurationSuffix}. -# Default value: ${option.key?matches("service_name")?then("Name of main class or jar", option.defaultValueAsString!)} +# Default value: ${option.key?matches("service_name")?then(defaultServiceName, option.defaultValueAsString!)} # -# ${option.key}=${option.key?matches("service_name")?then("Name of main class or jar", option.defaultValueAsString!)} +# ${option.key}=${option.key?matches("service_name")?then(defaultServiceName, option.defaultValueAsString!)} diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java index 2d3a5823c0..11db400bed 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java @@ -101,6 +101,7 @@ public abstract class AbstractServletContainerIntegrationTest { private final String expectedDefaultServiceName; @Nullable private GenericContainer debugProxy; + private TestApp currentTestApp; protected AbstractServletContainerIntegrationTest(GenericContainer servletContainer, String expectedDefaultServiceName, String deploymentPath, String containerName) { this(servletContainer, 8080, expectedDefaultServiceName, deploymentPath, containerName); @@ -207,6 +208,7 @@ protected Iterable> getTestClasses() { @Test public void testAllScenarios() throws Exception { for (TestApp testApp : getTestApps()) { + this.currentTestApp = testApp; waitFor(testApp.getStatusEndpoint()); clearMockServerLog(); testApp.test(this); @@ -340,39 +342,59 @@ public List getReportedErrors() { private List getEvents(String eventType) { try { - final List transactions = new ArrayList<>(); + final List events = new ArrayList<>(); final ObjectMapper objectMapper = new ObjectMapper(); for (HttpRequest httpRequest : mockServerContainer.getClient().retrieveRecordedRequests(request(INTAKE_V2_URL))) { - for (String ndJsonLine : httpRequest.getBodyAsString().split("\n")) { + final String bodyAsString = httpRequest.getBodyAsString(); + validateEventMetadata(bodyAsString); + for (String ndJsonLine : bodyAsString.split("\n")) { final JsonNode ndJson = objectMapper.readTree(ndJsonLine); if (ndJson.get(eventType) != null) { - transactions.add(ndJson.get(eventType)); + events.add(ndJson.get(eventType)); } } } - return transactions; + return events; } catch (IOException e) { throw new RuntimeException(e); } } - public void validateMetadata() { + private void validateEventMetadata(String bodyAsString) { try { final ObjectMapper objectMapper = new ObjectMapper(); - final JsonNode payload; - payload = objectMapper - .readTree(mockServerContainer.getClient() - .retrieveRecordedRequests(request(INTAKE_V2_URL))[0].getBodyAsString().split("\n")[0]); - JsonNode metadata = payload.get("metadata"); - assertThat(metadata.get("service").get("name").textValue()).isEqualTo(expectedDefaultServiceName); - JsonNode container = metadata.get("system").get("container"); - assertThat(container).isNotNull(); - assertThat(container.get("id").textValue()).isEqualTo(servletContainer.getContainerId()); + for (String line : bodyAsString.split("\n")) { + final JsonNode event = objectMapper.readTree(line); + final JsonNode metadata = event.get("metadata"); + if (metadata != null) { + validataMetadataEvent(metadata); + } else { + validateServiceName(event.get("error")); + validateServiceName(event.get("span")); + validateServiceName(event.get("transaction")); + } + } } catch (IOException e) { throw new RuntimeException(e); } } + private void validateServiceName(JsonNode event) { + if (currentTestApp.getExpectedServiceName() != null && event != null) { + assertThat(event.get("context").get("service")) + .withFailMessage("No service name set. Expected '%s'. Event was %s", currentTestApp.getExpectedServiceName(), event) + .isNotNull(); + assertThat(event.get("context").get("service").get("name").textValue()).isEqualTo(currentTestApp.getExpectedServiceName()); + } + } + + private void validataMetadataEvent(JsonNode metadata) { + assertThat(metadata.get("service").get("name").textValue()).isEqualTo(expectedDefaultServiceName); + JsonNode container = metadata.get("system").get("container"); + assertThat(container).isNotNull(); + assertThat(container.get("id").textValue()).isEqualTo(servletContainer.getContainerId()); + } + private void addSpans(List spans, JsonNode payload) { final JsonNode jsonSpans = payload.get("spans"); if (jsonSpans != null) { diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfApplicationServerTestApp.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfApplicationServerTestApp.java index 72a6a0dcd0..1d4550b4db 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfApplicationServerTestApp.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfApplicationServerTestApp.java @@ -31,7 +31,7 @@ public class JsfApplicationServerTestApp extends TestApp { public JsfApplicationServerTestApp() { - super("../jsf-app/jsf-app-dependent", "jsf-http-get.war", "/jsf-http-get/status.html"); + super("../jsf-app/jsf-app-dependent", "jsf-http-get.war", "/jsf-http-get/status.html", "jsf-http-get"); } @Override diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfServletContainerTestApp.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfServletContainerTestApp.java index c4a350dab7..2b946a4605 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfServletContainerTestApp.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/JsfServletContainerTestApp.java @@ -23,7 +23,7 @@ public class JsfServletContainerTestApp extends TestApp { public JsfServletContainerTestApp() { - super("../jsf-app/jsf-app-standalone", "jsf-http-get.war", "/jsf-http-get/status.html"); + super("../jsf-app/jsf-app-standalone", "jsf-http-get.war", "/jsf-http-get/status.html", "jsf-http-get"); } @Override diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/ServletApiTestApp.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/ServletApiTestApp.java index ab20a5a714..6ce4fc203f 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/ServletApiTestApp.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/ServletApiTestApp.java @@ -30,7 +30,7 @@ public class ServletApiTestApp extends TestApp { public ServletApiTestApp() { - super("../simple-webapp", "simple-webapp.war", "/simple-webapp/status.jsp"); + super("../simple-webapp", "simple-webapp.war", "/simple-webapp/status.jsp", "Simple Web App"); } @Override @@ -83,7 +83,6 @@ private void testTransactionReporting(AbstractServletContainerIntegrationTest te for (JsonNode span : spans) { assertThat(span.get("type").textValue()).isEqualTo("db.h2.query"); } - test.validateMetadata(); } } diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/SoapTestApp.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/SoapTestApp.java index 312f2d0fdc..b73fd3fd22 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/SoapTestApp.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/SoapTestApp.java @@ -31,7 +31,7 @@ public class SoapTestApp extends TestApp { public SoapTestApp() { - super("../soap-test", "soap-test.war", "/soap-test/status.html"); + super("../soap-test", "soap-test.war", "/soap-test/status.html", "soap-test"); } @Override diff --git a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/TestApp.java b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/TestApp.java index a0904bccd2..43e753c0b1 100644 --- a/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/TestApp.java +++ b/integration-tests/application-server-integration-tests/src/test/java/co/elastic/apm/servlet/tests/TestApp.java @@ -21,16 +21,21 @@ import co.elastic.apm.servlet.AbstractServletContainerIntegrationTest; +import javax.annotation.Nullable; + public abstract class TestApp { private final String modulePath; private final String appFileName; private final String statusEndpoint; + @Nullable + private final String expectedServiceName; - TestApp(String modulePath, String appFileName, String statusEndpoint) { + TestApp(String modulePath, String appFileName, String statusEndpoint, @Nullable String expectedServiceName) { this.modulePath = modulePath; this.appFileName = appFileName; this.statusEndpoint = statusEndpoint; + this.expectedServiceName = expectedServiceName; } public String getAppFilePath() { @@ -45,6 +50,11 @@ public String getStatusEndpoint() { return statusEndpoint; } + @Nullable + public String getExpectedServiceName() { + return expectedServiceName; + } + public abstract void test(AbstractServletContainerIntegrationTest test) throws Exception; } diff --git a/integration-tests/spring-boot-2/spring-boot-2-base/src/test/java/co/elastic/apm/spring/boot/AbstractSpringBootTest.java b/integration-tests/spring-boot-2/spring-boot-2-base/src/test/java/co/elastic/apm/spring/boot/AbstractSpringBootTest.java index e6d1d2897d..0451e37d00 100644 --- a/integration-tests/spring-boot-2/spring-boot-2-base/src/test/java/co/elastic/apm/spring/boot/AbstractSpringBootTest.java +++ b/integration-tests/spring-boot-2/spring-boot-2-base/src/test/java/co/elastic/apm/spring/boot/AbstractSpringBootTest.java @@ -20,7 +20,6 @@ package co.elastic.apm.spring.boot; import co.elastic.apm.agent.MockReporter; -import co.elastic.apm.api.ElasticApm; import co.elastic.apm.agent.bci.ElasticApmAgent; import co.elastic.apm.agent.configuration.SpyConfiguration; import co.elastic.apm.agent.impl.ElasticApmTracer; @@ -28,9 +27,12 @@ import co.elastic.apm.agent.impl.transaction.Transaction; import co.elastic.apm.agent.report.ReporterConfiguration; import co.elastic.apm.agent.web.WebConfiguration; +import co.elastic.apm.api.ElasticApm; import net.bytebuddy.agent.ByteBuddyAgent; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.SpringApplication; @@ -62,22 +64,26 @@ @SpringBootTest(webEnvironment = RANDOM_PORT) public abstract class AbstractSpringBootTest { - private MockReporter reporter; - private ConfigurationRegistry config; + private static MockReporter reporter; + private static ConfigurationRegistry config; @LocalServerPort private int port; private TestRestTemplate restTemplate; - @Before - public void setUp() { + @BeforeClass + public static void beforeClass() { config = SpyConfiguration.createSpyConfig(); - when(config.getConfig(ReporterConfiguration.class).isReportSynchronously()).thenReturn(true); reporter = new MockReporter(); ElasticApmTracer tracer = new ElasticApmTracerBuilder() .configurationRegistry(config) .reporter(reporter) .build(); ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install()); + } + + @Before + public void setUp() { + when(config.getConfig(ReporterConfiguration.class).isReportSynchronously()).thenReturn(true); restTemplate = new TestRestTemplate(new RestTemplateBuilder() .setConnectTimeout(0) .setReadTimeout(0) @@ -85,8 +91,8 @@ public void setUp() { reporter.reset(); } - @After - public void tearDown() { + @AfterClass + public static void tearDown() { ElasticApmAgent.reset(); } @@ -102,6 +108,7 @@ public void greetingShouldReturnDefaultMessage() throws Exception { assertThat(transaction.getContext().getUser().getId()).isEqualTo("id"); assertThat(transaction.getContext().getUser().getEmail()).isEqualTo("email"); assertThat(transaction.getContext().getUser().getUsername()).isEqualTo("username"); + assertThat(transaction.getTraceContext().getServiceName()).isEqualTo("spring-boot-test"); } @Test diff --git a/integration-tests/spring-boot-2/spring-boot-2-base/src/test/resources/application.properties b/integration-tests/spring-boot-2/spring-boot-2-base/src/test/resources/application.properties new file mode 100644 index 0000000000..d11e76254d --- /dev/null +++ b/integration-tests/spring-boot-2/spring-boot-2-base/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=spring-boot-test