Skip to content

Commit 56c824b

Browse files
authored
add runtime attach (#589)
1 parent 495d1f5 commit 56c824b

File tree

11 files changed

+237
-4
lines changed

11 files changed

+237
-4
lines changed

agent/entrypoint/src/main/java/co/elastic/otel/agent/ElasticAgent.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,34 @@
2424
/** Elastic agent entry point, delegates to OpenTelemetry agent */
2525
public class ElasticAgent {
2626

27-
@SuppressWarnings("unused")
27+
/**
28+
* Entry point for -javaagent JVM argument attach
29+
*
30+
* @param agentArgs agent arguments
31+
* @param inst instrumentation
32+
*/
2833
public static void premain(String agentArgs, Instrumentation inst) {
2934
OpenTelemetryAgent.premain(agentArgs, inst);
3035
}
3136

37+
/**
38+
* Entry point for runtime attach
39+
*
40+
* @param agentArgs agent arguments
41+
* @param inst instrumentation
42+
*/
43+
public static void agentmain(String agentArgs, Instrumentation inst) {
44+
OpenTelemetryAgent.agentmain(agentArgs, inst);
45+
}
46+
47+
/**
48+
* Entry point to execute as program
49+
*
50+
* @param args arguments
51+
*/
3252
public static void main(String[] args) {
3353
OpenTelemetryAgent.main(args);
3454
}
55+
56+
private ElasticAgent() {}
3557
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ gcpContribResources = { group = "io.opentelemetry.contrib", name = "opentelemetr
3535
contribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-resource-providers", version.ref = "opentelemetryContribAlpha" }
3636
contribSpanStacktrace = { group = "io.opentelemetry.contrib", name = "opentelemetry-span-stacktrace", version.ref = "opentelemetryContribAlpha" }
3737
contribInferredSpans = { group = "io.opentelemetry.contrib", name = "opentelemetry-inferred-spans", version.ref = "opentelemetryContribAlpha" }
38+
contribRuntimeAttach = { group = "io.opentelemetry.contrib", name = "opentelemetry-runtime-attach-core", version.ref = "opentelemetryContribAlpha" }
3839

3940
opentelemetrySemconv = { group = "io.opentelemetry.semconv", name = "opentelemetry-semconv", version.ref = "opentelemetrySemconv" }
4041
opentelemetrySemconvIncubating = { group = "io.opentelemetry.semconv", name = "opentelemetry-semconv-incubating", version.ref = "opentelemetrySemconvAlpha" }

runtime-attach/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
2+
# EDOT Runtime attach
3+
4+
This feature is currently in **Tech Preview**.
5+
6+
Runtime attach allows to:
7+
8+
- Allows to deploy the agent when access to JVM arguments or configuration is not possible, for example with some managed services
9+
- Agent deployment can be controlled by the application development team, without needing modifications to the run environment
10+
11+
However, it also has some limitations:
12+
13+
- It can't be used with multiple applications that share a JVM, for example with web-applications and application servers, in this case only the `-javaagent` option is supported
14+
- Agent update is tied to the application development and deployment lifecycle.
15+
- It requires minor modification of the application `main` entry point and adding one extra dependency.
16+
- Agent can only be attached at application start, it can't be used to attach later during application runtime.
17+
- Recent JVMs issue [warnings in standard error](#jvm-runtime-attach-warnings) and the feature might require explicit opt-in with JVM settings in the future
18+
19+
## Setup
20+
21+
Adding runtime attach to an application is a 3-step process:
22+
1. add runtime attach to the application dependencies
23+
2. minor code modification of the application `main` method
24+
3. package and re-deploy the application
25+
26+
### runtime attach dependency:
27+
28+
Maven:
29+
```xml
30+
<dependency>
31+
<groupId>co.elastic.otel</groupId>
32+
<artifactId>elastic-otel-runtime-attach</artifactId>
33+
<version>${VERSION}</version>
34+
</dependency>
35+
```
36+
37+
Gradle:
38+
```
39+
implementation("co.elastic.otel:elastic-otel-runtime-attach:${VERSION}")
40+
```
41+
42+
### code modification
43+
44+
A single call to `RuntimeAttach.attachJavaagentToCurrentJvm()` must be added early at the start of the `main` method body.
45+
Here is an example of a simple spring-boot application:
46+
47+
```java
48+
@SpringBootApplication
49+
public class MyApplication {
50+
51+
public static void main(String[] args) {
52+
RuntimeAttach.attachJavaagentToCurrentJvm();
53+
SpringApplication.run(MyApplication.class, args);
54+
}
55+
}
56+
```
57+
58+
## JVM Runtime attach warnings
59+
60+
With recent JVMs, the following warning may be issued in the process standard error output:
61+
62+
```
63+
WARNING: A Java agent has been loaded dynamically (/tmp/otel-agent6227828786286549290/agent.jar)
64+
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
65+
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
66+
WARNING: Dynamic loading of agents will be disallowed by default in a future release
67+
```
68+
69+
This message indicates that the dynamic agent attachment will be disabled by in the future,
70+
adding `-XX:+EnableDynamicAgentLoading` to the JVM arguments (or `JAVA_TOOL_OPTIONS` env variable) will allow to get rid of it.
71+
72+
It is also safe to ignore it until the runtime attach feature is disabled by default in a newer JVM version and the JVM version used to run the application is updated to it.

runtime-attach/build.gradle.kts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
plugins {
2+
id("elastic-otel.java-conventions")
3+
id("elastic-otel.sign-and-publish-conventions")
4+
}
5+
6+
description = "Elastic Distribution of OpenTelemetry Java Agent - runtime attach"
7+
8+
base.archivesName.set("elastic-otel-runtime-attach")
9+
10+
val agent: Configuration by configurations.creating {
11+
isCanBeResolved = true
12+
isCanBeConsumed = false
13+
}
14+
15+
dependencies {
16+
implementation(catalog.contribRuntimeAttach)
17+
agent(project(":agent"))
18+
}
19+
20+
tasks {
21+
jar {
22+
inputs.files(agent)
23+
from({
24+
agent.singleFile
25+
})
26+
rename("^(.*)\\.jar\$", "edot-agent.jar")
27+
}
28+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.otel.agent.attach;
20+
21+
import io.opentelemetry.contrib.attach.core.CoreRuntimeAttach;
22+
23+
/** Provides ability to attach EDOT Java agent to the current JVM at runtime. */
24+
public class RuntimeAttach {
25+
26+
/** Attaches EDOT Java agent to the current JVM, must be called early at application startup */
27+
public static void attachJavaagentToCurrentJvm() {
28+
CoreRuntimeAttach distroRuntimeAttach = new CoreRuntimeAttach("/edot-agent.jar");
29+
distroRuntimeAttach.attachJavaagentToCurrentJvm();
30+
}
31+
32+
private RuntimeAttach() {}
33+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ include("instrumentation:openai-client-instrumentation:instrumentation-0.8")
2323
include("instrumentation:openai-client-instrumentation:instrumentation-0.14")
2424
include("inferred-spans")
2525
include("resources")
26+
include("runtime-attach")
2627
include("smoke-tests")
2728
include("smoke-tests:test-app")
2829
include("smoke-tests:test-app-war")

smoke-tests/src/test/java/com/example/javaagent/smoketest/DynamicConfigSmokeTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ public static void end() {
5252
}
5353

5454
@AfterEach
55-
public void endTest() throws InterruptedException {
55+
public void endTest() {
5656
doRequest(getUrl("/dynamicconfig/reset"), okResponseBody("reset"));
5757
}
5858

5959
@Test
60-
public void flipSending() throws InterruptedException, IOException {
60+
public void flipSending() throws IOException {
6161
doRequest(getUrl("/health"), okResponseBody("Alive!"));
6262
doRequest(getUrl("/dynamicconfig/flipSending"), okResponseBody("stopped"));
6363

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package com.example.javaagent.smoketest;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
24+
import io.opentelemetry.proto.trace.v1.Span;
25+
import java.util.Arrays;
26+
import java.util.List;
27+
import java.util.stream.Collectors;
28+
import org.junit.jupiter.api.AfterAll;
29+
import org.junit.jupiter.api.BeforeAll;
30+
import org.junit.jupiter.api.Test;
31+
32+
public class RuntimeAttachSmokeTest extends TestAppSmokeTest {
33+
34+
@BeforeAll
35+
public static void start() {
36+
startTestApp(
37+
container -> {
38+
String jvmOptions = container.getEnvMap().get("JAVA_TOOL_OPTIONS");
39+
if (jvmOptions != null) {
40+
// remove '-javaagent' from JVM args
41+
jvmOptions =
42+
Arrays.asList(jvmOptions.split(" ")).stream()
43+
.map(String::trim)
44+
.filter(s -> !s.isEmpty())
45+
.filter(s -> !s.startsWith("-javaagent:"))
46+
.collect(Collectors.joining());
47+
container.withEnv("JAVA_TOOL_OPTIONS", jvmOptions);
48+
}
49+
50+
// make the app use runtime-attach
51+
container.withEnv("EDOT_RUNTIME_ATTACH", "true");
52+
});
53+
}
54+
55+
@AfterAll
56+
public static void end() {
57+
stopApp();
58+
}
59+
60+
@Test
61+
void runtimeAttachTrace() {
62+
// runtime attach is working if we can capture any signal, for simplicity we only test traces
63+
64+
doRequest(getUrl("/health"), okResponseBody("Alive!"));
65+
66+
List<ExportTraceServiceRequest> traces = waitForTraces();
67+
List<Span> spans = getSpans(traces).toList();
68+
assertThat(spans).hasSize(1).extracting("name").containsOnly("GET /health");
69+
}
70+
}

smoke-tests/src/test/java/com/example/javaagent/smoketest/SmokeTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ private static GenericContainer<?> addDockerDebugHost(GenericContainer<?> target
161161
}
162162

163163
@BeforeEach
164-
void beforeEach() throws IOException, InterruptedException {
164+
void beforeEach() throws IOException {
165165
// because traces reporting is asynchronous we need to wait for the healthcheck traces to be
166166
// reported and only then
167167
// flush before the test, otherwise the first test will see the healthcheck trace captured.

smoke-tests/test-app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ dependencies {
2020
implementation("io.opentelemetry:opentelemetry-api")
2121
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")
2222

23+
implementation(project(":runtime-attach"))
24+
2325
}
2426

2527
java {

0 commit comments

Comments
 (0)