├── plugin ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── co.elastic.apm.agent.sdk.ElasticApmInstrumentation │ │ └── java │ │ │ └── co │ │ │ └── elastic │ │ │ └── apm │ │ │ └── example │ │ │ └── webserver │ │ │ └── plugin │ │ │ ├── ExampleMetricsInstrumentation.java │ │ │ ├── ExampleMicrometerMetricsInstrumentation.java │ │ │ └── ExampleHttpServerInstrumentation.java │ └── test │ │ └── java │ │ └── co │ │ └── elastic │ │ └── apm │ │ ├── plugin │ │ └── AbstractInstrumentationTest.java │ │ ├── example │ │ └── webserver │ │ │ └── ExampleHttpServerInstrumentationIT.java │ │ └── mock │ │ └── MockApmServer.java └── pom.xml ├── .gitignore ├── application ├── src │ └── main │ │ └── java │ │ └── co │ │ └── elastic │ │ └── apm │ │ └── example │ │ └── webserver │ │ ├── ExampleHttpServer.java │ │ ├── ExampleClient.java │ │ ├── ExampleAlreadyInstrumentedHttpServer.java │ │ └── ExampleBasicHttpServer.java └── pom.xml ├── pom.xml ├── runExamples.bat ├── runExamples.bash ├── README.md └── LICENSE /plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation: -------------------------------------------------------------------------------- 1 | co.elastic.apm.example.webserver.plugin.ExampleHttpServerInstrumentation 2 | co.elastic.apm.example.webserver.plugin.ExampleMetricsInstrumentation 3 | co.elastic.apm.example.webserver.plugin.ExampleMicrometerMetricsInstrumentation 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | target 25 | .idea/ 26 | -------------------------------------------------------------------------------- /application/src/main/java/co/elastic/apm/example/webserver/ExampleHttpServer.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver; 2 | 3 | import java.io.IOException; 4 | 5 | public interface ExampleHttpServer { 6 | public void blockUntilReady(); 7 | public void blockUntilStopped(); 8 | public void stop(); 9 | public void start() throws IOException; 10 | public int getLocalPort(); 11 | } 12 | -------------------------------------------------------------------------------- /application/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | co.elastic.apm 4 | application 5 | 0.0.3-SNAPSHOT 6 | 7 | 8 | 9 | org.apache.maven.plugins 10 | maven-compiler-plugin 11 | 3.10.1 12 | 13 | 11 14 | 11 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | co.elastic.apm 6 | apm-agent-java-plugin-example 7 | 1.0.1 8 | pom 9 | 10 | APM Java Agent Plugin Example 11 | 12 | 2022 13 | 14 | 15 | Elastic Inc. 16 | https://www.elastic.co 17 | 18 | 19 | APM Java Agent Plugin Example 20 | https://github.com/elastic/apm-agent-java-plugin-example 21 | 22 | 23 | 24 | The Apache Software License, Version 2.0 25 | http://www.apache.org/licenses/LICENSE-2.0.txt 26 | 27 | 28 | 29 | 30 | 31 | Elastic 32 | https://discuss.elastic.co/c/apm 33 | Elastic Inc. 34 | https://www.elastic.co 35 | 36 | 37 | 38 | 39 | 3.8.8 40 | 41 | 42 | 43 | application 44 | plugin 45 | 46 | 47 | -------------------------------------------------------------------------------- /runExamples.bat: -------------------------------------------------------------------------------- 1 | IF "x%AGENT_JAR%" == "x" echo "ERROR: 'AGENT_JAR' env var must be set" && pause && exit 1 2 | 3 | FOR %%f in (application\target\*.jar) DO set TARGET_JAR=%%f 4 | 5 | echo "Run the standalone application, *with no agent*" 6 | echo "First the ExampleAlreadyInstrumentedHttpServer" 7 | java -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 8 | echo "Then the uninstrumented ExampleBasicHttpServer" 9 | java -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 10 | 11 | echo "Run the standalone application, *with the agent but no plugin*" 12 | echo "First the ExampleAlreadyInstrumentedHttpServer" 13 | java -Delastic.apm.server_url=%APM_SERVER_URL% -Delastic.apm.secret_token=%APM_SECRET_TOKEN% -javaagent:%AGENT_JAR% -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 14 | echo "Then the still uninstrumented ExampleBasicHttpServer (still uninstrumented because the plugin is not present)" 15 | java -Delastic.apm.server_url=%APM_SERVER_URL% -Delastic.apm.secret_token=%APM_SECRET_TOKEN% -javaagent:%AGENT_JAR% -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 16 | 17 | echo "Run the standalone application, *with the agent and plugin*" 18 | echo "First the ExampleAlreadyInstrumentedHttpServer" 19 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin\target -Delastic.apm.server_url=%APM_SERVER_URL% -Delastic.apm.secret_token=%APM_SECRET_TOKEN% -javaagent:%AGENT_JAR% -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 20 | echo "Then the now instrumented ExampleBasicHttpServer (instrumented by the plugin)" 21 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin\target -Delastic.apm.server_url=%APM_SERVER_URL% -Delastic.apm.secret_token=%APM_SECRET_TOKEN% -javaagent:%AGENT_JAR% -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp %TARGET_JAR% co.elastic.apm.example.webserver.ExampleClient 22 | -------------------------------------------------------------------------------- /runExamples.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set AGENT_JAR to the full or relative path to the Elastic APM Java agent jar, eg 3 | #export AGENT_JAR=../elastic-apm-agent-1.32.0.jar 4 | if [ "x$AGENT_JAR" == "x" ]; then 5 | echo "ERROR: 'AGENT_JAR' env var must be set" 6 | exit 1 7 | fi 8 | 9 | echo "Run the standalone application, *with no agent*" 10 | echo "First the ExampleAlreadyInstrumentedHttpServer" 11 | java -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 12 | echo "Then the uninstrumented ExampleBasicHttpServer" 13 | java -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 14 | 15 | echo "Run the standalone application, *with the agent but no plugin*" 16 | echo "First the ExampleAlreadyInstrumentedHttpServer" 17 | java -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 18 | echo "Then the still uninstrumented ExampleBasicHttpServer (still uninstrumented because the plugin is not present)" 19 | java -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 20 | 21 | echo "Run the standalone application, *with the agent and plugin*" 22 | echo "First the ExampleAlreadyInstrumentedHttpServer" 23 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin/target -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 24 | echo "Then the now instrumented ExampleBasicHttpServer (instrumented by the plugin)" 25 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin/target -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 26 | -------------------------------------------------------------------------------- /application/src/main/java/co/elastic/apm/example/webserver/ExampleClient.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.net.http.HttpClient; 6 | import java.net.http.HttpRequest; 7 | import java.net.http.HttpResponse; 8 | 9 | public class ExampleClient { 10 | public static int PORT; 11 | 12 | public static void main(String[] args) throws IOException, InterruptedException { 13 | // Start the server in a separate thread 14 | System.out.println("ExampleClient: Starting the webserver"); 15 | ExampleHttpServer server; 16 | if ("ExampleClient-ExampleAlreadyInstrumentedHttpServer".equals(System.getProperty("elastic.apm.service_name"))) { 17 | server = new ExampleAlreadyInstrumentedHttpServer(); 18 | } else if ("ExampleClient-ExampleBasicHttpServer".equals(System.getProperty("elastic.apm.service_name"))) { 19 | server = new ExampleBasicHttpServer(); 20 | } else { 21 | throw new IOException("Must set -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer or -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer"); 22 | } 23 | new Thread(() -> {startServer(server);}).start(); 24 | System.out.println("ExampleClient: waiting for webserver to be ready"); 25 | server.blockUntilReady(); 26 | PORT = server.getLocalPort(); 27 | HttpClient client = HttpClient.newHttpClient(); 28 | executeRequest(client, "nothing"); 29 | executeRequest(client, "nothing?withsomething=true"); 30 | executeRequest(client, "nothing#somelink"); 31 | executeRequest(client, "nothing#somelink?withsomething=true"); 32 | executeRequest(client, "exit"); 33 | 34 | System.out.println("ExampleClient: waiting for webserver terminate (or will exit in 10 seconds regardless)"); 35 | exitIn10Seconds(); 36 | server.blockUntilStopped(); 37 | 38 | System.out.println("ExampleClient: Exiting"); 39 | } 40 | 41 | private static void exitIn10Seconds() { 42 | new Thread() { 43 | public void run() { 44 | try {Thread.sleep(10_000L);} catch (InterruptedException e) {} 45 | System.exit(0); 46 | } 47 | }.start(); 48 | } 49 | 50 | private static void executeRequest(HttpClient client, String req) throws IOException, InterruptedException { 51 | HttpRequest request = HttpRequest.newBuilder() 52 | .uri(URI.create("http://localhost:"+PORT+"/"+req)) 53 | .GET() // GET is default 54 | .build(); 55 | 56 | System.out.println("ExampleClient: calling "+request); 57 | HttpResponse response = client.send(request, 58 | HttpResponse.BodyHandlers.ofString()); 59 | System.out.println("ExampleClient: call result status is "+response.statusCode()); 60 | } 61 | 62 | private static void startServer(ExampleHttpServer server) { 63 | try { 64 | server.start(); 65 | } catch (IOException e) { 66 | e.printStackTrace(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin/src/test/java/co/elastic/apm/plugin/AbstractInstrumentationTest.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.plugin; 2 | 3 | import co.elastic.apm.attach.ElasticApmAttacher; 4 | import co.elastic.apm.example.webserver.plugin.ExampleHttpServerInstrumentation; 5 | import co.elastic.apm.mock.MockApmServer; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class AbstractInstrumentationTest { 14 | 15 | private static final HashMap Properties = new HashMap<>(); 16 | protected static final MockApmServer ApmServer = new MockApmServer(); 17 | 18 | @BeforeAll 19 | public static void startApmServerAndSetAgentPropertiesAndStartAgent() throws IOException { 20 | //Start the mock APM server - receives transactions from the agent 21 | int mockApmServerPort = ApmServer.start(); 22 | ApmServer.blockUntilReady(); 23 | 24 | //Set properties before starting the agent 25 | setProperty("elastic.apm.server_url", "http://localhost:"+mockApmServerPort); 26 | setProperty("elastic.apm.plugins_dir", "target"); //to load the plugin 27 | setProperty("elastic.apm.enable_experimental_instrumentations", "true"); //need for Otel in 1.30 28 | setProperty("elastic.apm.api_request_size", "100b"); //flush quickly - inadvisably short outside tests 29 | setProperty("elastic.apm.report_sync", "true"); //DON'T USE EXCEPT IN TEST!! 30 | setProperty("elastic.apm.metrics_interval", "1s"); //flush metrics quickly - inadvisably short outside tests 31 | 32 | setProperty("elastic.apm.log_level", "DEBUG"); 33 | //Setting this makes the agent startup faster 34 | String instrumentations = "micrometer, opentelemetry, opentelemetry-metrics, "+String.join(", ", 35 | new ExampleHttpServerInstrumentation().getInstrumentationGroupNames()); 36 | setProperty("elastic.apm.enable_instrumentations", instrumentations); 37 | 38 | //Start the agent 39 | ElasticApmAttacher.attach(); 40 | } 41 | 42 | @AfterAll 43 | public static void stopApmServerAndResetProperties() { 44 | resetProperties(); 45 | ApmServer.stop(); 46 | } 47 | 48 | public static void setProperty(String name, String value){ 49 | synchronized (Properties) { 50 | if (Properties.containsKey(name)) { 51 | throw new IllegalStateException("Cannot redefine a property before resetting it: " + name); 52 | } 53 | String oldPropertyValue = System.getProperty(name); 54 | Properties.put(name, oldPropertyValue); 55 | System.setProperty(name, value); 56 | } 57 | } 58 | 59 | public static void resetProperties(){ 60 | synchronized (Properties) { 61 | for (Map.Entry entry : Properties.entrySet()) { 62 | if (entry.getValue() == null) { 63 | System.clearProperty(entry.getKey()); 64 | } else { 65 | System.setProperty(entry.getKey(), entry.getValue()); 66 | } 67 | } 68 | Properties.clear(); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /plugin/src/main/java/co/elastic/apm/example/webserver/plugin/ExampleMetricsInstrumentation.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver.plugin; 2 | 3 | import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; 4 | import io.opentelemetry.api.GlobalOpenTelemetry; 5 | import io.opentelemetry.api.metrics.LongCounter; 6 | import net.bytebuddy.asm.Advice; 7 | import net.bytebuddy.description.method.MethodDescription; 8 | import net.bytebuddy.description.type.TypeDescription; 9 | import net.bytebuddy.matcher.ElementMatcher; 10 | 11 | import java.util.Collection; 12 | import java.util.Collections; 13 | 14 | import static net.bytebuddy.matcher.ElementMatchers.named; 15 | import static net.bytebuddy.matcher.ElementMatchers.takesArgument; 16 | import static net.bytebuddy.matcher.ElementMatchers.takesArguments; 17 | 18 | /** 19 | * For detailed documentation of the 3 overridden methods, see the 20 | * ExampleHttpServerInstrumentation class in this package. 21 | * 22 | * A quick summary is that we are matching the 23 | * ExampleBasicHttpServer.handleRequest() method whenever 24 | * it gets loaded, and we'll instrument that method using 25 | * the inner AdviceClass class below 26 | */ 27 | public class ExampleMetricsInstrumentation extends ElasticApmInstrumentation { 28 | @Override 29 | public ElementMatcher getTypeMatcher() { 30 | return named("co.elastic.apm.example.webserver.ExampleBasicHttpServer"); 31 | } 32 | 33 | @Override 34 | public ElementMatcher getMethodMatcher() { 35 | return named("handleRequest").and(takesArguments(3)) 36 | .and(takesArgument(0, named("java.lang.String"))); 37 | } 38 | 39 | @Override 40 | public Collection getInstrumentationGroupNames() { 41 | return Collections.singletonList("elastic-plugin-example"); 42 | } 43 | 44 | 45 | /** 46 | * This advice class is applied when the instrumentation identifies 47 | * it needs to be applied, ie when the above matchers ({@code getTypeMatcher} 48 | * and {@code getMethodMatcher}) have been matched 49 | * 50 | * The Elastic APM Java agent provides the OpenTelemetry metrics capability - 51 | 52 | * the agent fully implements the OpenTelemetry metrics framework. See 53 | * https://www.elastic.co/guide/en/apm/agent/java/master/opentelemetry-bridge.html#otel-metrics 54 | */ 55 | public static class AdviceClass { 56 | /** 57 | * At initialization, we register with the OpenTelemetry registry by 58 | 59 | * creating a meter, a page count metric (`page_views`) which will be available 60 | * in the Elastic APM metrics views. 61 | * 62 | * For details on the Byte Buddy advice annotation used here, 63 | * see the ExampleHttpServerInstrumentation$AdviceClass 64 | * class and it's `onEnterHandle` method javadoc, in this package. 65 | */ 66 | private static volatile LongCounter pageViewCounter; 67 | 68 | @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) 69 | public static void onEnterHandle() { 70 | if (pageViewCounter == null) { 71 | pageViewCounter = GlobalOpenTelemetry 72 | .getMeter("ExampleHttpServer") 73 | .counterBuilder("page_views") 74 | .setDescription("Page view count") 75 | .build(); 76 | } 77 | pageViewCounter.add(1); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /plugin/src/main/java/co/elastic/apm/example/webserver/plugin/ExampleMicrometerMetricsInstrumentation.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver.plugin; 2 | 3 | import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; 4 | import io.micrometer.core.instrument.Clock; 5 | import io.micrometer.core.instrument.Metrics; 6 | import io.micrometer.core.instrument.simple.CountingMode; 7 | import io.micrometer.core.instrument.simple.SimpleConfig; 8 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 9 | import net.bytebuddy.asm.Advice; 10 | import net.bytebuddy.description.method.MethodDescription; 11 | import net.bytebuddy.description.type.TypeDescription; 12 | import net.bytebuddy.matcher.ElementMatcher; 13 | 14 | import java.time.Duration; 15 | import java.util.Collection; 16 | import java.util.Collections; 17 | 18 | import static net.bytebuddy.matcher.ElementMatchers.named; 19 | import static net.bytebuddy.matcher.ElementMatchers.takesArgument; 20 | import static net.bytebuddy.matcher.ElementMatchers.takesArguments; 21 | 22 | /** 23 | * For detailed documentation of the 3 overridden methods, see the 24 | * ExampleHttpServerInstrumentation class in this package. 25 | * 26 | * A quick summary is that we are matching the 27 | * ExampleBasicHttpServer.handleRequest() method whenever 28 | * it gets loaded, and we'll instrument that method using 29 | * the inner AdviceClass class below 30 | */ 31 | public class ExampleMicrometerMetricsInstrumentation extends ElasticApmInstrumentation { 32 | @Override 33 | public ElementMatcher getTypeMatcher() { 34 | return named("co.elastic.apm.example.webserver.ExampleBasicHttpServer"); 35 | } 36 | 37 | @Override 38 | public ElementMatcher getMethodMatcher() { 39 | return named("handleRequest").and(takesArguments(3)) 40 | .and(takesArgument(0, named("java.lang.String"))); 41 | } 42 | 43 | @Override 44 | public Collection getInstrumentationGroupNames() { 45 | return Collections.singletonList("elastic-plugin-example"); 46 | } 47 | 48 | 49 | /** 50 | * This advice class is applied when the instrumentation identifies 51 | * it needs to be applied, ie when the above matchers ({@code getTypeMatcher} 52 | * and {@code getMethodMatcher}) have been matched 53 | * 54 | * The ELastic APM Java agent provides a metrics capability using 55 | * the Micrometer framework, see 56 | * https://www.elastic.co/guide/en/apm/agent/java/current/metrics.html#metrics-micrometer 57 | */ 58 | public static class AdviceClass { 59 | /** 60 | * At method entry, we want to ensure that we've registered the 61 | * with the micrometer registry (only needed to do once, so 62 | * it's guarded with a boolean), then we'll increment 63 | * a page count metric, `page_count` which will be available 64 | * in the Elastic APM metrics views. 65 | * 66 | * For details on the Byte Buddy advice annotation used here, 67 | * see the ExampleHttpServerInstrumentation$AdviceClass 68 | * class and it's `onEnterHandle` method javadoc, in this package. 69 | */ 70 | private static volatile boolean metricWasAdded = false; 71 | @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) 72 | public static void onEnterHandle() { 73 | if (!metricWasAdded) { 74 | Metrics.addRegistry(new SimpleMeterRegistry(new SimpleConfig() { 75 | 76 | @Override 77 | public CountingMode mode() { 78 | // to report the delta since the last report 79 | // this makes building dashboards a bit easier 80 | return CountingMode.STEP; 81 | } 82 | 83 | @Override 84 | public Duration step() { 85 | // the duration should match metrics_interval, which defaults to 30s 86 | return Duration.ofSeconds(30); 87 | } 88 | 89 | @Override 90 | public String get(String key) { 91 | return null; 92 | } 93 | }, Clock.SYSTEM)); 94 | metricWasAdded = true; 95 | } 96 | Metrics.counter("page_counter").increment(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | co.elastic.apm 4 | plugin 5 | 0.0.3-SNAPSHOT 6 | 7 | 8 | 1.39.0 9 | 10 | 11 | 12 | 13 | 14 | co.elastic.apm 15 | apm-agent-plugin-sdk 16 | ${version.elastic-agent} 17 | provided 18 | 19 | 20 | 21 | io.opentelemetry 22 | opentelemetry-api 23 | 1.25.0 24 | 25 | 26 | io.micrometer 27 | micrometer-core 28 | 1.9.0 29 | 30 | 31 | org.hdrhistogram 32 | HdrHistogram 33 | 34 | 35 | 36 | 37 | 38 | org.junit.jupiter 39 | junit-jupiter 40 | 5.8.2 41 | test 42 | 43 | 44 | 45 | co.elastic.apm 46 | apm-agent-attach 47 | ${version.elastic-agent} 48 | test 49 | 50 | 51 | 52 | co.elastic.apm 53 | application 54 | 0.0.3-SNAPSHOT 55 | test 56 | 57 | 58 | 59 | com.fasterxml.jackson.core 60 | jackson-databind 61 | 2.13.2.2 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-surefire-plugin 71 | 2.22.0 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-failsafe-plugin 77 | 2.22.0 78 | 79 | 80 | 81 | integration-test 82 | verify 83 | 84 | 85 | 86 | 87 | 89 | 90 | org.apache.maven.plugins 91 | maven-compiler-plugin 92 | 3.10.1 93 | 94 | 11 95 | 11 96 | 97 | 98 | 99 | 100 | maven-jar-plugin 101 | 3.0.2 102 | 103 | 104 | default-jar 105 | none 106 | 107 | 108 | 109 | 110 | maven-assembly-plugin 111 | 112 | 113 | package 114 | 115 | single 116 | 117 | 118 | 119 | jar-with-dependencies 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /application/src/main/java/co/elastic/apm/example/webserver/ExampleAlreadyInstrumentedHttpServer.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver; 2 | 3 | import com.sun.net.httpserver.HttpExchange; 4 | import com.sun.net.httpserver.HttpHandler; 5 | import com.sun.net.httpserver.HttpServer; 6 | 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | import java.net.InetSocketAddress; 10 | 11 | /** 12 | * This uses the HttpServer embedded in the JDK. This HTTP server is already 13 | * instrumented by the Elastic Java agent in the apm-jdk-httpserver-plugin at 14 | * https://github.com/elastic/apm-agent-java/tree/master/apm-agent-plugins/apm-jdk-httpserver-plugin 15 | *

