Skip to content

Commit 00a3928

Browse files
[maven-extension] Propagate OTel context to plugin mojos (#786)
**Description:** Propagate OTel context to plugin mojos so they can add their own attributes and spans to traces. **Existing Issue(s):** None **Testing:** No unit test framework compatible with JUnit 5 for Maven builds yet. **Documentation:** See updated README.md **Outstanding items:** None
1 parent d68c1e0 commit 00a3928

File tree

3 files changed

+175
-4
lines changed

3 files changed

+175
-4
lines changed

maven-extension/README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Add the Maven OpenTelemetry Extension in the `pom.xml` file:
3636
<extension>
3737
<groupId>io.opentelemetry.contrib</groupId>
3838
<artifactId>opentelemetry-maven-extension</artifactId>
39-
<version>1.10.0-alpha</version>
39+
<version>1.23.0-alpha</version>
4040
</extension>
4141
</extensions>
4242
</build>
@@ -175,6 +175,70 @@ The [Jenkins OpenTelemetry Plugin](https://plugins.jenkins.io/opentelemetry/) ex
175175

176176
The [`otel-cli`](https://github.com/equinix-labs/otel-cli) is a command line wrapper to observe the execution of a shell command as an OpenTelemetry trace.
177177

178+
## Instrumenting Maven Mojos for better visibility in Maven builds
179+
180+
Maven plugin authors can instrument Mojos for better visibility in Maven builds.
181+
182+
Common instrumentation patterns include:
183+
184+
* Adding contextual data as attributes on the spans created by the OpenTelemetry Maven Extension,
185+
* Creating additional sub spans to breakdown long mojo goal executions in finer grained steps
186+
187+
Note that the instrumentation of a plugin is enabled when the OpenTelemetry Maven extension is added to the build and activated.
188+
Otherwise, the instrumentation of the Maven plugin is noop.
189+
190+
It is recommended to enrich spans using the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/)
191+
to improve the visualization and analysis in Observability products.
192+
The [HTTP](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/)
193+
and [database client calls](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/database/)
194+
conventions are particularly useful when invoking external systems.
195+
196+
Steps to instrument a Maven Mojo:
197+
198+
* Add the OpenTelemetry API dependency in the `pom.xml` of the Maven plugin.
199+
200+
```xml
201+
<project>
202+
...
203+
<dependencies>
204+
<dependency>
205+
<groupId>io.opentelemetry</groupId>
206+
<artifactId>opentelemetry-api</artifactId>
207+
<version>LATEST</version>
208+
</dependency>
209+
...
210+
</dependencies>
211+
</project>
212+
````
213+
214+
* Instrument the Mojo:
215+
216+
```java
217+
@Mojo(name = "test", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
218+
public class TestMojo extends AbstractMojo {
219+
220+
@Override
221+
public void execute() {
222+
Span mojoExecuteSpan = Span.current();
223+
224+
// ENRICH THE DEFAULT SPAN OF THE MOJO EXECUTION
225+
// (this span is created by the opentelemetry-maven-extension)
226+
mojoExecuteSpan.setAttribute("an-attribute", "a-value");
227+
228+
// ... some logic
229+
230+
// CREATE SUB SPANS TO CAPTURE FINE GRAINED DETAILS OF THE MOJO EXECUTION
231+
Tracer tracer = GlobalOpenTelemetry.get().getTracer("com.example.maven.otel_aware_plugin");
232+
Span childSpan = tracer.spanBuilder("otel-aware-goal-sub-span").setAttribute("another-attribute", "another-value").startSpan();
233+
try (Scope ignored2 = childSpan.makeCurrent()) {
234+
// ... mojo sub operation
235+
} finally {
236+
childSpan.end();
237+
}
238+
}
239+
}
240+
```
241+
178242
## Component owners
179243

180244
- [Cyrille Le Clerc](https://github.com/cyrille-leclerc), Elastic

maven-extension/src/main/java/io/opentelemetry/maven/OtelExecutionListener.java

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
import io.opentelemetry.api.trace.StatusCode;
1212
import io.opentelemetry.api.trace.Tracer;
1313
import io.opentelemetry.context.Context;
14+
import io.opentelemetry.context.ContextStorage;
15+
import io.opentelemetry.context.Scope;
1416
import io.opentelemetry.context.propagation.TextMapGetter;
1517
import io.opentelemetry.maven.handler.MojoGoalExecutionHandler;
1618
import io.opentelemetry.maven.handler.MojoGoalExecutionHandlerConfiguration;
1719
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
18-
import java.util.HashMap;
1920
import java.util.Locale;
2021
import java.util.Map;
22+
import java.util.Optional;
2123
import java.util.stream.Collectors;
2224
import javax.annotation.Nullable;
2325
import org.apache.maven.execution.AbstractExecutionListener;
@@ -45,6 +47,15 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
4547

4648
private static final Logger logger = LoggerFactory.getLogger(OtelExecutionListener.class);
4749

50+
/**
51+
* Note that using a thread local around the mojo goal execution to carry the {@link Scope } works
52+
* even when using Maven build parallelization. {@link Span#current()} invoked in {@link
53+
* org.apache.maven.plugin.Mojo#execute()} returns as expected the span set in {@link
54+
* ExecutionListener#mojoStarted(ExecutionEvent)} using {@link Span#makeCurrent()}. For this
55+
* reason, we can carry over the {@link Scope} in a thread local variable.
56+
*/
57+
private static final ThreadLocal<Scope> MOJO_EXECUTION_SCOPE = new ThreadLocal<>();
58+
4859
@SuppressWarnings("NullAway") // Automatically initialized by DI
4960
@Requirement
5061
private SpanRegistry spanRegistry;
@@ -53,7 +64,7 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
5364
@Requirement
5465
private OpenTelemetrySdkService openTelemetrySdkService;
5566

56-
private Map<MavenGoal, MojoGoalExecutionHandler> mojoGoalExecutionHandlers = new HashMap<>();
67+
private final Map<MavenGoal, MojoGoalExecutionHandler> mojoGoalExecutionHandlers;
5768

5869
public OtelExecutionListener() {
5970
this.mojoGoalExecutionHandlers =
@@ -66,6 +77,28 @@ public OtelExecutionListener() {
6677
+ mojoGoalExecutionHandlers.entrySet().stream()
6778
.map(entry -> entry.getKey().toString() + ": " + entry.getValue().toString())
6879
.collect(Collectors.joining(", ")));
80+
81+
// help debugging class loader issues when the OTel APIs used in
82+
// Maven plugin mojos are mistakenly not loaded by the OTel Maven extension
83+
// causing the lack of context propagation from the OTel Maven extension to the plugin mojos
84+
ContextStorage contextStorage = ContextStorage.get();
85+
logger.debug(
86+
"ContextStorage: "
87+
+ contextStorage
88+
+ ", identity="
89+
+ System.identityHashCode(contextStorage));
90+
Class<? extends ContextStorage> contextStorageClass = contextStorage.getClass();
91+
logger.debug(
92+
"ContextStorageClass="
93+
+ contextStorageClass.getName()
94+
+ ", identity="
95+
+ System.identityHashCode(contextStorageClass)
96+
+ " classloader="
97+
+ contextStorageClass.getClassLoader()
98+
+ " codeLocation="
99+
+ Optional.of(contextStorageClass.getProtectionDomain().getCodeSource())
100+
.map(source -> source.getLocation().toString())
101+
.orElse("#unknown#"));
69102
}
70103
}
71104

@@ -232,7 +265,28 @@ public void mojoStarted(ExecutionEvent executionEvent) {
232265
}
233266

234267
Span span = spanBuilder.startSpan();
268+
@SuppressWarnings("MustBeClosedChecker")
269+
Scope scope = span.makeCurrent();
235270
spanRegistry.putSpan(span, mojoExecution, executionEvent.getProject());
271+
Optional.ofNullable(MOJO_EXECUTION_SCOPE.get())
272+
.ifPresent(
273+
previousScope ->
274+
logger.warn(
275+
"OpenTelemetry: Scope "
276+
+ System.identityHashCode(previousScope)
277+
+ "already attached to thread '"
278+
+ Thread.currentThread().getName()
279+
+ "'"));
280+
MOJO_EXECUTION_SCOPE.set(scope);
281+
if (logger.isDebugEnabled()) {
282+
logger.debug(
283+
"OpenTelemetry: Attach scope "
284+
+ System.identityHashCode(scope)
285+
+ " to thread '"
286+
+ Thread.currentThread().getName()
287+
+ "' for "
288+
+ mojoExecution);
289+
}
236290
}
237291

238292
@Override
@@ -247,8 +301,27 @@ public void mojoSucceeded(ExecutionEvent executionEvent) {
247301
executionEvent.getProject());
248302
Span mojoExecutionSpan = spanRegistry.removeSpan(mojoExecution, executionEvent.getProject());
249303
mojoExecutionSpan.setStatus(StatusCode.OK);
250-
251304
mojoExecutionSpan.end();
305+
Scope scope = MOJO_EXECUTION_SCOPE.get();
306+
if (scope == null) {
307+
logger.warn(
308+
"OpenTelemetry: No scope found on thread '"
309+
+ Thread.currentThread().getName()
310+
+ "' for succeeded "
311+
+ mojoExecution);
312+
} else {
313+
scope.close();
314+
MOJO_EXECUTION_SCOPE.remove();
315+
if (logger.isDebugEnabled()) {
316+
logger.debug(
317+
"OpenTelemetry: Remove scope "
318+
+ System.identityHashCode(scope)
319+
+ " on thread '"
320+
+ Thread.currentThread().getName()
321+
+ "' for succeeded "
322+
+ mojoExecution);
323+
}
324+
}
252325
}
253326

254327
@Override
@@ -273,6 +346,26 @@ public void mojoFailed(ExecutionEvent executionEvent) {
273346
mojoExecutionSpan.recordException(exception);
274347
}
275348
mojoExecutionSpan.end();
349+
Scope scope = MOJO_EXECUTION_SCOPE.get();
350+
if (scope == null) {
351+
logger.warn(
352+
"OpenTelemetry: No scope found on thread '"
353+
+ Thread.currentThread().getName()
354+
+ "' for failed "
355+
+ mojoExecution);
356+
} else {
357+
scope.close();
358+
MOJO_EXECUTION_SCOPE.remove();
359+
if (logger.isDebugEnabled()) {
360+
logger.debug(
361+
"OpenTelemetry: Remove scope "
362+
+ System.identityHashCode(scope)
363+
+ " on thread '"
364+
+ Thread.currentThread().getName()
365+
+ "' for failed "
366+
+ mojoExecution);
367+
}
368+
}
276369
}
277370

278371
@Override
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<extension>
2+
<exportedPackages>
3+
<!-- opentelemetry.api.* -->
4+
<exportedPackage>io.opentelemetry.api</exportedPackage>
5+
<exportedPackage>io.opentelemetry.api.*</exportedPackage>
6+
<exportedPackage>io.opentelemetry.api.baggage</exportedPackage>
7+
<exportedPackage>io.opentelemetry.api.baggage.propagation</exportedPackage>
8+
<exportedPackage>io.opentelemetry.api.common</exportedPackage>
9+
<exportedPackage>io.opentelemetry.api.internal</exportedPackage>
10+
<exportedPackage>io.opentelemetry.api.metrics</exportedPackage>
11+
<exportedPackage>io.opentelemetry.api.trace</exportedPackage>
12+
<exportedPackage>io.opentelemetry.context</exportedPackage>
13+
</exportedPackages>
14+
</extension>

0 commit comments

Comments
 (0)