diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dc6350bfdb..f4167def9f 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -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 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 d6679b1491..c1714f62d5 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 @@ -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" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-jar-plugin\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "----\n" + + "\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 <>.\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")) @@ -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 <>, " + + "the agent can auto-detect the service version based on the `Implementation-Title` attribute in `META-INF/MANIFEST.MF`.\n" + + "See <> on how to set this attribute.\n" + + "\n") .defaultValue(ServiceInfo.autoDetected().getServiceVersion()) .build(); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceInfo.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceInfo.java index 35b9b880eb..f83398fa22 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceInfo.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/ServiceInfo.java @@ -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 { @@ -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())) { @@ -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); @@ -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, @@ -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 + '}'; } } diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletServiceNameHelper.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletServiceNameHelper.java index 5ddd2011ce..8ed2c4850d 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletServiceNameHelper.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/ServletServiceNameHelper.java @@ -50,6 +50,11 @@ public static void determineServiceName(ServletContextAdapter ServiceInfo detectServiceInfo(ServletContextAdapter adapter, ServletContext servletContext, ClassLoader servletContextClassLoader) { String servletContextName = adapter.getServletContextName(servletContext); String contextPath = adapter.getContextPath(servletContext); @@ -75,7 +80,7 @@ public static void determineServiceName(ServletContextAdapter + + ${project.groupId} + apm-servlet-plugin + ${project.version} + + javax.servlet javax.servlet-api @@ -28,6 +34,7 @@ ${version.spring} provided + org.springframework spring-test @@ -46,12 +53,6 @@ 2.0.2.RELEASE test - - ${project.groupId} - apm-servlet-plugin - ${project.version} - test - org.hamcrest hamcrest-core diff --git a/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/springwebmvc/SpringServiceNameInstrumentation.java b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/springwebmvc/SpringServiceNameInstrumentation.java index e0922c121d..60c2ff3a97 100644 --- a/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/springwebmvc/SpringServiceNameInstrumentation.java +++ b/apm-agent-plugins/apm-spring-webmvc-plugin/src/main/java/co/elastic/apm/agent/springwebmvc/SpringServiceNameInstrumentation.java @@ -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; @@ -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()) { + 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; } } } diff --git a/apm-agent/src/test/resources/configuration.asciidoc.ftl b/apm-agent/src/test/resources/configuration.asciidoc.ftl index 6de0111723..62be8c5bd2 100644 --- a/apm-agent/src/test/resources/configuration.asciidoc.ftl +++ b/apm-agent/src/test/resources/configuration.asciidoc.ftl @@ -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 <>, <> and <>. -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 @@ -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 [float] diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index daa869d7cc..f77db4ee12 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -57,7 +57,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 <>, <> and <>. -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 @@ -471,21 +474,59 @@ NOTE: Changing this value at runtime can slow down the application temporarily. 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. +Instead of configuring the service name manually, +you can also choose to rely on the service name auto-detection mechanisms of the agent. +If `service_name` is set explicitly, all auto-detection mechanisms are disabled. + +This is how the service name auto-detection works: + +* For standalone applications +** The agent uses `Implementation-Title` in the `META-INF/MANIFEST.MF` file if the application is started via `java -jar`. +** Falls back to the name of the main class or jar file. +* For applications that are deployed to a servlet container/application server, the agent auto-detects the name for each application. +** For Spring-based applications, the agent uses the `spring.application.name` property, if set. +** For servlet-based applications, falls back to the `Implementation-Title` in the `META-INF/MANIFEST.MF` file. +** Falls back to 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). + +Generally, it is recommended to rely on the service name detection based on `META-INF/MANIFEST.MF`. +Spring Boot automatically adds the relevant manifest entries. +For other applications that are built with Maven, this is how you add the manifest entries: + +[source,xml] +.pom.xml +---- + + + + + maven-jar-plugin + + + + + true + + + + + + + +---- + 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 <>. - NOTE: Service name auto discovery mechanisms require APM Server 7.0+. @@ -494,12 +535,7 @@ NOTE: Service name auto discovery mechanisms require APM Server 7.0+. [options="header"] |============ | Default | Type | Dynamic -| 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 | String | false |============ @@ -549,6 +585,11 @@ NOTE: Metrics views can utilize this configuration since APM Server 7.5 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. +Similar to the auto-detection of <>, the agent can auto-detect the service version based on the `Implementation-Title` attribute in `META-INF/MANIFEST.MF`. +See <> on how to set this attribute. + + + @@ -2870,31 +2911,66 @@ 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. # +# Instead of configuring the service name manually, +# you can also choose to rely on the service name auto-detection mechanisms of the agent. +# If `service_name` is set explicitly, all auto-detection mechanisms are disabled. +# +# This is how the service name auto-detection works: +# +# * For standalone applications +# ** The agent uses `Implementation-Title` in the `META-INF/MANIFEST.MF` file if the application is started via `java -jar`. +# ** Falls back to the name of the main class or jar file. +# * For applications that are deployed to a servlet container/application server, the agent auto-detects the name for each application. +# ** For Spring-based applications, the agent uses the `spring.application.name` property, if set. +# ** For servlet-based applications, falls back to the `Implementation-Title` in the `META-INF/MANIFEST.MF` file. +# ** Falls back to 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). +# +# Generally, it is recommended to rely on the service name detection based on `META-INF/MANIFEST.MF`. +# Spring Boot automatically adds the relevant manifest entries. +# For other applications that are built with Maven, this is how you add the manifest entries: +# +# +# [source,xml] +# .pom.xml +# ---- +# +# +# +# +# maven-jar-plugin +# +# +# +# +# true +# +# +# +# +# +# +# +# ---- +# +# # 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 <>. -# # NOTE: Service name auto discovery mechanisms require APM Server 7.0+. # # This setting can not be changed at runtime. Changes require a restart of the application. # Type: String -# 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). -# 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. +# Default value: Auto-detected based on the rules described above # # # service_name= @@ -2919,6 +2995,11 @@ The default unit for this option is `ms`. # service_node_name= # 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. +# +# Similar to the auto-detection of <>, the agent can auto-detect the service version based on the `Implementation-Title` attribute in `META-INF/MANIFEST.MF`. +# See <> on how to set this attribute. +# +# # # This setting can not be changed at runtime. Changes require a restart of the application. # Type: String 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 9504404734..936a8cd2e5 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 @@ -102,7 +102,9 @@ 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"); + // as this test runs in a standalone application and not in a servlet container, + // the service.name will not be overwritten for the webapp class loader based on spring.application.name + assertThat(transaction.getTraceContext().getServiceName()).isNull(); assertThat(transaction.getFrameworkName()).isEqualTo("Spring Web MVC"); assertThat(transaction.getFrameworkVersion()).isEqualTo("5.1.9.RELEASE"); }