16 | * This class is only here as a reference for you to compare agent internal 17 | * instrumentation against the instrumentation implemented here for the 18 | * other `ExampleHttpServer` 19 | */ 20 | public class ExampleAlreadyInstrumentedHttpServer implements ExampleHttpServer { 21 | private static volatile HttpServer TheServerInstance; 22 | private static String TheServerRootPage; 23 | private HttpServer thisServer; 24 | 25 | @Override 26 | public int getLocalPort() { 27 | return TheServerInstance == null ? -1 : TheServerInstance.getAddress().getPort(); 28 | } 29 | 30 | @Override 31 | public synchronized void start() throws IOException { 32 | if (TheServerInstance != null) { 33 | throw new IOException("ExampleHttpServer: Ooops, you can't start this instance more than once"); 34 | } 35 | InetSocketAddress addr = new InetSocketAddress("0.0.0.0", 0); 36 | thisServer = HttpServer.create(addr, 10); 37 | MyHttpHandler[] handlers = new MyHttpHandler[]{ 38 | new ExitHandler(), new RootHandler(), //order matters 39 | }; 40 | StringBuffer sb = new StringBuffer(); 41 | for (MyHttpHandler httpHandler : handlers) { 42 | sb.append("") 45 | .append(httpHandler.getContext().substring(1)) 46 | .append("
"); 47 | thisServer.createContext(httpHandler.getContext(), httpHandler); 48 | } 49 | TheServerRootPage = sb.toString(); 50 | System.out.println("ExampleAlreadyInstrumentedHttpServer: Starting new webservice on port " + thisServer.getAddress().getPort()); 51 | thisServer.start(); 52 | TheServerInstance = thisServer; 53 | } 54 | 55 | public void stop() { 56 | thisServer.stop(1); 57 | try { 58 | Thread.sleep(2000L); 59 | } catch (InterruptedException e1) { 60 | } 61 | TheServerInstance = null; 62 | } 63 | 64 | @Override 65 | public void blockUntilReady() { 66 | while (TheServerInstance == null) { 67 | try { 68 | Thread.sleep(1L); 69 | } catch (InterruptedException e) { 70 | // do nothing, just enter the next sleep 71 | } 72 | } 73 | } 74 | 75 | @Override 76 | public void blockUntilStopped() { 77 | while (TheServerInstance != null) { 78 | try { 79 | Thread.sleep(1L); 80 | } catch (InterruptedException e) { 81 | // do nothing, just enter the next sleep 82 | } 83 | } 84 | } 85 | 86 | abstract static class MyHttpHandler implements HttpHandler { 87 | public abstract String getContext(); 88 | 89 | public abstract void myHandle(HttpExchange t) throws Exception; 90 | 91 | public void handle(HttpExchange t) { 92 | try { 93 | myHandle(t); 94 | } catch (Exception e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | 99 | } 100 | 101 | public static class RootHandler extends MyHttpHandler { 102 | @Override 103 | public String getContext() { 104 | return "/"; 105 | } 106 | 107 | public void myHandle(HttpExchange t) throws IOException { 108 | String response = TheServerRootPage; 109 | t.sendResponseHeaders(200, response.length()); 110 | OutputStream os = t.getResponseBody(); 111 | os.write(response.getBytes()); 112 | os.close(); 113 | } 114 | } 115 | 116 | public static class ExitHandler extends MyHttpHandler { 117 | 118 | private static final int STOP_TIME = 3; 119 | 120 | @Override 121 | public String getContext() { 122 | return "/exit"; 123 | } 124 | 125 | @Override 126 | public void myHandle(HttpExchange t) throws IOException { 127 | String response = TheServerRootPage; 128 | t.sendResponseHeaders(200, response.length()); 129 | OutputStream os = t.getResponseBody(); 130 | os.write(response.getBytes()); 131 | os.close(); 132 | TheServerInstance.stop(STOP_TIME); 133 | TheServerInstance = null; 134 | } 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /plugin/src/test/java/co/elastic/apm/example/webserver/ExampleHttpServerInstrumentationIT.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver; 2 | 3 | import co.elastic.apm.example.webserver.plugin.ExampleHttpServerInstrumentation; 4 | import co.elastic.apm.plugin.AbstractInstrumentationTest; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import io.opentelemetry.api.GlobalOpenTelemetry; 7 | import io.opentelemetry.api.trace.Tracer; 8 | import org.junit.jupiter.api.AfterAll; 9 | import org.junit.jupiter.api.BeforeAll; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.io.IOException; 13 | import java.net.URI; 14 | import java.net.http.HttpClient; 15 | import java.net.http.HttpRequest; 16 | import java.net.http.HttpResponse; 17 | import java.time.Duration; 18 | import java.util.List; 19 | import java.util.concurrent.TimeoutException; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | 25 | public class ExampleHttpServerInstrumentationIT extends AbstractInstrumentationTest { 26 | 27 | protected static Exception START_EXCEPTION; 28 | protected static int PORT = -1; 29 | protected static ExampleBasicHttpServer Server; 30 | protected static HttpClient Client = HttpClient.newHttpClient(); 31 | 32 | @BeforeAll 33 | public static void startServer() throws IOException { 34 | Server = new ExampleBasicHttpServer(); 35 | new Thread(() -> { 36 | try { 37 | Server.start(); 38 | } catch (Exception e) { 39 | START_EXCEPTION = e; 40 | } 41 | }).start(); 42 | assertTimeoutPreemptively(Duration.ofSeconds(3), () -> {Server.blockUntilReady();}); 43 | assertTrue(START_EXCEPTION == null); 44 | PORT = Server.getLocalPort(); 45 | } 46 | 47 | @AfterAll 48 | public static void stopServer() throws IOException, InterruptedException { 49 | assertEquals(executeRequest("exit"), 200); 50 | Server.stop(); 51 | assertTimeoutPreemptively(Duration.ofSeconds(8), () -> {Server.blockUntilStopped();}); 52 | } 53 | 54 | @Test 55 | void testInstrumentationMakes1TransactionPerRequestWithCorrectNaming() throws IOException, InterruptedException, TimeoutException { 56 | Tracer elasticTracer = GlobalOpenTelemetry.get().getTracer("ExampleHttpServer"); 57 | for (String request: List.of("nothing", "nothing?withsomething=true", "nothing#somelink", "nothing#somelink?withsomething=true")) { 58 | assertEquals(200, executeRequest(request)); 59 | JsonNode transaction = ApmServer.getAndRemoveTransaction(0, 1000); 60 | assertEquals("GET /nothing", transaction.get("name").asText()); 61 | assertEquals(0, ApmServer.getTransactionCount()); 62 | } 63 | } 64 | 65 | private static int executeRequest(String req) throws IOException, InterruptedException { 66 | HttpRequest request = HttpRequest.newBuilder() 67 | .uri(URI.create("http://localhost:"+PORT+"/"+req)) 68 | .GET() // GET is default 69 | .build(); 70 | 71 | HttpResponse response = Client.send(request, 72 | HttpResponse.BodyHandlers.ofString()); 73 | return response.statusCode(); 74 | } 75 | 76 | @Test 77 | void testInstrumentationIncrementsTheOtelPageCounterMetric() throws IOException, InterruptedException, TimeoutException { 78 | testInstrumentationIncrementsThePageCounterMetrics("page_views", 5000L); 79 | } 80 | 81 | @Test 82 | void testInstrumentationIncrementsTheMicrometerPageCounterMetric() throws IOException, InterruptedException, TimeoutException { 83 | //Although we've set the metrics to be sent every second, micrometer itself is not synchronous, 84 | //so it can take a while for micrometer to update the metric. So the test is set to run for up to 50 seconds 85 | testInstrumentationIncrementsThePageCounterMetrics("page_counter", 50000L); 86 | } 87 | 88 | void testInstrumentationIncrementsThePageCounterMetrics(String metricName, long timeoutInMillis) throws IOException, InterruptedException, TimeoutException { 89 | assertEquals(200, executeRequest("random_with_"+metricName)); 90 | JsonNode transaction = ApmServer.getAndRemoveTransaction(0, 1000); 91 | assertEquals("GET /random_with_"+metricName, transaction.get("name").asText()); 92 | 93 | JsonNode metricset = ApmServer.popMetricset(5000); 94 | boolean foundPageCountMetric = false; 95 | boolean foundNonZeroPageCountMetric = false; 96 | long start = System.currentTimeMillis(); 97 | for(long now = start; metricset != null && now-start < timeoutInMillis; now = System.currentTimeMillis()) { 98 | if (metricset.get("samples") != null && metricset.get("samples").get(metricName) != null) { 99 | foundPageCountMetric = true; 100 | int pageCountValue = metricset.get("samples").get(metricName).get("value").intValue(); 101 | if (pageCountValue > 0) { 102 | foundNonZeroPageCountMetric = true; 103 | break; 104 | } 105 | } 106 | metricset = ApmServer.popMetricset(5000); 107 | } 108 | assertEquals(true, foundPageCountMetric); 109 | assertEquals(true, foundNonZeroPageCountMetric); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /application/src/main/java/co/elastic/apm/example/webserver/ExampleBasicHttpServer.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.io.PrintWriter; 7 | import java.net.ServerSocket; 8 | import java.net.Socket; 9 | 10 | /** 11 | * Really basic single-threaded HTTP server only really useful for 12 | * just this example as an HTTP server to instrument. Don't use this 13 | * for anything else is my advice. Can only handle 1 connection at 14 | * a time, the start() call is blocking, doesn't keep-alive, no SSL, 15 | * no compression, only returns a hardcoded page, will break on 16 | * large requests, so incredibly limited! 17 | *

18 | * It's also really verbose and uses System.out instead of logging 19 | *

20 | * Calling /exit as the URL path will terminate it, any other path 21 | * returns the same page in `TheHtmlPage` 22 | */ 23 | public class ExampleBasicHttpServer implements ExampleHttpServer { 24 | private static final String TheHttpHeader = "HTTP/1.0 200 OK\nContent-Type: text/html; charset=utf-8\nServer: ExampleHttpServer\n\n"; 25 | private static final String TheHtmlPage = "ExampleHttpServerNothing Here"; 26 | 27 | private volatile ServerSocket server; 28 | private volatile boolean isReady = false; 29 | 30 | @Override 31 | public void blockUntilReady() { 32 | while (!this.isReady) { 33 | try { 34 | Thread.sleep(1L); 35 | } catch (InterruptedException e) { 36 | // do nothing, just enter the next sleep 37 | } 38 | } 39 | } 40 | 41 | @Override 42 | public void blockUntilStopped() { 43 | while (this.server != null && !this.server.isClosed()) { 44 | try { 45 | Thread.sleep(1L); 46 | } catch (InterruptedException e) { 47 | // do nothing, just enter the next sleep 48 | } 49 | } 50 | } 51 | 52 | @Override 53 | public void stop() { 54 | try { 55 | if (this.server != null) { 56 | System.out.println("ExampleHttpServer: Attempting to call stop()"); 57 | this.server.close(); 58 | System.out.println("ExampleHttpServer: Successfully called stop()"); 59 | } else { 60 | System.out.println("ExampleHttpServer: Attempted to call stop() on a server that was never start() successfully!"); 61 | } 62 | } catch (IOException e) { 63 | System.out.println("ExampleHttpServer: Unsuccessfully called stop(), stack trace follows, error is:" + e.getLocalizedMessage()); 64 | e.printStackTrace(System.out); 65 | } 66 | } 67 | 68 | @Override 69 | public synchronized void start() throws IOException { 70 | System.out.println("ExampleHttpServer: Attempting to call start()"); 71 | if (this.server != null) { 72 | throw new IOException("ExampleHttpServer: Ooops, you can't start this instance more than once"); 73 | } 74 | this.server = new ServerSocket(0); 75 | System.out.println("ExampleHttpServer: Successfully called start(), now listening for requests"); 76 | boolean keepGoing = true; 77 | while (keepGoing) { 78 | this.isReady = true; 79 | try (Socket client = this.server.accept()) { 80 | keepGoing = processClient(client); 81 | } 82 | } 83 | stop(); 84 | } 85 | 86 | @Override 87 | public int getLocalPort() { 88 | return this.server == null ? -1 : this.server.getLocalPort(); 89 | } 90 | 91 | private boolean processClient(Socket client) { 92 | boolean keepGoing = true; 93 | System.out.println("ExampleHttpServer: Received a client connection, now attempting to read the request"); 94 | while (!client.isClosed() && !client.isInputShutdown() && !client.isOutputShutdown()) { 95 | try (BufferedReader clientInput = new BufferedReader(new InputStreamReader(client.getInputStream()))) { 96 | String line = clientInput.readLine(); 97 | if (line == null) { 98 | //hmmm, try again 99 | try { 100 | Thread.sleep(10); 101 | } catch (InterruptedException e) { 102 | } 103 | line = clientInput.readLine(); 104 | if (line == null) { 105 | clientInput.close(); 106 | break; 107 | } 108 | } 109 | if (line.startsWith("GET /exit")) { 110 | keepGoing = false; 111 | } 112 | PrintWriter outputToClient = new PrintWriter(client.getOutputStream()); 113 | handleRequest(line, clientInput, outputToClient); 114 | clientInput.close(); 115 | outputToClient.close(); 116 | } catch (IOException e) { 117 | e.printStackTrace(); 118 | } 119 | } 120 | return keepGoing; 121 | } 122 | 123 | private void handleRequest(String request, BufferedReader clientInput, PrintWriter outputToClient) throws IOException { 124 | System.out.println("ExampleHttpServer: HTTP-HEADER: " + request); 125 | String line; 126 | while ((line = clientInput.readLine()) != null && line.length() != 0) { 127 | System.out.println("ExampleHttpServer: HTTP-HEADER: " + line); 128 | } 129 | System.out.println("ExampleHttpServer: Now replying the standard page and terminating the connection"); 130 | outputToClient.println(TheHttpHeader); 131 | outputToClient.println(TheHtmlPage); 132 | outputToClient.flush(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apm-agent-java-plugin-example 2 | Example of instrumentation which plugs in to the Elastic Java Agent and instruments an application 3 | 4 | ## Overview 5 | 6 | The [Elastic APM Java Agent](https://github.com/elastic/apm-agent-java/) is a Java agent that automatically measures the performance of your application and tracks errors. [Full documentation is available](https://www.elastic.co/guide/en/apm/agent/java/current/intro.html). 7 | 8 | This project provides a detailed example of using the Elastic APM Java Plugin API to add custom instrumentation in to the agent, which the agent will automatically apply without the need for users to touch the target application's code. 9 | 10 | Detailed articles on [creating the instrumentation](https://www.elastic.co/blog/create-your-own-instrumentation-with-the-java-agent-plugin) and on [regression testing it](https://www.elastic.co/blog/create-your-own-instrumentation-with-the-java-agent-plugin) are available 11 | 12 | ## Sub-projects 13 | 14 | This project has two sub-projects 15 | 16 | * application - a standalone runnable example application that is here purely to provide an example target to instrument 17 | * plugin - the plugin that instruments the application 18 | 19 | ## Application sub-project 20 | 21 | The application consists of a webserver and a client that executes some requests against the webserver. A [webserver interface](application/src/main/java/co/elastic/apm/example/webserver/ExampleHttpServer.java) and two implementations are provided: 22 | 23 | * [ExampleAlreadyInstrumentedHttpServer](application/src/main/java/co/elastic/apm/example/webserver/ExampleAlreadyInstrumentedHttpServer.java) uses the `com.sun.net.httpserver.HttpServer` that is a standard part of the JDK to implement the webserver interface; the Elastic APM Java Agent already automatically instruments this technology, so this implementation is provided as a reference for checking logging and output 24 | * [ExampleBasicHttpServer](application/src/main/java/co/elastic/apm/example/webserver/ExampleBasicHttpServer.java) implements a very restricted custom webserver, to provide a target for the custom instrumentation 25 | 26 | The [ExampleClient](application/src/main/java/co/elastic/apm/example/webserver/ExampleClient.java) provides an application entry point that, when run, will start the selected webserver (chosen by setting the property `elastic.apm.service_name`), and send it some requests before terminating. 27 | 28 | Note the application implementation is deliberately simple (eg System.out instead of a logging framework) to keep it as easy to understand as possible. 29 | 30 | ## Plugin sub-project 31 | 32 | The plugin consists of [one file](plugin/src/main/java/co/elastic/apm/example/webserver/plugin/ExampleHttpServerInstrumentation.java) holding the custom tracing instrumentation, [one file](plugin/src/main/java/co/elastic/apm/example/webserver/plugin/ExampleMetricsInstrumentation.java) holding custom metrics instrumentation, several classes for regression testing, and a pom that builds the correct plugin jar. The details of the plugin project are explained in the articles [creating the instrumentation](https://www.elastic.co/blog/create-your-own-instrumentation-with-the-java-agent-plugin) and [regression testing it](https://www.elastic.co/blog/create-your-own-instrumentation-with-the-java-agent-plugin). 33 | 34 | ## Building 35 | 36 | The full project can be built by cloning to your local system, changing to the root directory, and running `mvn clean install`. 37 | 38 | Prerequisites: git, maven and JDK 11+ installed 39 | 40 | ```aidl 41 | git clone https://github.com/elastic/apm-agent-java-plugin-example.git 42 | cd apm-agent-java-plugin-example 43 | mvn clean install 44 | ``` 45 | 46 | Each sub-project can also be separately built the same way (changing to the sub-project root directory and running `mvn clean install`). 47 | 48 | ## Running 49 | 50 | You need an Elastic APM Java Agent jar (the latest version is recommended, but at least version 1.31.0 to instrument traces and at least 1.39.0 to instrument metrics). Additionally an Elastic APM server is recommended, though not required (communications to the server will be dropped if it's unavailable). 51 | 52 | The full set of example run instructions below are also available as a batch script in the project root directory, in file runExamples.bash/runExamples.bat 53 | 54 | The latest agent version can be found in [Maven](https://search.maven.org/search?q=g:co.elastic.apm%20AND%20a:elastic-apm-agent). You can download using any of the standard download mechanisms, eg 55 | 56 | ```aidl 57 | wget https://repo1.maven.org/maven2/co/elastic/apm/elastic-apm-agent/1.32.0/elastic-apm-agent-1.32.0.jar 58 | ``` 59 | 60 | ### Standalone Application, no agent, no plugin 61 | 62 | You can run the standalone application, *with no agent*, from the project root directory as follows 63 | 64 | ```aidl 65 | java -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 66 | java -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 67 | ``` 68 | 69 | ### Application with agent, no plugin 70 | 71 | Assuming you have set the jar location in `AGENT_JAR` and if available the APM server is specified in `APM_SERVER_URL` and `APM_SECRET_TOKEN`, you can run the application with agent *but no plugin*, from the project root directory, as follows 72 | 73 | ```aidl 74 | export AGENT_JAR=... 75 | export APM_SERVER_URL=... 76 | export APM_SECRET_TOKEN=... 77 | 78 | java -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 79 | java -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 80 | ``` 81 | 82 | ### Application with agent and plugin 83 | 84 | Assuming you have set the jar location in `AGENT_JAR` and if available the APM server is specified in `APM_SERVER_URL` and `APM_SECRET_TOKEN`, you can run the application with agent and plugin, from the project root directory, as follows 85 | 86 | ```aidl 87 | export AGENT_JAR=... 88 | export APM_SERVER_URL=... 89 | export APM_SECRET_TOKEN=... 90 | 91 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin/target -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleAlreadyInstrumentedHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 92 | java -Delastic.apm.enable_experimental_instrumentations=true -Delastic.apm.plugins_dir=plugin/target -Delastic.apm.server_url=$APM_SERVER_URL -Delastic.apm.secret_token=$APM_SECRET_TOKEN -javaagent:$AGENT_JAR -Delastic.apm.log_level=DEBUG -Delastic.apm.service_name=ExampleClient-ExampleBasicHttpServer -cp application/target/application-*.jar co.elastic.apm.example.webserver.ExampleClient 93 | ``` 94 | -------------------------------------------------------------------------------- /plugin/src/test/java/co/elastic/apm/mock/MockApmServer.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.mock; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.sun.net.httpserver.HttpExchange; 7 | import com.sun.net.httpserver.HttpHandler; 8 | import com.sun.net.httpserver.HttpServer; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | import java.net.InetSocketAddress; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.concurrent.TimeoutException; 18 | 19 | /** 20 | * This is a server which just accepts lines of JSON code and if the JSON 21 | * is valid and the root node is "transaction", then adds that JSON object 22 | * to a transaction list which is accessible externally to the class. 23 | * 24 | * The Elastic agent sends lines of JSON code, and so this mock server 25 | * can be used as a basic APM server for testing. 26 | * 27 | * The HTTP server used is the JDK embedded com.sun.net.httpserver 28 | */ 29 | public class MockApmServer { 30 | /** 31 | * Simple main that starts a mock APM server, prints the port it is 32 | * running on, and exits after 2_000 seconds. This is not needed 33 | * for testing, it is just a convenient template for trying things out 34 | * if you want play around. 35 | */ 36 | public static void main(String[] args) throws IOException, InterruptedException { 37 | MockApmServer server = new MockApmServer(); 38 | System.out.println(server.start()); 39 | server.blockUntilReady(); 40 | Thread.sleep(2_000_000L); 41 | server.stop(); 42 | server.blockUntilStopped(); 43 | } 44 | 45 | private static volatile HttpServer TheServerInstance; 46 | 47 | private final List transactions = new ArrayList<>(); 48 | private final List metricsets = new ArrayList<>(); 49 | 50 | /** 51 | * A count of the number of transactions received and not yet removed 52 | * @return the number of transactions received and not yet removed 53 | */ 54 | public int getTransactionCount() { 55 | synchronized (transactions) { 56 | return transactions.size(); 57 | } 58 | } 59 | 60 | /** 61 | * Gets the transaction at index i if it exists within the timeout 62 | * specified, and removes it from the transaction list. 63 | * If it doesn't exist within the timeout period, an 64 | * IllegalArgumentException is thrown 65 | * @param i - the index to retrieve a transaction from 66 | * @param timeOutInMillis - millisecond timeout to wait for the 67 | * transaction at index i to exist 68 | * @return - the transaction information as a JSON object 69 | * @throws TimeoutException - thrown if no transaction 70 | * exists at index i by timeout 71 | */ 72 | public JsonNode getAndRemoveTransaction(int i, long timeOutInMillis) throws TimeoutException { 73 | //because the agent writes to the server asynchronously, 74 | //any transaction created in a client is not here immediately 75 | long start = System.currentTimeMillis(); 76 | long elapsedTime = 0; 77 | while (elapsedTime < timeOutInMillis) { 78 | synchronized (transactions) { 79 | if (transactions.size() > i) { 80 | break; 81 | } 82 | if (timeOutInMillis-elapsedTime > 0) { 83 | try { 84 | transactions.wait(timeOutInMillis - elapsedTime); 85 | } catch (InterruptedException e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | elapsedTime = System.currentTimeMillis() - start; 90 | } 91 | } 92 | synchronized (transactions) { 93 | if (transactions.size() <= i) { 94 | throw new TimeoutException("The apm server does not have a transaction at index " + i); 95 | } 96 | } 97 | synchronized (transactions) { 98 | return transactions.remove(i); 99 | } 100 | } 101 | 102 | public JsonNode popMetricset(long timeOutInMillis) throws TimeoutException { 103 | //because the agent writes to the server asynchronously, 104 | //any metricset created in a client is not here immediately 105 | long start = System.currentTimeMillis(); 106 | long elapsedTime = 0; 107 | while (elapsedTime < timeOutInMillis) { 108 | synchronized (metricsets) { 109 | if (metricsets.size() > 0) { 110 | break; 111 | } 112 | if (timeOutInMillis-elapsedTime > 0) { 113 | try { 114 | metricsets.wait(timeOutInMillis - elapsedTime); 115 | } catch (InterruptedException e) { 116 | e.printStackTrace(); 117 | } 118 | } 119 | elapsedTime = System.currentTimeMillis() - start; 120 | } 121 | } 122 | if (timeOutInMillis-elapsedTime <= 0) { 123 | return null; 124 | } 125 | synchronized (metricsets) { 126 | return metricsets.remove(0); 127 | } 128 | } 129 | 130 | /** 131 | * Start the Mock APM server. Just returns empty JSON structures for every incoming message 132 | * @return - the port the Mock APM server started on 133 | * @throws IOException 134 | */ 135 | public synchronized int start() throws IOException { 136 | if (TheServerInstance != null) { 137 | throw new IOException("MockApmServer: Ooops, you can't start this instance more than once"); 138 | } 139 | InetSocketAddress addr = new InetSocketAddress("0.0.0.0", 0); 140 | HttpServer server = HttpServer.create(addr, 10); 141 | server.createContext("/exit", new ExitHandler()); 142 | server.createContext("/", new RootHandler()); 143 | 144 | server.start(); 145 | TheServerInstance = server; 146 | System.out.println("MockApmServer started on port "+server.getAddress().getPort()); 147 | return server.getAddress().getPort(); 148 | } 149 | 150 | /** 151 | * Stop the server gracefully if possible 152 | */ 153 | public synchronized void stop() { 154 | TheServerInstance.stop(1); 155 | TheServerInstance = null; 156 | } 157 | 158 | class RootHandler implements HttpHandler { 159 | public void handle(HttpExchange t) { 160 | try { 161 | InputStream body = t.getRequestBody(); 162 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 163 | byte[] buffer = new byte[8*1024]; 164 | int lengthRead; 165 | while((lengthRead = body.read(buffer)) > 0) { 166 | bytes.write(buffer, 0, lengthRead); 167 | } 168 | reportTransactionsAndMetrics(bytes.toString()); 169 | String response = "{}"; 170 | t.sendResponseHeaders(200, response.length()); 171 | OutputStream os = t.getResponseBody(); 172 | os.write(response.getBytes()); 173 | os.close(); 174 | } catch (Exception e) { 175 | e.printStackTrace(); 176 | } 177 | } 178 | 179 | private void reportTransactionsAndMetrics(String json) { 180 | String[] lines = json.split("[\r\n]"); 181 | for (String line: lines) { 182 | reportTransactionOrMetric(line); 183 | } 184 | } 185 | private void reportTransactionOrMetric(String line) { 186 | System.out.println("MockApmServer reading JSON objects: "+ line); 187 | ObjectMapper objectMapper = new ObjectMapper(); 188 | JsonNode messageRootNode = null; 189 | try { 190 | messageRootNode = objectMapper.readTree(line); 191 | JsonNode transactionNode = messageRootNode.get("transaction"); 192 | if (transactionNode != null) { 193 | synchronized (transactions) { 194 | transactions.add(transactionNode); 195 | transactions.notify(); 196 | } 197 | } 198 | JsonNode metricsetNode = messageRootNode.get("metricset"); 199 | if (metricsetNode != null) { 200 | synchronized (metricsets) { 201 | metricsets.add(metricsetNode); 202 | metricsets.notify(); 203 | } 204 | } 205 | } catch (JsonProcessingException e) { 206 | System.out.println("Not JSON: "+line); 207 | e.printStackTrace(); 208 | } 209 | } 210 | } 211 | 212 | static class ExitHandler implements HttpHandler { 213 | private static final int STOP_TIME = 3; 214 | 215 | public void handle(HttpExchange t) { 216 | try { 217 | InputStream body = t.getRequestBody(); 218 | String response = "{}"; 219 | t.sendResponseHeaders(200, response.length()); 220 | OutputStream os = t.getResponseBody(); 221 | os.write(response.getBytes()); 222 | os.close(); 223 | TheServerInstance.stop(STOP_TIME); 224 | TheServerInstance = null; 225 | } catch (Exception e) { 226 | e.printStackTrace(); 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Wait until the server is ready to accept messages 233 | */ 234 | public void blockUntilReady() { 235 | while (TheServerInstance == null) { 236 | try { 237 | Thread.sleep(1L); 238 | } catch (InterruptedException e) { 239 | // do nothing, just enter the next sleep 240 | } 241 | } 242 | } 243 | 244 | /** 245 | * Wait until the server is terminated 246 | */ 247 | public void blockUntilStopped() { 248 | while (TheServerInstance != null) { 249 | try { 250 | Thread.sleep(1L); 251 | } catch (InterruptedException e) { 252 | // do nothing, just enter the next sleep 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /plugin/src/main/java/co/elastic/apm/example/webserver/plugin/ExampleHttpServerInstrumentation.java: -------------------------------------------------------------------------------- 1 | package co.elastic.apm.example.webserver.plugin; 2 | 3 | import static net.bytebuddy.matcher.ElementMatchers.named; 4 | import static net.bytebuddy.matcher.ElementMatchers.takesArgument; 5 | import static net.bytebuddy.matcher.ElementMatchers.takesArguments; 6 | 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | 10 | import co.elastic.apm.agent.sdk.ElasticApmInstrumentation; 11 | import io.opentelemetry.api.GlobalOpenTelemetry; 12 | import io.opentelemetry.api.trace.Span; 13 | import io.opentelemetry.api.trace.SpanKind; 14 | import io.opentelemetry.api.trace.StatusCode; 15 | import io.opentelemetry.api.trace.Tracer; 16 | import io.opentelemetry.context.Scope; 17 | import net.bytebuddy.asm.Advice; 18 | import net.bytebuddy.description.method.MethodDescription; 19 | import net.bytebuddy.description.type.TypeDescription; 20 | import net.bytebuddy.matcher.ElementMatcher; 21 | 22 | /** 23 | * This class basically implements that: if the class 24 | * `co.elastic.apm.example.webserver.ExampleBasicHttpServer` 25 | * gets loaded, and it has any methods matching the signature 26 | * `handleRequest(String,x,y)` (return type doesn't matter, 27 | * nor do the types of the 2nd and 3rd argument), then it 28 | * will instrument that `handleRequest` method to add a Span 29 | * which starts at method entry and finishes at method exit. 30 | * 31 | * The Elastic Java agent finds this class as follows: 32 | * 1. It looks for the `plugins` directory which is either 33 | * 1a. in the agent home directory (where the agent jar file was placed), or 34 | * 1b. https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-plugins-dir 35 | * 2. Every jar in the `plugins` directory is scanned for the file 36 | * 2a. META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation 37 | * 3. If that file exists, then each line in the file is assumed to be a 38 | * fully qualified classname subclassed from ElasticApmInstrumentation 39 | * and present in the jar 40 | * 4. Each such class is loaded and used to instrument the loaded code 41 | */ 42 | public class ExampleHttpServerInstrumentation extends ElasticApmInstrumentation { 43 | 44 | /** 45 | * This instrumentation will get triggered when both the Elastic Java agent 46 | * is loaded, and the class `ExampleBasicHttpServer` is loaded 47 | * (ie the class could load before the agent, agent before class, and 48 | * this instrumentation still gets triggered when they are both loaded). 49 | * 50 | * This implementation looks only for this class name, but you can 51 | * be much more flexible, looking for subclasses, or interface implementations 52 | * or even multiple alternatives of these, and other things too 53 | */ 54 | @Override 55 | public ElementMatcher getTypeMatcher() { 56 | return named("co.elastic.apm.example.webserver.ExampleBasicHttpServer"); 57 | } 58 | 59 | /** 60 | * This looks for the method signature `handleRequest(String,x,y)` 61 | * for the classes identified by {@code getTypeMatcher}. You could also 62 | * specify the return type and other aspects of the method signature 63 | */ 64 | @Override 65 | public ElementMatcher getMethodMatcher() { 66 | return named("handleRequest").and(takesArguments(3)) 67 | .and(takesArgument(0, named("java.lang.String"))); 68 | } 69 | 70 | /** 71 | * This name `elastic-plugin-example` can be used to disable the instrumentation using 72 | * https://www.elastic.co/guide/en/apm/agent/java/current/config-core.html#config-disable-instrumentations 73 | * 74 | * @return A list of String names that can be used to disable this instrumentation 75 | */ 76 | @Override 77 | public Collection getInstrumentationGroupNames() { 78 | return Collections.singletonList("elastic-plugin-example"); 79 | } 80 | 81 | /** 82 | * It's not necessary to override this, as the default implementation is to 83 | * return this instrumentation class name with "$AdviceClass" appended. But I've 84 | * implemented this override so that it's obvious here how the instrumentation 85 | * joins up with the advice inner class below 86 | * 87 | * @return the name of the Advice class that implements the instrumentation 88 | * for the class+methods matched by this Instrumentation class 89 | */ 90 | @Override 91 | public String getAdviceClassName() { 92 | return "co.elastic.apm.example.webserver.plugin.ExampleHttpServerInstrumentation$AdviceClass"; 93 | } 94 | 95 | /** 96 | * The advice class is applied when the instrumentation identifies 97 | * it needs to be applied, ie when the above matchers ({@code getTypeMatcher} 98 | * and {@code getMethodMatcher}) have been matched 99 | * 100 | * The OpenTelemetry pattern for wrapping a method in a span is 101 | *

102 |      *   Span span = tracer.spanBuilder(spanName).startSpan();
103 |      *   try (Scope scope = span.makeCurrent()) {
104 |      *     methodBeingWrapped(...);
105 |      *   } catch(Throwable thrown){
106 |      *     span.setStatus(StatusCode.ERROR);
107 |      *     span.recordException(thrown);
108 |      *     throw t;
109 |      *   } finally {
110 |      *     span.end();
111 |      *   }
112 |      * 
113 | * 114 | * The way we instrument to get that is to break up the code into the 115 | * part before the method being called, and the part after the method is 116 | * finished. Then we have the first run at method entry, and the latter 117 | * at method exit. 118 | */ 119 | public static class AdviceClass { 120 | /** 121 | * At method entry we want to create & start the Span, and make the scope current. 122 | * We use the ByteBuddy advice annotation `OnMethodEnter` to say this method 123 | * is executed at method entry. 124 | *
    125 | *
  • the method must have a `public static` signature 126 | *
  • the method can return an object or be void 127 | *
  • the method name (here `onEnterHandle`) can be any valid method name 128 | *
  • `@Advice.Argument(0) String requestLine` lets us use `requestLine` holding 129 | * the value of the first String parameter of ExampleBasicHttpServer.handleRequest() 130 | *
  • `suppress` means that if any Throwable exception is thrown while the method runs, 131 | * that exception will be suppressed (not thrown by ExampleBasicHttpServer.handleRequest() 132 | * nor make it exit early) 133 | *
  • `inline` `false` means that the code in `onEnterHandle()` will not be inlined into 134 | * `ExampleBasicHttpServer.handleRequest()`, instead `ExampleHttpServerInstrumentation$AdviceClass.onEnterHandle()` 135 | * will be called on entry of `ExampleBasicHttpServer.handleRequest()` 136 | *
137 | * 138 | * @return the Scope object so that the `OnMethodExit` {@code onExitHandle} method can get that 139 | * object and close it. This is best practice for scope handling 140 | */ 141 | @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) 142 | public static Object onEnterHandle(@Advice.Argument(0) String requestLine) { 143 | //ExampleBasicHttpServer.handleRequest() has the full HTTP line for a request 144 | //so first we'll strip off the part after the URI request path, ie 145 | // requestLine = "GET /something?y#x HTTP/1.1" 146 | // fullRequest = "GET /something?y#x" 147 | String fullRequest = requestLine.substring(0, requestLine.indexOf(" HTTP")); 148 | //We'll just use the base URI path for the name, ie 149 | // fullRequest = "GET /something?y#x" 150 | // request = "GET /something" 151 | // this is so that we have a lower cardinality name, essential 152 | // for good indexing and composition 153 | String request = basicRequestPath(fullRequest); 154 | //Support ignoring some subset of requests 155 | if (shouldIgnoreThisRequest(request)) { 156 | return null; 157 | } 158 | // This is the recommended way to obtain the tracer with the Elastic OpenTelemetry bridge 159 | Tracer tracer = GlobalOpenTelemetry.get().getTracer("ExampleHttpServer"); 160 | Span span = tracer.spanBuilder(request).setSpanKind(SpanKind.SERVER).startSpan(); 161 | //return the scope object so that it can be closed in the OnMethodExit method 162 | return span.makeCurrent(); 163 | } 164 | 165 | private static String basicRequestPath(String request) { 166 | int index = request.indexOf("?"); 167 | if (index > 0) { 168 | request = request.substring(0, index); 169 | } 170 | index = request.indexOf("#"); 171 | if (index > 0) { 172 | request = request.substring(0, index); 173 | } 174 | return request; 175 | } 176 | 177 | /** 178 | * A more complete implementation would ignore the request if it 179 | * is a type that the various configuration options say to ignore: 180 | * https://www.elastic.co/guide/en/apm/agent/java/current/config-http.html#config-transaction-ignore-urls 181 | * https://www.elastic.co/guide/en/apm/agent/java/current/config-http.html#config-transaction-ignore-user-agents 182 | * 183 | * @param request the HTTP request being processed 184 | * @return false if this request should be traced, otherwise true 185 | */ 186 | private static boolean shouldIgnoreThisRequest(String request) { 187 | return false; 188 | } 189 | 190 | /** 191 | * At method exit we want to end the Span, capture any exception, and close the scope. 192 | * We use the ByteBuddy advice annotation `OnMethodExit` to say that this method 193 | * will be executed at method exit. 194 | *
    195 | *
  • the method must have a `public static` signature 196 | *
  • the method can return an object or be void 197 | *
  • the method name (here `onExitHandle`) can be any valid method name 198 | *
  • `(@Advice.Thrown Throwable thrown` lets us use `thrown` as the value 199 | * of any exception thrown by `ExampleBasicHttpServer.handleRequest()` - it 200 | * has a null value if no exception was thrown 201 | *
  • `@Advice.Enter Object scopeObject` lets us use `scopeObject` holding 202 | * the value of the Scope object returned from the {@code onEnterHandle} method 203 | *
  • `suppress` means that if any Throwable exception is thrown while the method runs, 204 | * that exception will be suppressed (not thrown by `ExampleBasicHttpServer.handleRequest()` 205 | * nor make it exit early) 206 | *
  • `onThrowable` tells ByteBuddy that we want this onExitHandle() method to be called 207 | * even if any Throwable is thrown by the `ExampleBasicHttpServer.handleRequest()` 208 | *
  • `inline` `false` means that the code in `onEnterHandle()` will not be inlined into 209 | * `ExampleBasicHttpServer.handleRequest()`, instead `ExampleHttpServerInstrumentation$AdviceClass.onEnterHandle()` 210 | * will be called on entry of `ExampleBasicHttpServer.handleRequest()` 211 | *
212 | * 213 | * @param thrown - any exception thrown from `ExampleBasicHttpServer.handleRequest()` 214 | * @param scopeObject - the Scope object returned from {@code onEnterHandle} 215 | */ 216 | @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class, inline = false) 217 | public static void onExitHandle(@Advice.Thrown Throwable thrown, @Advice.Enter Object scopeObject) { 218 | //Use a defensive implementation - nothing 219 | //that might fail will prevent anything else 220 | try { 221 | Span span = Span.current(); 222 | try { 223 | if (thrown != null) { 224 | span.setStatus(StatusCode.ERROR); 225 | span.recordException(thrown); 226 | } 227 | } finally { 228 | span.end(); 229 | } 230 | } finally{ 231 | Scope scope = (Scope) scopeObject; 232 | scope.close(); 233 | } 234 | } 235 | } 236 | } 237 | --------------------------------------------------------------------------------