From e94772175f508e28f0ba4bb148e92d68d4b190cb Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Thu, 19 Oct 2023 17:15:16 -0400 Subject: [PATCH] Add elastic-apm-agent-spring-boot-starter for ease of use with Spring Boot The Spring Boot starter allows for use of Elastic APM in Spring Boot projects without writing any code. To use it, add a dependency on elastic-apm-agent-spring-boot-starter to the Spring Boot project. Elastic APM configuration can be provided via Spring configuration under elastic.apm.* For example, elastic.apm.server_url=http://127.0.0.1:8200 could be specified in application.properties. Relaxed binding, expressions, profiles, and all other Spring configuration features are available. The combination of no-code use (by just adding the dependency) and powerful configuration improves the usability of Elastic APM with Spring Boot projects. Signed-off-by: Craig Andrews --- CHANGELOG.asciidoc | 1 + elastic-apm-agent-spring-boot-starter/pom.xml | 79 ++++++++++++++++ .../ElasticApmAgentAutoConfiguration.java | 91 +++++++++++++++++++ .../apm/springboot/ElasticApmProperties.java | 33 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../ElasticApmAgentAutoConfigurationTest.java | 66 ++++++++++++++ pom.xml | 1 + 7 files changed, 273 insertions(+) create mode 100644 elastic-apm-agent-spring-boot-starter/pom.xml create mode 100644 elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfiguration.java create mode 100644 elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmProperties.java create mode 100644 elastic-apm-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 elastic-apm-agent-spring-boot-starter/src/test/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfigurationTest.java diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index c55082b3378..123002e943c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -35,6 +35,7 @@ Use subheadings with the "=====" level for adding notes for unreleased changes: ===== Features * Added protection against invalid timestamps provided by manual instrumentation - {pull}3363[#3363] * Added support for AWS SDK 2.21 - {pull}3373[#3373] +* Added a Spring Boot starter, `elastic-apm-agent-spring-boot-starter`, to improve ease of use of Elastic APM with Spring Boot projects. [float] ===== Bug fixes diff --git a/elastic-apm-agent-spring-boot-starter/pom.xml b/elastic-apm-agent-spring-boot-starter/pom.xml new file mode 100644 index 00000000000..79bd1d65884 --- /dev/null +++ b/elastic-apm-agent-spring-boot-starter/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + apm-agent-parent + co.elastic.apm + 1.43.1-SNAPSHOT + + + elastic-apm-agent-spring-boot-starter + ${project.groupId}:${project.artifactId} + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + ${project.basedir}/.. + 3.1.5 + + + + + ${project.groupId} + apm-agent-attach + ${project.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + true + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + org.mockito + mockito-inline + ${version.mockito} + test + + + net.bytebuddy + byte-buddy + test + + + + + + + maven-source-plugin + + + generate-source-jar + package + + jar-no-fork + + + + + + + + diff --git a/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfiguration.java b/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfiguration.java new file mode 100644 index 00000000000..9b07ddb0f00 --- /dev/null +++ b/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.springboot; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Elastic APM. + * + * Initializes Elastic APM with configuration specified in {@code elastic.apm} configuration properties. + * + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(co.elastic.apm.attach.ElasticApmAttacher.class) +@EnableConfigurationProperties(ElasticApmProperties.class) +public class ElasticApmAgentAutoConfiguration { + @Bean + static ElasticApmAttacher elasticApmAttacher() { + return new ElasticApmAttacher(); + } + + /** + * The Elastic APM agent should be attached as early as possible. + * {@link BeanFactoryPostProcessor} is the earliest available option that has the necessary dependencies to allow proper configuration of the agent. + */ + private static class ElasticApmAttacher implements BeanFactoryPostProcessor, EnvironmentAware { + private Environment environment; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + Assert.notNull(environment, "environment cannot be null"); + + // Beans are not fully initialized yet so beanFactory.getBean(ElasticApmProperties.class) would return an uninitialized bean without any properties set + ElasticApmProperties elasticApmProperties = Binder.get(environment).bind("elastic", ElasticApmProperties.class).get(); + + final Map configuration = new HashMap<>(elasticApmProperties.getApm()); + // If the application follows the Spring Boot best practice of having a @SpringBootApplication annotated class in the parent package of project, + // then Elastic APM can be configured based on that class. + // See: https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.using-the-springbootapplication-annotation + Map springBootApplications = beanFactory.getBeansWithAnnotation(SpringBootApplication.class); + if(springBootApplications.size() == 1) { + Object springBootApplicationBean = springBootApplications.values().iterator().next(); + String implementationTitle = springBootApplicationBean.getClass().getPackage().getImplementationTitle(); + if (implementationTitle != null) { + configuration.put("service_name", implementationTitle); + } + String implementationVersion = springBootApplicationBean.getClass().getPackage().getImplementationVersion(); + if (implementationVersion != null) { + configuration.put("service_version", implementationVersion); + } + configuration.put("application_packages", springBootApplicationBean.getClass().getPackageName()); + } + co.elastic.apm.attach.ElasticApmAttacher.attach(configuration); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + } +} diff --git a/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmProperties.java b/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmProperties.java new file mode 100644 index 00000000000..10c2b7c8b02 --- /dev/null +++ b/elastic-apm-agent-spring-boot-starter/src/main/java/co/elastic/apm/springboot/ElasticApmProperties.java @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.springboot; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "elastic") +public class ElasticApmProperties { + private final Map apm = new HashMap<>(); + + public Map getApm() { + return apm; + } +} diff --git a/elastic-apm-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/elastic-apm-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..3beb9ac53da --- /dev/null +++ b/elastic-apm-agent-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +co.elastic.apm.springboot.ElasticApmAgentAutoConfiguration + diff --git a/elastic-apm-agent-spring-boot-starter/src/test/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfigurationTest.java b/elastic-apm-agent-spring-boot-starter/src/test/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfigurationTest.java new file mode 100644 index 00000000000..ce3977e8afb --- /dev/null +++ b/elastic-apm-agent-spring-boot-starter/src/test/java/co/elastic/apm/springboot/ElasticApmAgentAutoConfigurationTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +package co.elastic.apm.springboot; + +import static org.mockito.Mockito.only; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; +import org.mockito.Mockito; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.ApplicationContext; + +import co.elastic.apm.attach.ElasticApmAttacher; + +class ElasticApmAgentAutoConfigurationTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void testAttach() { + final String PROPERTY_KEY = "testkey1"; + final String PROPERTY_VALUE = "testvalue1"; + try(MockedStatic elasticApmAttacher = Mockito.mockStatic(ElasticApmAttacher.class)){ + contextRunner.withPropertyValues("elastic.test=taco", "elastic.apm." + PROPERTY_KEY + "=" + PROPERTY_VALUE).withUserConfiguration(TestApplication.class).run(new ContextConsumer() { + @Override + public void accept(ApplicationContext context) throws Throwable { + Verification verification = new Verification() { + @Override + public void apply() throws Throwable { + Map configuration = new HashMap<>(); + configuration.put("application_packages", TestApplication.class.getPackageName()); + configuration.put(PROPERTY_KEY, PROPERTY_VALUE); + ElasticApmAttacher.attach(configuration); + } + }; + elasticApmAttacher.verify(verification, only()); + } + }); + } + } + + @SpringBootApplication + public static class TestApplication { + } +} diff --git a/pom.xml b/pom.xml index 338f7d30c6d..a3fb413f77a 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,7 @@ apm-agent-lambda-layer elastic-apm-agent-premain elastic-apm-agent-java8 + elastic-apm-agent-spring-boot-starter apm-agent-benchmarks apm-agent-plugins apm-agent-api