Skip to content

Commit 9379f5f

Browse files
authored
Use display-name or context path as the default service name (#514)
Enables to have multiple service names per JVM, one per class loader. closes #136
1 parent 700b9e6 commit 9379f5f

File tree

48 files changed

+658
-226
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+658
-226
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# next (1.5.0)
22

3+
## Potentially breaking changes
4+
* 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)
5+
previously and you are dealing with a servlet-based application (including Spring Boot),
6+
your `service_name` will change.
7+
See the documentation for [`service_name`](https://www.elastic.co/guide/en/apm/agent/java/master/config-core.html#config-service-name)
8+
and the corresponding section in _Features_ for more information.
9+
Note: this requires APM Server 7.0+. If using previous versions, nothing will change.
10+
311
## Features
412
* Support for number and boolean labels in the public API (#497).
513
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 @@
917
* Support async queries made by Elasticsearch REST client
1018
* Added `setStartTimestamp(long epochMicros)` and `end(long epochMicros)` API methods to `Span` and `Transaction`,
1119
allowing to set custom start and end timestamps.
20+
* Auto-detection of the `service_name` based on the `<display-name>` element of the `web.xml` with a fallback to the servlet context path.
21+
If you are using a spring-based application, the agent will use the setting for `spring.application.name` for its `service_name`.
22+
See the documentation for [`service_name`](https://www.elastic.co/guide/en/apm/agent/java/master/config-core.html#config-service-name)
23+
for more information.
24+
Note: this requires APM Server 7.0+. If using previous versions, nothing will change.
1225

1326
## Bug Fixes
1427

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/methodmatching/TraceMethodInstrumentation.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import co.elastic.apm.agent.bci.bytebuddy.SimpleMethodSignatureOffsetMappingFactory;
2424
import co.elastic.apm.agent.configuration.CoreConfiguration;
2525
import co.elastic.apm.agent.impl.transaction.AbstractSpan;
26+
import co.elastic.apm.agent.impl.transaction.TraceContext;
2627
import co.elastic.apm.agent.impl.transaction.TraceContextHolder;
2728
import co.elastic.apm.agent.matcher.WildcardMatcher;
2829
import net.bytebuddy.asm.Advice;
@@ -58,12 +59,13 @@ public TraceMethodInstrumentation(MethodMatcher methodMatcher) {
5859
}
5960

6061
@Advice.OnMethodEnter(suppress = Throwable.class)
61-
public static void onMethodEnter(@SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature,
62+
public static void onMethodEnter(@Advice.Origin Class<?> clazz,
63+
@SimpleMethodSignatureOffsetMappingFactory.SimpleMethodSignature String signature,
6264
@Advice.Local("span") AbstractSpan<?> span) {
6365
if (tracer != null) {
6466
final TraceContextHolder<?> parent = tracer.getActive();
6567
if (parent == null) {
66-
span = tracer.startTransaction()
68+
span = tracer.startTransaction(TraceContext.asRoot(), null, clazz.getClassLoader())
6769
.withName(signature)
6870
.activate();
6971
} else {

apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,20 @@ public class CoreConfiguration extends ConfigurationOptionProvider {
7373
.description("This is used to keep all the errors and transactions of your service together\n" +
7474
"and is the primary filter in the Elastic APM user interface.\n" +
7575
"\n" +
76-
"NOTE: The service name must conform to this regular expression: ^[a-zA-Z0-9 _-]+$. In less regexy terms: Your service name " +
77-
"must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces.")
76+
"The service name must conform to this regular expression: `^[a-zA-Z0-9 _-]+$`.\n" +
77+
"In less regexy terms:\n" +
78+
"Your service name must only contain characters from the ASCII alphabet, numbers, dashes, underscores and spaces.\n" +
79+
"\n" +
80+
"NOTE: When relying on auto-discovery of the service name in Servlet environments (including Spring Boot),\n" +
81+
"there is currently a caveat related to metrics.\n" +
82+
"The consequence is that the 'Metrics' tab of a service does not show process-global metrics like CPU utilization.\n" +
83+
"The reason is that metrics are reported with the detected default service name for the JVM,\n" +
84+
"for example `tomcat-application`.\n" +
85+
"That is because there may be multiple web applications deployed to a single JVM/servlet container.\n" +
86+
"However, you can view those metrics by selecting the `tomcat-application` service name, for example.\n" +
87+
"Future versions of the Elastic APM stack will have better support for that scenario.\n" +
88+
"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" +
89+
"or to disable the corresponding `*-service-name` detecting instrumentations via <<config-disable-instrumentations>>.")
7890
.addValidator(RegexValidator.of("^[a-zA-Z0-9 _-]+$", "Your service name \"{0}\" must only contain characters " +
7991
"from the ASCII alphabet, numbers, dashes, underscores and spaces"))
8092
.buildWithDefault(ServiceNameUtil.getDefaultServiceName());
@@ -303,6 +315,10 @@ public String getServiceName() {
303315
return serviceName.get();
304316
}
305317

318+
public ConfigurationOption<String> getServiceNameConfig() {
319+
return serviceName;
320+
}
321+
306322
public String getServiceVersion() {
307323
return serviceVersion.get();
308324
}

apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceNameUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import javax.annotation.Nonnull;
2323
import javax.annotation.Nullable;
2424

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

2828
static String getDefaultServiceName() {
@@ -70,7 +70,7 @@ private static String getSpecialServiceName(String command) {
7070
return null;
7171
}
7272

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

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package co.elastic.apm.agent.impl;
2121

2222
import co.elastic.apm.agent.configuration.CoreConfiguration;
23+
import co.elastic.apm.agent.configuration.ServiceNameUtil;
2324
import co.elastic.apm.agent.context.LifecycleListener;
2425
import co.elastic.apm.agent.impl.async.ContextInScopeCallableWrapper;
2526
import co.elastic.apm.agent.impl.async.ContextInScopeRunnableWrapper;
@@ -40,6 +41,7 @@
4041
import co.elastic.apm.agent.objectpool.impl.QueueBasedObjectPool;
4142
import co.elastic.apm.agent.report.Reporter;
4243
import co.elastic.apm.agent.report.ReporterConfiguration;
44+
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
4345
import org.jctools.queues.atomic.AtomicQueueFactory;
4446
import org.slf4j.Logger;
4547
import org.slf4j.LoggerFactory;
@@ -97,6 +99,7 @@ protected Deque<TraceContextHolder<?>> initialValue() {
9799
private final MetricRegistry metricRegistry;
98100
private Sampler sampler;
99101
boolean assertionsEnabled = false;
102+
private static final WeakConcurrentMap<ClassLoader, String> serviceNameByClassLoader = new WeakConcurrentMap.WithInlinedExpunction<>();
100103

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

181-
public Transaction startTransaction() {
182-
return startTransaction(TraceContext.asRoot(), null);
183-
}
184-
185-
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent) {
186-
return startTransaction(childContextCreator, parent, sampler, -1);
184+
/**
185+
* Starts a transaction as a child of the provided parent
186+
*
187+
* @param childContextCreator used to make the transaction a child of the provided parent
188+
* @param parent the parent of the transaction. May be a traceparent header.
189+
* @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction.
190+
* Used to determine the service name.
191+
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
192+
* @return a transaction which is a child of the provided parent
193+
*/
194+
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, @Nullable ClassLoader initiatingClassLoader) {
195+
return startTransaction(childContextCreator, parent, sampler, -1, initiatingClassLoader);
187196
}
188197

189-
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros) {
198+
/**
199+
* Starts a transaction as a child of the provided parent
200+
*
201+
* @param childContextCreator used to make the transaction a child of the provided parent
202+
* @param parent the parent of the transaction. May be a traceparent header.
203+
* @param sampler the {@link Sampler} instance which is responsible for determining the sampling decision if this is a root transaction
204+
* @param epochMicros the start timestamp
205+
* @param initiatingClassLoader the class loader corresponding to the service which initiated the creation of the transaction
206+
* Used to determine the service name.
207+
* @param <T> the type of the parent. {@code String} in case of a traceparent header.
208+
* @return a transaction which is a child of the provided parent
209+
*/
210+
public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> childContextCreator, @Nullable T parent, Sampler sampler, long epochMicros, @Nullable ClassLoader initiatingClassLoader) {
190211
Transaction transaction;
191212
if (!coreConfiguration.isActive()) {
192213
transaction = noopTransaction();
@@ -200,6 +221,10 @@ public <T> Transaction startTransaction(TraceContext.ChildContextCreator<T> chil
200221
new RuntimeException("this exception is just used to record where the transaction has been started from"));
201222
}
202223
}
224+
final String serviceName = getServiceName(initiatingClassLoader);
225+
if (serviceName != null) {
226+
transaction.getTraceContext().setServiceName(serviceName);
227+
}
203228
return transaction;
204229
}
205230

@@ -269,11 +294,21 @@ private boolean isTransactionSpanLimitReached(Transaction transaction) {
269294
return coreConfiguration.getTransactionMaxSpans() <= transaction.getSpanCount().getStarted().get();
270295
}
271296

272-
public void captureException(@Nullable Throwable e) {
273-
captureException(System.currentTimeMillis() * 1000, e, getActive());
297+
/**
298+
* Captures an exception without providing an explicit reference to a parent {@link TraceContextHolder}
299+
*
300+
* @param e the exception to capture
301+
* @param initiatingClassLoader the class
302+
*/
303+
public void captureException(@Nullable Throwable e, ClassLoader initiatingClassLoader) {
304+
captureException(System.currentTimeMillis() * 1000, e, getActive(), initiatingClassLoader);
274305
}
275306

276-
public void captureException(long epochMicros, @Nullable Throwable e, @Nullable TraceContextHolder<?> parent) {
307+
public void captureException(long epochMicros, @Nullable Throwable e, TraceContextHolder<?> parent) {
308+
captureException(epochMicros, e, parent, null);
309+
}
310+
311+
public void captureException(long epochMicros, @Nullable Throwable e, @Nullable TraceContextHolder<?> parent, @Nullable ClassLoader initiatingClassLoader) {
277312
if (e != null) {
278313
ErrorCapture error = errorPool.createInstance();
279314
error.withTimestamp(epochMicros);
@@ -287,6 +322,7 @@ public void captureException(long epochMicros, @Nullable Throwable e, @Nullable
287322
error.asChildOf(parent);
288323
} else {
289324
error.getTraceContext().getId().setToRandomValue();
325+
error.getTraceContext().setServiceName(getServiceName(initiatingClassLoader));
290326
}
291327
reporter.report(error);
292328
}
@@ -463,4 +499,40 @@ private void assertIsActive(Object span, @Nullable Object currentlyActive) {
463499
public MetricRegistry getMetricRegistry() {
464500
return metricRegistry;
465501
}
502+
503+
/**
504+
* Overrides the service name for all {@link Transaction}s,
505+
* {@link Span}s and {@link ErrorCapture}s which are created by the service which corresponds to the provided {@link ClassLoader}.
506+
* <p>
507+
* The main use case is being able to differentiate between multiple services deployed to the same application server.
508+
* </p>
509+
*
510+
* @param classLoader the class loader which corresponds to a particular service
511+
* @param serviceName the service name for this class loader
512+
*/
513+
public void overrideServiceNameForClassLoader(@Nullable ClassLoader classLoader, @Nullable String serviceName) {
514+
// overriding the service name for the bootstrap class loader is not an actual use-case
515+
// null may also mean we don't know about the initiating class loader
516+
if (classLoader == null
517+
|| serviceName == null || serviceName.isEmpty()
518+
// if the service name is set explicitly, don't override it
519+
|| !coreConfiguration.getServiceNameConfig().isDefault()) {
520+
return;
521+
}
522+
if (!serviceNameByClassLoader.containsKey(classLoader)) {
523+
serviceNameByClassLoader.putIfAbsent(classLoader, ServiceNameUtil.replaceDisallowedChars(serviceName));
524+
}
525+
}
526+
527+
@Nullable
528+
private String getServiceName(@Nullable ClassLoader initiatingClassLoader) {
529+
if (initiatingClassLoader == null) {
530+
return null;
531+
}
532+
return serviceNameByClassLoader.get(initiatingClassLoader);
533+
}
534+
535+
public void resetServiceNameOverrides() {
536+
serviceNameByClassLoader.clear();
537+
}
466538
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ public void resetState() {
114114
duration = 0;
115115
isLifecycleManagingThreadSwitch = false;
116116
traceContext.resetState();
117-
// don't reset previouslyActive, as deactivate can be called after end
118117
}
119118

120119
public boolean isChildOf(AbstractSpan<?> parent) {

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
2727

28+
import javax.annotation.Nullable;
2829
import java.util.concurrent.Callable;
2930

3031
/**
@@ -100,6 +101,8 @@ public boolean asChildOf(TraceContext child, Object ignore) {
100101
* @see EpochTickClock
101102
*/
102103
private EpochTickClock clock = new EpochTickClock();
104+
@Nullable
105+
private String serviceName;
103106

104107
private TraceContext(ElasticApmTracer tracer, Id id) {
105108
super(tracer);
@@ -214,6 +217,7 @@ public void asChildOf(TraceContext parent) {
214217
flags = parent.flags;
215218
id.setToRandomValue();
216219
clock.init(parent.clock);
220+
serviceName = parent.serviceName;
217221
onMutation();
218222
}
219223

@@ -229,6 +233,7 @@ public void resetState() {
229233
outgoingHeader.setLength(0);
230234
flags = 0;
231235
clock.resetState();
236+
serviceName = null;
232237
}
233238

234239
/**
@@ -335,6 +340,7 @@ public void copyFrom(TraceContext other) {
335340
outgoingHeader.append(other.outgoingHeader);
336341
flags = other.flags;
337342
clock.init(other.clock);
343+
serviceName = other.serviceName;
338344
onMutation();
339345
}
340346

@@ -351,6 +357,20 @@ public boolean isRoot() {
351357
return parentId.isEmpty();
352358
}
353359

360+
@Nullable
361+
public String getServiceName() {
362+
return serviceName;
363+
}
364+
365+
/**
366+
* Overrides the {@link co.elastic.apm.agent.impl.payload.Service#name} property sent via the meta data Intake V2 event.
367+
*
368+
* @param serviceName the service name for this event
369+
*/
370+
public void setServiceName(@Nullable String serviceName) {
371+
this.serviceName = serviceName;
372+
}
373+
354374
@Override
355375
public TraceContext getTraceContext() {
356376
return this;

0 commit comments

Comments
 (0)