Skip to content

Commit

Permalink
Add elastic-apm-agent-spring-boot-starter for ease of use with Spring…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>
  • Loading branch information
candrews committed Oct 19, 2023
1 parent 4f4b1c7 commit e947721
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions elastic-apm-agent-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>apm-agent-parent</artifactId>
<groupId>co.elastic.apm</groupId>
<version>1.43.1-SNAPSHOT</version>
</parent>

<artifactId>elastic-apm-agent-spring-boot-starter</artifactId>
<name>${project.groupId}:${project.artifactId}</name>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>

<properties>
<apm-agent-parent.base.dir>${project.basedir}/..</apm-agent-parent.base.dir>
<spring-boot.version>3.1.5</spring-boot.version>
</properties>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>apm-agent-attach</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${version.mockito}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>generate-source-jar</id>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, Object> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> apm = new HashMap<>();

public Map<String, String> getApm() {
return apm;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
co.elastic.apm.springboot.ElasticApmAgentAutoConfiguration

Original file line number Diff line number Diff line change
@@ -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> elasticApmAttacher = Mockito.mockStatic(ElasticApmAttacher.class)){
contextRunner.withPropertyValues("elastic.test=taco", "elastic.apm." + PROPERTY_KEY + "=" + PROPERTY_VALUE).withUserConfiguration(TestApplication.class).run(new ContextConsumer<ApplicationContext>() {
@Override
public void accept(ApplicationContext context) throws Throwable {
Verification verification = new Verification() {
@Override
public void apply() throws Throwable {
Map<String, String> 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 {
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<module>apm-agent-lambda-layer</module>
<module>elastic-apm-agent-premain</module>
<module>elastic-apm-agent-java8</module>
<module>elastic-apm-agent-spring-boot-starter</module>
<module>apm-agent-benchmarks</module>
<module>apm-agent-plugins</module>
<module>apm-agent-api</module>
Expand Down

0 comments on commit e947721

Please sign in to comment.