Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ paths. The BCI warmup is on by default and may be disabled through the internal
* Fix External plugins automatic setting of span outcome - {pull}2376[#2376]
* Avoid early initialization of JMX on Weblogic - {pull}2420[#2420]
* Automatically disable class sharing on AWS lambda layer - {pull}2438[#2438]
* Avoid standalone spring applications to have two different service names, one based on the jar name, the other based on `spring.application.name`.

[[release-notes-1.x]]
=== Java Agent version 1.x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,61 @@ 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" +
"Instead of configuring the service name manually,\n" +
"you can also choose to rely on the service name auto-detection mechanisms of the agent.\n" +
"If `service_name` is set explicitly, all auto-detection mechanisms are disabled.\n" +
"\n" +
"This is how the service name auto-detection works:\n" +
"\n" +
"* For standalone applications\n" +
"** The agent uses `Implementation-Title` in the `META-INF/MANIFEST.MF` file if the application is started via `java -jar`.\n" +
"** Falls back to the name of the main class or jar file.\n" +
"* For applications that are deployed to a servlet container/application server, the agent auto-detects the name for each application.\n" +
"** For Spring-based applications, the agent uses the `spring.application.name` property, if set.\n" +
"** For servlet-based applications, falls back to the `Implementation-Title` in the `META-INF/MANIFEST.MF` file.\n" +
"** Falls back to the `display-name` of the `web.xml`, if available.\n" +
"** Falls back to the servlet context path the application is mapped to (unless mapped to the root context).\n" +
"\n" +
"Generally, it is recommended to rely on the service name detection based on `META-INF/MANIFEST.MF`.\n" +
"Spring Boot automatically adds the relevant manifest entries.\n" +
"For other applications that are built with Maven, this is how you add the manifest entries:\n" +
"\n" +
"<#noparse>\n" +
"[source,xml]\n" +
".pom.xml\n" +
"----\n" +
" <build>\n" +
" <plugins>\n" +
" <plugin>\n" +
" <!-- replace with 'maven-war-plugin' if you're building a war -->\n" +
" <artifactId>maven-jar-plugin</artifactId>\n" +
" <configuration>\n" +
" <archive>\n" +
" <!-- Adds\n" +
" Implementation-Title based on ${project.name} and\n" +
" Implementation-Version based on ${project.version}\n" +
" -->\n" +
" <manifest>\n" +
" <addDefaultImplementationEntries>true</addDefaultImplementationEntries>\n" +
" </manifest>\n" +
" <!-- To customize the Implementation-* entries, remove addDefaultImplementationEntries and add them manually\n" +
" <manifestEntries>\n" +
" <Implementation-Title>foo</Implementation-Title>\n" +
" <Implementation-Version>4.2.0</Implementation-Version>\n" +
" </manifestEntries>\n" +
" -->\n" +
" </archive>\n" +
" </configuration>\n" +
" </plugin>\n" +
" </plugins>\n" +
" </build>\n" +
"----\n" +
"</#noparse>\n" +
"\n" +
"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 <<config-disable-instrumentations>>.\n" +
"\n" +
"NOTE: Service name auto discovery mechanisms require APM Server 7.0+.")
.addValidator(RegexValidator.of("^[a-zA-Z0-9 _-]+$", "Your service name \"{0}\" must only contain characters " +
"from the ASCII alphabet, numbers, dashes, underscores and spaces"))
Expand Down Expand Up @@ -144,7 +184,12 @@ public class CoreConfiguration extends ConfigurationOptionProvider {
.configurationCategory(CORE_CATEGORY)
.description("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.")
"e.g. the output of git rev-parse HEAD.\n" +
"\n" +
"Similar to the auto-detection of <<config-service-name>>, " +
"the agent can auto-detect the service version based on the `Implementation-Title` attribute in `META-INF/MANIFEST.MF`.\n" +
"See <<config-service-name>> on how to set this attribute.\n" +
"\n")
.defaultValue(ServiceInfo.autoDetected().getServiceVersion())
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,18 @@ public class ServiceInfo {
private final String serviceName;
@Nullable
private final String serviceVersion;
private final boolean multiServiceContainer;

public ServiceInfo(@Nullable String serviceName) {
this(serviceName, null);
}

private ServiceInfo(@Nullable String serviceName, @Nullable String serviceVersion) {
this(serviceName, serviceVersion, false);
}

private ServiceInfo(@Nullable String serviceName, @Nullable String serviceVersion, boolean multiServiceContainer) {
this.multiServiceContainer = multiServiceContainer;
if (serviceName == null || serviceName.trim().isEmpty()) {
this.serviceName = DEFAULT_SERVICE_NAME;
} else {
Expand All @@ -57,6 +63,10 @@ public static ServiceInfo of(@Nullable String serviceName) {
return of(serviceName, null);
}

public static ServiceInfo ofMultiServiceContainer(String serviceName) {
return new ServiceInfo(serviceName, null, true);
}

public static ServiceInfo of(@Nullable String serviceName, @Nullable String serviceVersion) {
if ((serviceName == null || serviceName.isEmpty()) &&
(serviceVersion == null || serviceVersion.isEmpty())) {
Expand Down Expand Up @@ -94,7 +104,7 @@ private static ServiceInfo createFromSunJavaCommand(@Nullable String command) {
command = command.trim();
String serviceName = getContainerServiceName(command);
if (serviceName != null) {
return ServiceInfo.of(serviceName);
return ServiceInfo.ofMultiServiceContainer(serviceName);
}
if (command.contains(".jar")) {
return fromJarCommand(command);
Expand Down Expand Up @@ -181,6 +191,15 @@ public String getServiceVersion() {
return serviceVersion;
}

/**
* Returns true if the service is a container service that can host multiple other applications.
* For example, an application server or servlet container.
* A standalone application that's built on embedded Tomcat, for example, would return {@code false}.
*/
public boolean isMultiServiceContainer() {
return multiServiceContainer;
}

public ServiceInfo withFallback(ServiceInfo fallback) {
return ServiceInfo.of(
hasServiceName() ? serviceName : fallback.serviceName,
Expand All @@ -200,19 +219,20 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ServiceInfo that = (ServiceInfo) o;
return serviceName.equals(that.serviceName) && Objects.equals(serviceVersion, that.serviceVersion);
return multiServiceContainer == that.multiServiceContainer && serviceName.equals(that.serviceName) && Objects.equals(serviceVersion, that.serviceVersion);
}

@Override
public int hashCode() {
return Objects.hash(serviceName, serviceVersion);
return Objects.hash(serviceName, serviceVersion, multiServiceContainer);
}

@Override
public String toString() {
return "ServiceInfo{" +
"name='" + serviceName + '\'' +
", version='" + serviceVersion + '\'' +
"serviceName='" + serviceName + '\'' +
", serviceVersion='" + serviceVersion + '\'' +
", multiServiceContainer=" + multiServiceContainer +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public static <ServletContext> void determineServiceName(ServletContextAdapter<S
if (servletContextClassLoader == null || nameInitialized.putIfAbsent(servletContextClassLoader, Boolean.TRUE) != null) {
return;
}
ServiceInfo serviceInfo = detectServiceInfo(adapter, servletContext, servletContextClassLoader);
tracer.overrideServiceInfoForClassLoader(servletContextClassLoader, serviceInfo);
}

public static <ServletContext> ServiceInfo detectServiceInfo(ServletContextAdapter<ServletContext> adapter, ServletContext servletContext, ClassLoader servletContextClassLoader) {
String servletContextName = adapter.getServletContextName(servletContext);
String contextPath = adapter.getContextPath(servletContext);

Expand All @@ -75,7 +80,7 @@ public static <ServletContext> void determineServiceName(ServletContextAdapter<S
// remove leading slash
fromContextPath = ServiceInfo.of(contextPath.substring(1));
}
tracer.overrideServiceInfoForClassLoader(servletContextClassLoader, fromWarManifest.withFallback(fromContextName).withFallback(fromContextPath));
return fromWarManifest.withFallback(fromContextName).withFallback(fromContextPath);
}

@Nullable
Expand Down
13 changes: 7 additions & 6 deletions apm-agent-plugins/apm-spring-webmvc-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
</properties>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-servlet-plugin</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
Expand All @@ -28,6 +34,7 @@
<version>${version.spring}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
Expand All @@ -46,12 +53,6 @@
<version>2.0.2.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-servlet-plugin</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

import co.elastic.apm.agent.bci.TracerAwareInstrumentation;
import co.elastic.apm.agent.configuration.ServiceInfo;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import co.elastic.apm.agent.servlet.ServletServiceNameHelper;
import co.elastic.apm.agent.servlet.adapter.JavaxServletApiAdapter;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.method.MethodDescription;
Expand Down Expand Up @@ -67,50 +67,47 @@ public String getAdviceClassName() {

public static class SpringServiceNameAdvice {

private static final Logger logger = LoggerFactory.getLogger(SpringServiceNameAdvice.class);

@Advice.OnMethodExit(suppress = Throwable.class, inline = false)
public static void afterInitPropertySources(@Advice.This WebApplicationContext applicationContext) {
// avoid having two service names for a standalone jar
// one based on Implementation-Title and one based on spring.application.name
if (!ServiceInfo.autoDetected().isMultiServiceContainer()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could still honor the spring application name here in case the manifest entry is not set.

  • If the manifest entry is available, we make isMultiServiceContainer() return false and then ServiceInfo.autoDetected() will match what was read in the manifest
  • If the manifest entry is not available, we should still set it, so in the case of our test application it should still work as expected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified this through an offline discussion: we have another fallback here for the ServiceInfo.autoDetected() that relies on the .jar name. Unfortunately when running in the IDE the test app is not packaged and thus not available.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some background:
The risk of that is that if we don't properly detect that we're running on an application server/servlet container. In that case, we wouldn't be able to determine the service name based on spring.application.name. That's why I didn't add the check in Tracer.overrideServiceInfoForClassLoader but made it specific to this instrumentation. This way, we at least can still override based on the servlet context path and the display-name. It's quite unlikely that a standalone application that's using embedded Tomcat, for example, sets a display-name or even a servlet context path. Therefore, if any of these are set, that's a pretty good indication that we're running in a container and not in a standalone jar.

return;
}
// This method will be called whenever the spring application context is refreshed which may be more than once
//
// For example, using Tomcat Servlet container, it's called twice with the first not having a ServletContext,
// while the second does, and later requests are initiated with the Servlet classloader and not the application
// classloader.
ClassLoader classLoader = applicationContext.getClassLoader();

ServiceInfo fromServletContext = ServiceInfo.empty();
ServletContext servletContext = applicationContext.getServletContext();
if (servletContext != null) {
try {
ClassLoader servletClassloader = servletContext.getClassLoader();
if (servletClassloader != null) {
classLoader = servletClassloader;
fromServletContext = ServletServiceNameHelper.detectServiceInfo(JavaxServletApiAdapter.get(), servletContext, servletClassloader);
}
} catch (UnsupportedOperationException e) {
// silently ignored
}
}

String appName = applicationContext.getEnvironment().getProperty("spring.application.name", "");
ServiceInfo fromSpringApplicationNameProperty = ServiceInfo.of(applicationContext.getEnvironment().getProperty("spring.application.name", ""));
ServiceInfo fromApplicationContextApplicationName = ServiceInfo.of(removeLeadingSlash(applicationContext.getApplicationName()));

if (!appName.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("Setting service name `{}` to be used for class loader [{}], based on the value of " +
"the `spring.application.name` environment variable", appName, classLoader);
}
} else {
// fallback when application name isn't set through an environment property
appName = applicationContext.getApplicationName();
// remove '/' (if any) from application name
if (appName.startsWith("/")) {
appName = appName.substring(1);
}
if (logger.isDebugEnabled()) {
logger.debug("``spring.application.name` environment variable is not set, falling back to using `{}` " +
"as service name for class loader [{}]", appName, classLoader);
}
}
tracer.overrideServiceInfoForClassLoader(classLoader, fromSpringApplicationNameProperty
.withFallback(fromServletContext)
.withFallback(fromApplicationContextApplicationName));
}

tracer.overrideServiceInfoForClassLoader(classLoader, ServiceInfo.of(appName));
private static String removeLeadingSlash(String appName) {
// remove '/' (if any) from application name
if (appName.startsWith("/")) {
appName = appName.substring(1);
}
return appName;
}
}
}
12 changes: 5 additions & 7 deletions apm-agent/src/test/resources/configuration.asciidoc.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ Only the external file can be used for dynamic configuration.
In order to get started with Elastic APM,
the most important configuration options are <<config-service-name>>,
<<config-server-url>> and <<config-application-packages>>.
So a minimal version of a configuration might look like this:
Note that even these settings are optional.
Click on their name to see how the default values are determined.

An example configuration looks like this:

[source,bash]
.System properties
Expand All @@ -80,12 +83,7 @@ ELASTIC_APM_APPLICATION_PACKAGES=org.example,org.another.example
ELASTIC_APM_SERVER_URL=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).
Falls back to the `Implementation-Title` in the `MANIFEST.MF` file.
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.
Auto-detected based on the rules described above
</#assign>

[float]
Expand Down
Loading