├── 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 super TypeDescription> getTypeMatcher() {
30 | return named("co.elastic.apm.example.webserver.ExampleBasicHttpServer");
31 | }
32 |
33 | @Override
34 | public ElementMatcher super MethodDescription> 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 super TypeDescription> getTypeMatcher() {
34 | return named("co.elastic.apm.example.webserver.ExampleBasicHttpServer");
35 | }
36 |
37 | @Override
38 | public ElementMatcher super MethodDescription> 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 super TypeDescription> 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 super MethodDescription> 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 | *
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 | *