[maven-extension] Capture details on mojo goal executions: `deploy:deploy`, `spring-boot:build-image`, `jib:build`, `snyk:test`, `snyk:monitor` (#146)

* Capture details on mojo goal executions: deploy:deploy, spring-boot:build-image

* Update screenshot

* Make spotless happy

* Clarify TODO + better class name

* Improve code

* Code cleanup

* Code cleanup

* Add handlers for `snyk:test` and `snyk:monitor`

* Add support for the Google Maven Jib Plugin

* Add unit tests, code cleanup

* Better README.md

* Better README.md

* Cleanup code

* Iterate on the semantic conventions

* Replace Optional by `@Nullable

* Make the built-in MojoGoalExecutionHandlers non-public while keeping the SPI mechanism

* Make the built-in MojoGoalExecutionHandlers non-public while keeping the SPI mechanism

* spotless apply

* Stop capturing auth username waiting for config flag to disable this

* Fix Javadocs

* Fix typo
This commit is contained in:
Cyrille Le Clerc 2022-01-04 00:49:55 +01:00 committed by GitHub
parent 8cf71605a5
commit 2c02e42003
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1480 additions and 60 deletions

View File

@ -57,42 +57,126 @@ mvn verify
Without this setting, the traces won't be exported and the OpenTelemetry Maven Extension will behave as a NoOp extension. `otlp` is currently the only supported exporter.
The Maven OpenTelemetry Extension supports a subset of the [OpenTelemetry auto configuration environment variables and JVM system properties](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure).
The Maven OpenTelemetry Extension supports a subset of the [OpenTelemetry autoconfiguration environment variables and JVM system properties](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure).
| System property | Environment variable | Default value | Description |
|------------------------------|-----------------------------|-------------------------|---------------------------------------------------------------------------|
| otel.traces.exporter | OTEL_TRACES_EXPORTER | `none` | Select the OpenTelemetry exporter for tracing, the currently only supported values are `none` and `otlp`. `none` makes the instrumentation NoOp |
| otel.exporter.otlp.endpoint | OTEL_EXPORTER_OTLP_ENDPOINT | `http://localhost:4317` | The OTLP traces and metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. |
| otel.exporter.otlp.headers | OTEL_EXPORTER_OTLP_HEADERS | | Key-value pairs separated by commas to pass as request headers on OTLP trace and metrics requests. |
| otel.exporter.otlp.timeout | OTEL_EXPORTER_OTLP_TIMEOUT | `10000` | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. |
| otel.resource.attributes | OTEL_RESOURCE_ATTRIBUTES | | Specify resource attributes in the following format: key1=val1,key2=val2,key3=val3 |
| otel.instrumentation.maven.mojo.enabled | OTEL_INSTRUMENTATION_MAVEN_MOJO_ENABLED | `true` | Whether to create spans for mojo goal executions, `true` or `false`. Can be configured to reduce the number of spans created for large builds. |
| System property <br /> Environment variable | Default value | Description |
|--------------------------------------------------------------------------------------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `otel.traces.exporter` <br /> `OTEL_TRACES_EXPORTER` | `none` | Select the OpenTelemetry exporter for tracing, the currently only supported values are `none` and `otlp`. `none` makes the instrumentation NoOp |
| `otel.exporter.otlp.endpoint` <br /> `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | The OTLP traces and metrics endpoint to connect to. Must be a URL with a scheme of either `http` or `https` based on the use of TLS. |
| `otel.exporter.otlp.headers` <br /> `OTEL_EXPORTER_OTLP_HEADERS` | | Key-value pairs separated by commas to pass as request headers on OTLP trace and metrics requests. |
| `otel.exporter.otlp.timeout` <br /> `OTEL_EXPORTER_OTLP_TIMEOUT` | `10000` | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. |
| `otel.resource.attributes` <br /> `OTEL_RESOURCE_ATTRIBUTES` | | Specify resource attributes in the following format: key1=val1,key2=val2,key3=val3 |
| `otel.instrumentation.maven.mojo.enabled` <br /> `OTEL_INSTRUMENTATION_MAVEN_MOJO_ENABLED` | `true` | Whether to create spans for mojo goal executions, `true` or `false`. Can be configured to reduce the number of spans created for large builds. |
The `service.name` is set to `maven` and the `service.version` to the version of the Maven runtime in use.
## Examples
Example of a trace of a Maven build.
![](https://raw.githubusercontent.com/open-telemetry/opentelemetry-java-contrib/main/maven-extension/docs/images/maven-execution-trace-jaeger.png)
## Example of a distributed trace of a Jenkins pipeline executing a Maven build
### Example of a distributed trace of a Jenkins pipeline executing a Maven build
Distributed trace of a Jenkins pipeline invoking a Maven build instrumented with the [Jenkins OpenTelemetry plugin](https://plugins.jenkins.io/opentelemetry/) and the OpenTelemetry Maven Extension and visualized with [Jaeger Tracing](https://www.jaegertracing.io/)
![](https://raw.githubusercontent.com/open-telemetry/opentelemetry-java-contrib/main/maven-extension/docs/images/jenkins-maven-execution-trace-jaeger.png)
# Other CI/CD Tools supporting OpenTelemetry traces
## Span attributes per Maven plugin goal execution
### Span attributes captured for every Maven plugin goal execution
| Span attribute | Type | Description |
|----------------------------------|--------|----------------------------------------------------------------------|
| `maven.project.groupId` | string | Group ID of the Maven project on which the Maven goal is executed |
| `maven.project.artifactId` | string | Artifact ID of the Maven project on which the Maven goal is executed |
| `maven.project.version` | string | Version of the Maven project on which the Maven goal is executed |
| `maven.plugin.groupId` | string | Group ID of the Maven plugin on which the Maven goal is executed |
| `maven.plugin.artifactId` | string | Artifact ID of the Maven plugin on which the Maven goal is executed |
| `maven.plugin.version` | string | Version of the Maven plugin on which the Maven goal is executed |
| `maven.execution.goal` | string | Goal that is being executed |
| `maven.execution.id` | string | ID of the execution |
| `maven.execution.lifecyclePhase` | string | Lifecycle phase to which belong the execution |
### `deploy:deploy`
In addition to the span attributes captured on every Maven plugin goal execution as described above:
| Span attribute | Type | Description |
|----------------------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| `http.method` | string | `POST` |
| `http.url` | string | Base URL of the uploaded artifact `${maven.build.repository.url}/${groupId}/${artifactId}/${version}` where the `.` of `${groupId}` are replaced by `/` |
| `maven.build.repository.id` | string | ID of the Maven repository to which the artifact is deployed. See [Maven POM reference / Repository](https://maven.apache.org/pom.html#repository) |
| `maven.build.repository.url` | string | URL of the Maven repository to which the artifact is deployed. See [Maven POM reference / Repository](https://maven.apache.org/pom.html#repository) |
| `peer.service` | string | Maven repository hostname deduced from the Repository URL |
The `span.kind` is set to `client`
### `jib:build`
| Span attribute | Type | Description |
|-------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------|
| `http.method` | string | `POST` |
| `http.url` | string | URL on the Docker registry deduced from the Docker image specified in the `build` goal configuration. |
| `maven.build.container.image.name` | string | Name of the produced Docker image |
| `maven.build.container.image.tags` | string[] | Tags of the produced Docker image |
| `maven.build.container.registry.url` | string | URL of the container registry to which this image is uploaded. |
| `peer.service` | string | Docker Registry hostname. |
The `span.kind` is set to `client`
### `snyk:monitor`
See https://github.com/snyk/snyk-maven-plugin
| Span attribute | Type | Description |
|----------------|--------|------------------------------------------------------------------------------------------------|
| `http.method` | string | `POST` |
| `http.url` | string | `https://snyk.io/api/v1/monitor/maven` the underlying Snyk API URL invoked by the Maven plugin.|
| `rpc.method` | string | `monitor`, the underlying Snyk CLI command invoked by the Maven plugin.|
| `peer.service` | string | `snyk.io` |
The `span.kind` is set to `client`
### `snyk:test`
See https://github.com/snyk/snyk-maven-plugin
| Span attribute | Type | Description |
|----------------|--------|----------------------------------------------------------------------|
| `http.method` | string | `POST` |
| `http.url` | string | `https://snyk.io/api/v1/test-dep-graph` |
| `rpc.method` | string | `test`, the underlying Snyk CLI command invoked by the Maven plugin. |
| `peer.service` | string | `snyk.io` |
The `span.kind` is set to `client`
### `spring-boot:build-image`
| Span attribute | Type | Description |
|------------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `http.method` | string | `POST`. Attribute only added when the `build-image` goal publishes the Docker image. |
| `http.url` | string | URL on the Docker registry, deduced from the Docker image. Attribute only added when the `build-image` goal publishes the Docker image. |
| `maven.build.container.image.name` | string | Name of the produced Docker image. Attribute only added when the `build-image` goal publishes the Docker image. |
| `maven.build.container.image.tags` | string[] | Tags of the produced Docker image. Attribute only added when the `build-image` goal publishes the Docker image. |
| `maven.build.container.registry.url` | string | URL of the container registry to which this image is uploaded. Attribute only added when the `build-image` goal publishes the Docker image.|
| `peer.service` | string | Docker Registry hostname. Attribute only added when the `build-image` goal publishes the Docker image. |
The `span.kind` is set to `client`
## Other CI/CD Tools supporting OpenTelemetry traces
List of other CI/CD tools that support OpenTelemetry traces and integrate with the Maven OpenTelemetry Extension creating a distributed traces providing end to end visibility.
## Jenkins OpenTelemetry Plugin
### Jenkins OpenTelemetry Plugin
The [Jenkins OpenTelemetry Plugin](https://plugins.jenkins.io/opentelemetry/) exposes Jenkins pipelines & jobs as OpenTelemetry traces and exposes Jenkins health indicators as OpenTelemetry metrics.
## Otel CLI
### Otel CLI
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 519 KiB

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven;
import com.google.auto.value.AutoValue;
import org.apache.maven.plugin.MojoExecution;
@AutoValue
public abstract class MavenGoal {
public static MavenGoal create(String groupId, String artifactId, String goal) {
return new AutoValue_MavenGoal(groupId, artifactId, goal);
}
public static MavenGoal create(MojoExecution mojoExecution) {
return create(
mojoExecution.getGroupId(), mojoExecution.getArtifactId(), mojoExecution.getGoal());
}
abstract String groupId();
abstract String artifactId();
abstract String goal();
@Override
public final String toString() {
return "MavenGoal{ "
+ groupId()
+ ":"
+ MavenUtils.getPluginArtifactIdShortName(artifactId())
+ ":"
+ goal()
+ "}";
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven;
import org.apache.maven.plugin.MojoExecution;
final class MavenUtils {
private MavenUtils() {}
/**
* Shorten plugin identifiers.
*
* <p>Examples:
*
* <ul>
* <li>maven-clean-plugin -&gt; clean
* <li>sisu-maven-plugin -&gt; sisu
* <li>spotbugs-maven-plugin -&gt; spotbugs
* </ul>
*
* @param pluginArtifactId the artifact ID of the mojo {@link MojoExecution#getArtifactId()}
* @return shortened name
*/
static String getPluginArtifactIdShortName(String pluginArtifactId) {
if (pluginArtifactId.endsWith("-maven-plugin")) {
return pluginArtifactId.substring(0, pluginArtifactId.length() - "-maven-plugin".length());
} else if (pluginArtifactId.startsWith("maven-") && pluginArtifactId.endsWith("-plugin")) {
return pluginArtifactId.substring(
"maven-".length(), pluginArtifactId.length() - "-plugin".length());
} else {
return pluginArtifactId;
}
}
}

View File

@ -6,19 +6,25 @@
package io.opentelemetry.maven;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.maven.handler.MojoGoalExecutionHandler;
import io.opentelemetry.maven.handler.MojoGoalExecutionHandlerConfiguration;
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.maven.execution.AbstractExecutionListener;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.ExecutionListener;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.LifecycleExecutionException;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.annotations.Component;
@ -27,7 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Close the OpenTelemetry SDK (see {@link OpenTelemetrySdkService#dispose()} on the end of
* Close the OpenTelemetry SDK (see {@link OpenTelemetrySdkService#dispose()}) on the end of
* execution of the last project ({@link #projectSucceeded(ExecutionEvent)} and {@link
* #projectFailed(ExecutionEvent)}) rather than on the end of the Maven session {@link
* #sessionEnded(ExecutionEvent)} because OpenTelemetry and GRPC classes are unloaded by the Maven
@ -47,6 +53,22 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
@Requirement
private OpenTelemetrySdkService openTelemetrySdkService;
private Map<MavenGoal, MojoGoalExecutionHandler> mojoGoalExecutionHandlers = new HashMap<>();
public OtelExecutionListener() {
this.mojoGoalExecutionHandlers =
MojoGoalExecutionHandlerConfiguration.loadMojoGoalExecutionHandler(
OtelExecutionListener.class.getClassLoader());
if (logger.isDebugEnabled()) {
logger.debug(
"OpenTelemetry: mojoGoalExecutionHandlers: "
+ mojoGoalExecutionHandlers.entrySet().stream()
.map(entry -> entry.getKey().toString() + ": " + entry.getValue().toString())
.collect(Collectors.joining(", ")));
}
}
/**
* Register in given {@link OtelExecutionListener} to the lifecycle of the given {@link
* MavenSession}
@ -168,16 +190,16 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
Span rootSpan = spanRegistry.getSpan(executionEvent.getProject());
final String spanName =
getPluginArtifactIdShortName(mojoExecution.getArtifactId())
MavenUtils.getPluginArtifactIdShortName(mojoExecution.getArtifactId())
+ ":"
+ mojoExecution.getGoal()
+ " ("
+ executionEvent.getMojoExecution().getExecutionId()
+ mojoExecution.getExecutionId()
+ ")"
+ " @ "
+ executionEvent.getProject().getArtifactId();
logger.debug("OpenTelemetry: Start mojo execution: span {}", spanName);
Span span =
SpanBuilder spanBuilder =
this.openTelemetrySdkService
.getTracer()
.spanBuilder(spanName)
@ -205,8 +227,16 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
MavenOtelSemanticAttributes.MAVEN_EXECUTION_ID, mojoExecution.getExecutionId())
.setAttribute(
MavenOtelSemanticAttributes.MAVEN_EXECUTION_LIFECYCLE_PHASE,
mojoExecution.getLifecyclePhase())
.startSpan();
mojoExecution.getLifecyclePhase());
// enrich spans with MojoGoalExecutionHandler
MojoGoalExecutionHandler handler =
this.mojoGoalExecutionHandlers.get(MavenGoal.create(mojoExecution));
logger.debug("OpenTelemetry: {} handler {}", executionEvent, handler);
if (handler != null) {
handler.enrichSpan(spanBuilder, executionEvent);
}
Span span = spanBuilder.startSpan();
spanRegistry.putSpan(span, mojoExecution, executionEvent.getProject());
}
@ -238,6 +268,13 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
executionEvent.getProject());
Span mojoExecutionSpan = spanRegistry.removeSpan(mojoExecution, executionEvent.getProject());
mojoExecutionSpan.setStatus(StatusCode.ERROR, "Mojo Failed"); // TODO verify description
Throwable exception = executionEvent.getException();
if (exception instanceof LifecycleExecutionException) {
LifecycleExecutionException executionException = (LifecycleExecutionException) exception;
// we already capture the context, no need to capture it again
exception = executionException.getCause();
}
mojoExecutionSpan.recordException(exception);
mojoExecutionSpan.end();
}
@ -247,32 +284,6 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
spanRegistry.removeRootSpan().end();
}
/**
* Shorten plugin identifiers.
*
* <p>Examples:
*
* <ul>
* <li>maven-clean-plugin -&gt; clean
* <li>sisu-maven-plugin -&gt; sisu
* <li>spotbugs-maven-plugin -&gt; spotbugs
* </ul>
*
* @param pluginArtifactId the artifact ID of the mojo {@link MojoExecution#getArtifactId()}
* @return shortened name
*/
// Visible for testing
String getPluginArtifactIdShortName(String pluginArtifactId) {
if (pluginArtifactId.endsWith("-maven-plugin")) {
return pluginArtifactId.substring(0, pluginArtifactId.length() - "-maven-plugin".length());
} else if (pluginArtifactId.startsWith("maven-") && pluginArtifactId.endsWith("-plugin")) {
return pluginArtifactId.substring(
"maven-".length(), pluginArtifactId.length() - "-plugin".length());
} else {
return pluginArtifactId;
}
}
private static class ToUpperCaseTextMapGetter implements TextMapGetter<Map<String, String>> {
@Override
public Iterable<String> keys(Map<String, String> environmentVariables) {

View File

@ -0,0 +1,96 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.maven.execution.ExecutionEvent;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** See https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin */
final class GoogleJibBuildHandler implements MojoGoalExecutionHandler {
private static final Logger logger = LoggerFactory.getLogger(GoogleJibBuildHandler.class);
@Override
public List<MavenGoal> getSupportedGoals() {
return Collections.singletonList(
MavenGoal.create("com.google.cloud.tools", "jib-maven-plugin", "build"));
}
@Override
public void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent executionEvent) {
spanBuilder.setSpanKind(SpanKind.CLIENT);
Xpp3Dom pluginNode = executionEvent.getMojoExecution().getConfiguration();
if (pluginNode == null) {
logger.debug("OpenTelemetry: GoogleJibBuildHandler: config node not found");
return;
}
Xpp3Dom toNode = pluginNode.getChild("to");
if (pluginNode == null) {
logger.debug("OpenTelemetry: GoogleJibBuildHandler: 'to' node not found");
return;
}
Xpp3Dom imageNode = toNode.getChild("image");
if (pluginNode == null) {
logger.debug("OpenTelemetry: GoogleJibBuildHandler: 'to/image' node not found");
return;
}
String imageNameAndTagValue = imageNode.getValue();
if (imageNameAndTagValue == null) {
logger.debug("OpenTelemetry: GoogleJibBuildHandler: value of node 'to/image' is null");
return;
}
String imageName;
List<String> imageTags = new ArrayList<>();
int colonIdx = imageNameAndTagValue.indexOf(':');
if (colonIdx == -1) {
imageName = imageNameAndTagValue;
// imageTag not specified
} else {
imageName = imageNameAndTagValue.substring(0, colonIdx);
imageTags.add(imageNameAndTagValue.substring(colonIdx + 1));
}
Xpp3Dom tagsNode = toNode.getChild("tags");
Xpp3Dom[] tagNodes = tagsNode == null ? new Xpp3Dom[0] : tagsNode.getChildren("tag");
Arrays.stream(tagNodes).map(Xpp3Dom::getValue).forEach(imageTags::add);
if (imageTags.isEmpty()) {
// default value
imageTags.add(executionEvent.getProject().getVersion());
}
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME, imageName);
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS, imageTags);
// REGISTRY URL
String registryHostname =
imageName.indexOf('/') == -1 ? "docker.io" : imageName.substring(0, imageName.indexOf('/'));
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL,
"https://" + registryHostname);
spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, "https://" + registryHostname);
spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, "POST");
// Note: setting the "peer.service" helps visualization on Jaeger but
// may not fully comply with the OTel "peer.service" spec as we don't know if the remote
// service will be instrumented and what it "service.name" would be
spanBuilder.setAttribute(SemanticAttributes.PEER_SERVICE, registryHostname);
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Note: Later, we may prefer to have one span per actual upload than a generic span. We could
* achieve this instrumenting the <a
* href="https://projects.eclipse.org/projects/technology.aether">Aether library</a>
*/
final class MavenDeployHandler implements MojoGoalExecutionHandler {
private static final Logger logger = LoggerFactory.getLogger(MavenDeployHandler.class);
@Override
public void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent execution) {
spanBuilder.setSpanKind(SpanKind.CLIENT);
MavenProject project = execution.getProject();
ArtifactRepository optRepository = project.getDistributionManagementArtifactRepository();
if (optRepository == null) {
return;
}
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_REPOSITORY_ID, optRepository.getId());
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_REPOSITORY_URL, optRepository.getUrl());
String artifactRepositoryUrl = optRepository.getUrl();
if (artifactRepositoryUrl != null
&& (artifactRepositoryUrl.startsWith("https://")
|| artifactRepositoryUrl.startsWith("http://"))) {
try {
// Note: setting the "peer.service" helps visualization on Jaeger but
// may not fully comply with the OTel "peer.service" spec as we don't know if the remote
// service will be instrumented and what it "service.name" would be
spanBuilder.setAttribute(
SemanticAttributes.PEER_SERVICE, new URL(artifactRepositoryUrl).getHost());
} catch (MalformedURLException e) {
logger.debug("Ignore exception parsing artifact repository URL", e);
}
Artifact artifact = project.getArtifact();
String artifactRootUrl = artifactRepositoryUrl;
if (!artifactRootUrl.endsWith("/")) {
artifactRootUrl += '/';
}
artifactRootUrl +=
artifact.getGroupId().replace('.', '/')
+ '/'
+ artifact.getArtifactId()
+ '/'
+ artifact.getVersion();
spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, artifactRootUrl);
spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, "POST");
}
}
@Override
public List<MavenGoal> getSupportedGoals() {
return Collections.singletonList(
MavenGoal.create("org.apache.maven.plugins", "maven-deploy-plugin", "deploy"));
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.maven.MavenGoal;
import java.util.List;
import org.apache.maven.execution.ExecutionEvent;
public interface MojoGoalExecutionHandler {
default void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent executionEvent) {}
List<MavenGoal> getSupportedGoals();
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.maven.MavenGoal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.stream.Stream;
public class MojoGoalExecutionHandlerConfiguration {
public static Map<MavenGoal, MojoGoalExecutionHandler> loadMojoGoalExecutionHandler(
ClassLoader classLoader) {
// built-in handlers
List<MojoGoalExecutionHandler> builtInHandlers =
Arrays.asList(
new GoogleJibBuildHandler(),
new MavenDeployHandler(),
new SnykMonitorHandler(),
new SnykTestHandler(),
new SpringBootBuildImageHandler());
List<MojoGoalExecutionHandler> spiHandlers = new ArrayList();
// Must use the classloader of the class rather the default ThreadContextClassloader to prevent
// java.util.ServiceConfigurationError:
// io.opentelemetry.maven.handler.MojoGoalExecutionHandler:
// io.opentelemetry.maven.handler.SpringBootBuildImageHandler not a subtype
ServiceLoader.load(MojoGoalExecutionHandler.class, classLoader)
.forEach(handler -> spiHandlers.add(handler));
Map<MavenGoal, MojoGoalExecutionHandler> mojoGoalExecutionHandlers = new HashMap<>();
Stream.concat(builtInHandlers.stream(), spiHandlers.stream())
.forEach(
handler ->
handler
.getSupportedGoals()
.forEach(
goal -> {
MojoGoalExecutionHandler previousHandler =
mojoGoalExecutionHandlers.put(goal, handler);
if (previousHandler != null) {
throw new IllegalStateException(
"More than one handler found for maven goal "
+ goal
+ ": "
+ previousHandler
+ ", "
+ handler);
}
}));
return mojoGoalExecutionHandlers;
}
private MojoGoalExecutionHandlerConfiguration() {}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.Collections;
import java.util.List;
import org.apache.maven.execution.ExecutionEvent;
/** See https://github.com/snyk/snyk-maven-plugin */
final class SnykMonitorHandler implements MojoGoalExecutionHandler {
/**
* Snyk command "reversed engineered" invoking the Snyk CLI on a Maven project with the `-d` debug
* flag `snyk -d monitor`. See <a href="https://snyk.io/blog/snyk-cli-cheat-sheet/">Snyk CLI Cheat
* Sheet</a>
*/
@Override
public void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent executionEvent) {
spanBuilder.setSpanKind(SpanKind.CLIENT);
spanBuilder.setAttribute(SemanticAttributes.PEER_SERVICE, "snyk.io");
spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, "https://snyk.io/api/v1/monitor/maven");
spanBuilder.setAttribute(SemanticAttributes.RPC_METHOD, "monitor");
spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, "POST");
}
@Override
public List<MavenGoal> getSupportedGoals() {
return Collections.singletonList(MavenGoal.create("io.snyk", "snyk-maven-plugin", "monitor"));
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.util.Collections;
import java.util.List;
import org.apache.maven.execution.ExecutionEvent;
/** See https://github.com/snyk/snyk-maven-plugin */
final class SnykTestHandler implements MojoGoalExecutionHandler {
/**
* Snyk command "reversed engineered" invoking the Snyk CLI on a Maven project with the `-d` debug
* flag `snyk -d test`. See <a href="https://snyk.io/blog/snyk-cli-cheat-sheet/">Snyk CLI Cheat
* Sheet</a>
*/
@Override
public void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent executionEvent) {
spanBuilder.setSpanKind(SpanKind.CLIENT);
spanBuilder.setAttribute(SemanticAttributes.PEER_SERVICE, "snyk.io");
spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, "https://snyk.io/api/v1/test-dep-graph");
spanBuilder.setAttribute(SemanticAttributes.RPC_METHOD, "test");
spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, "POST");
}
@Override
public List<MavenGoal> getSupportedGoals() {
return Collections.singletonList(MavenGoal.create("io.snyk", "snyk-maven-plugin", "test"));
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import org.apache.maven.execution.ExecutionEvent;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* See
*
* <ul>
* <li><a
* href="https://docs.spring.io/spring-boot/docs/2.6.1/maven-plugin/reference/htmlsingle/">Spring
* Boot Maven Plugin</a>
* <li><a
* href="https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin">GitHub
* : spring-boot/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/</a>
* </ul>
*/
final class SpringBootBuildImageHandler implements MojoGoalExecutionHandler {
private static final Logger logger = LoggerFactory.getLogger(SpringBootBuildImageHandler.class);
@Override
public List<MavenGoal> getSupportedGoals() {
return Collections.singletonList(
MavenGoal.create("org.springframework.boot", "spring-boot-maven-plugin", "build-image"));
}
@Override
public void enrichSpan(SpanBuilder spanBuilder, ExecutionEvent executionEvent) {
spanBuilder.setSpanKind(SpanKind.CLIENT);
Xpp3Dom pluginNode = executionEvent.getMojoExecution().getConfiguration();
String imageNameAndTag = null;
if (pluginNode != null) {
Xpp3Dom imageNode = pluginNode.getChild("image");
if (imageNode != null) {
Xpp3Dom nameNode = imageNode.getChild("name");
if (nameNode != null) {
imageNameAndTag = nameNode.getValue();
}
}
}
String imageName;
String imageTag;
if (imageNameAndTag == null) {
// default image name docker.io/library/${project.artifactId}:${project.version}
// see
// https://docs.spring.io/spring-boot/docs/2.6.1/maven-plugin/reference/htmlsingle/#build-image.customization
imageName = "docker.io/library/" + executionEvent.getProject().getArtifactId();
imageTag = executionEvent.getProject().getVersion();
} else {
int colonIdx = imageNameAndTag.indexOf(':');
if (colonIdx == -1) {
imageName = imageNameAndTag;
imageTag = "latest";
} else {
imageTag = imageNameAndTag.substring(colonIdx + 1);
imageName = imageNameAndTag.substring(0, colonIdx);
}
}
// TODO handle use cases when additional additional `tags` are provided
// cyrille didn't understand from the Spring docs how to define multiple tags in the plugin cfg
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME, imageName);
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS,
Collections.singletonList(imageTag));
Xpp3Dom publishNode = pluginNode == null ? null : pluginNode.getChild("publish");
if (publishNode != null && Boolean.parseBoolean(publishNode.getValue())) {
Xpp3Dom dockerNode = pluginNode.getChild("docker");
Xpp3Dom registryNode = dockerNode == null ? null : dockerNode.getChild("publishRegistry");
if (registryNode != null) {
Xpp3Dom registryUrlNode = registryNode.getChild("url");
String registryUrl = registryUrlNode == null ? null : registryUrlNode.getValue();
// REGISTRY URL
if (registryUrl != null
&& (registryUrl.startsWith("http://") || registryUrl.startsWith("https://"))) {
spanBuilder.setAttribute(
MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL, registryUrl);
spanBuilder.setAttribute(SemanticAttributes.HTTP_URL, registryUrl);
spanBuilder.setAttribute(SemanticAttributes.HTTP_METHOD, "POST");
try {
// Note: setting the "peer.service" helps visualization on Jaeger but
// may not fully comply with the OTel "peer.service" spec as we don't know if the remote
// service will be instrumented and what it "service.name" would be
spanBuilder.setAttribute(
SemanticAttributes.PEER_SERVICE, new URL(registryUrl).getHost());
} catch (MalformedURLException e) {
logger.debug("Ignore exception parsing container registry URL", e);
}
}
}
}
}
}

View File

@ -5,9 +5,12 @@
package io.opentelemetry.maven.semconv;
import static io.opentelemetry.api.common.AttributeKey.stringArrayKey;
import static io.opentelemetry.api.common.AttributeKey.stringKey;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.util.List;
/**
* Semantic attributes for Maven executions.
@ -15,22 +18,37 @@ import io.opentelemetry.api.common.AttributeKey;
* @see io.opentelemetry.api.common.Attributes
* @see io.opentelemetry.semconv.trace.attributes.SemanticAttributes
*/
public final class MavenOtelSemanticAttributes {
public class MavenOtelSemanticAttributes {
/** See {@link ResourceAttributes#CONTAINER_IMAGE_NAME} */
public static final AttributeKey<String> MAVEN_BUILD_CONTAINER_IMAGE_NAME =
stringKey("maven.build.container.image.name");
/** See {@link ResourceAttributes#CONTAINER_IMAGE_TAG} */
public static final AttributeKey<List<String>> MAVEN_BUILD_CONTAINER_IMAGE_TAGS =
stringArrayKey("maven.build.container.image.tags");
public static final AttributeKey<String> MAVEN_BUILD_CONTAINER_REGISTRY_URL =
stringKey("maven.build.container.registry.url");
public static final AttributeKey<String> MAVEN_BUILD_REPOSITORY_ID =
stringKey("maven.build.repository.id");
public static final AttributeKey<String> MAVEN_BUILD_REPOSITORY_URL =
stringKey("maven.build.repository.url");
public static final AttributeKey<String> MAVEN_EXECUTION_GOAL = stringKey("maven.execution.goal");
public static final AttributeKey<String> MAVEN_EXECUTION_ID = stringKey("maven.execution.id");
public static final AttributeKey<String> MAVEN_EXECUTION_LIFECYCLE_PHASE =
stringKey("maven.execution.lifecyclePhase");
public static final AttributeKey<String> MAVEN_PLUGIN_ARTIFACT_ID =
stringKey("maven.plugin.artifactId");
public static final AttributeKey<String> MAVEN_PLUGIN_GROUP_ID =
stringKey("maven.plugin.groupId");
public static final AttributeKey<String> MAVEN_PLUGIN_VERSION = stringKey("maven.plugin.version");
public static final AttributeKey<String> MAVEN_PROJECT_ARTIFACT_ID =
stringKey("maven.project.artifactId");
public static final AttributeKey<String> MAVEN_PROJECT_GROUP_ID =
stringKey("maven.project.groupId");
public static final AttributeKey<String> MAVEN_PROJECT_VERSION =
stringKey("maven.project.version");
public static final AttributeKey<String> MAVEN_PLUGIN_ARTIFACT_ID =
stringKey("maven.plugin.artifactId");
public static final AttributeKey<String> MAVEN_PLUGIN_GROUP_ID =
stringKey("maven.plugin.groupId");
public static final AttributeKey<String> MAVEN_PLUGIN_VERSION = stringKey("maven.plugin.version");
public static final AttributeKey<String> MAVEN_EXECUTION_GOAL = stringKey("maven.execution.goal");
public static final AttributeKey<String> MAVEN_EXECUTION_LIFECYCLE_PHASE =
stringKey("maven.execution.lifecyclePhase");
public static final String SERVICE_NAME_VALUE = "maven";

View File

@ -9,20 +9,18 @@ import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
public class OtelExecutionListenerTest {
public class MavenUtilsTests {
@Test
public void getPluginArtifactIdShortName_builtinPluginName() {
OtelExecutionListener otelEventSpy = new OtelExecutionListener();
String actual = otelEventSpy.getPluginArtifactIdShortName("maven-clean-plugin");
String actual = MavenUtils.getPluginArtifactIdShortName("maven-clean-plugin");
String expected = "clean";
assertThat(actual).isEqualTo(expected);
}
@Test
public void getPluginArtifactIdShortName_thirdPartyPluginName() {
OtelExecutionListener otelEventSpy = new OtelExecutionListener();
String actual = otelEventSpy.getPluginArtifactIdShortName("spotbugs-maven-plugin");
String actual = MavenUtils.getPluginArtifactIdShortName("spotbugs-maven-plugin");
String expected = "spotbugs";
assertThat(actual).isEqualTo(expected);
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.maven.OtelExecutionListener;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class MojoGoalExecutionHandlerConfigurationTest {
@Test
public void mojoGoalExecutionHandlers() {
final Map<MavenGoal, MojoGoalExecutionHandler> actual =
MojoGoalExecutionHandlerConfiguration.loadMojoGoalExecutionHandler(
OtelExecutionListener.class.getClassLoader());
assertThat(actual.size()).isEqualTo(5);
}
}

View File

@ -0,0 +1,438 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven.handler;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.maven.MavenGoal;
import io.opentelemetry.maven.semconv.MavenOtelSemanticAttributes;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
import org.apache.maven.artifact.InvalidRepositoryException;
import org.apache.maven.bridge.MavenRepositorySystem;
import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.artifact.ProjectArtifact;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.junit.jupiter.api.Test;
/**
* TODO Find a better solution to instantiate a MavenProject and a MojoExecutionEvent. See
* https://github.com/takari/takari-lifecycle/blob/master/takari-lifecycle-plugin/src/test/java/io/takari/maven/plugins/plugin/PluginDescriptorMojoTest.java
*/
@SuppressWarnings("DeduplicateConstants")
public class MojoGoalExecutionHandlerTest {
@Test
public void testMavenDeploy() throws Exception {
String pomXmlPath = "projects/jar/pom.xml";
String mojoGroupId = "org.apache.maven.plugins";
String mojoArtifactId = "maven-deploy-plugin";
String mojoVersion = "2.8.2";
String mojoGoal = "deploy";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
MavenDeployHandler mavenDeployHandler = new MavenDeployHandler();
List<MavenGoal> supportedGoals = mavenDeployHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider.tracerBuilder("test-tracer").build().spanBuilder("deploy");
mavenDeployHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_REPOSITORY_ID))
.isEqualTo("snapshots");
assertThat(span.getAttribute(SemanticAttributes.HTTP_METHOD)).isEqualTo("POST");
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL))
.isEqualTo(
"https://maven.example.com/repository/maven-snapshots/io/opentelemetry/contrib/maven/test-jar/1.0-SNAPSHOT");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_REPOSITORY_URL))
.isEqualTo("https://maven.example.com/repository/maven-snapshots/");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("maven.example.com");
assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
}
}
@Test
public void testSpringBootBuildImage_springboot_1() throws Exception {
String pomXmlPath = "projects/springboot_1/pom.xml";
String mojoGroupId = "org.springframework.boot";
String mojoArtifactId = "spring-boot-maven-plugin";
String mojoVersion = "2.5.6";
String mojoGoal = "build-image";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
SpringBootBuildImageHandler buildImageHandler = new SpringBootBuildImageHandler();
List<MavenGoal> supportedGoals = buildImageHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider
.tracerBuilder("test-tracer")
.build()
.spanBuilder("spring-boot:build-image");
buildImageHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
// TODO improve the Maven test harness that interpolates the maven properties like
// ${project.artifactId}
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME))
.isEqualTo("docker.io/john/${project.artifactId}");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS))
.isEqualTo(Collections.singletonList("latest"));
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL)).isEqualTo("https://docker.io");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("docker.io");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL))
.isEqualTo("https://docker.io");
}
}
@Test
public void testSpringBootBuildImage_springboot_2() throws Exception {
String pomXmlPath = "projects/springboot_2/pom.xml";
String mojoGroupId = "org.springframework.boot";
String mojoArtifactId = "spring-boot-maven-plugin";
String mojoVersion = "2.5.6";
String mojoGoal = "build-image";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
SpringBootBuildImageHandler buildImageHandler = new SpringBootBuildImageHandler();
List<MavenGoal> supportedGoals = buildImageHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider
.tracerBuilder("test-tracer")
.build()
.spanBuilder("spring-boot:build-image");
buildImageHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
// TODO improve the Maven test harness that interpolates the maven properties like
// ${project.artifactId}
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME))
.isEqualTo("docker.io/john/${project.artifactId}");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS))
.isEqualTo(Collections.singletonList("${project.version}"));
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL)).isEqualTo("https://docker.io");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("docker.io");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL))
.isEqualTo("https://docker.io");
}
}
@Test
public void testGoogleJibBuild_jib_1() throws Exception {
String pomXmlPath = "projects/jib_1/pom.xml";
String mojoGroupId = "com.google.cloud.tools";
String mojoArtifactId = "jib-maven-plugin";
String mojoVersion = "3.1.4";
String mojoGoal = "build";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
GoogleJibBuildHandler buildImageHandler = new GoogleJibBuildHandler();
List<MavenGoal> supportedGoals = buildImageHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider.tracerBuilder("test-tracer").build().spanBuilder("jib:build");
buildImageHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
// TODO improve the Maven test harness that interpolates the maven properties like
// ${project.artifactId}
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME))
.isEqualTo("docker.io/john/${project.artifactId}");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS))
.isEqualTo(Arrays.asList("latest", "${project.version}"));
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL)).isEqualTo("https://docker.io");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("docker.io");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL))
.isEqualTo("https://docker.io");
}
}
@Test
public void testGoogleJibBuild_jib_2() throws Exception {
String pomXmlPath = "projects/jib_2/pom.xml";
String mojoGroupId = "com.google.cloud.tools";
String mojoArtifactId = "jib-maven-plugin";
String mojoVersion = "3.1.4";
String mojoGoal = "build";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
GoogleJibBuildHandler buildImageHandler = new GoogleJibBuildHandler();
List<MavenGoal> supportedGoals = buildImageHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider.tracerBuilder("test-tracer").build().spanBuilder("jib:build");
buildImageHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
// TODO improve the Maven test harness that interpolates the maven properties like
// ${project.artifactId}
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_NAME))
.isEqualTo("gcr.io/my-gcp-project/my-app");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_IMAGE_TAGS))
.isEqualTo(Collections.singletonList("1.0-SNAPSHOT"));
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL)).isEqualTo("https://gcr.io");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("gcr.io");
assertThat(span.getAttribute(MavenOtelSemanticAttributes.MAVEN_BUILD_CONTAINER_REGISTRY_URL))
.isEqualTo("https://gcr.io");
}
}
@Test
public void testSnykTest_snyk_1() throws Exception {
String pomXmlPath = "projects/snyk_1/pom.xml";
String mojoGroupId = "io.snyk";
String mojoArtifactId = "snyk-maven-plugin";
String mojoVersion = "2.0.0";
String mojoGoal = "test";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
SnykTestHandler snykTestHandler = new SnykTestHandler();
List<MavenGoal> supportedGoals = snykTestHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider.tracerBuilder("test-tracer").build().spanBuilder("snyk:test");
snykTestHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL))
.isEqualTo("https://snyk.io/api/v1/test-dep-graph");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("snyk.io");
assertThat(span.getAttribute(SemanticAttributes.RPC_METHOD)).isEqualTo("test");
}
}
@Test
public void testSnykMonitor_snyk_1() throws Exception {
String pomXmlPath = "projects/snyk_1/pom.xml";
String mojoGroupId = "io.snyk";
String mojoArtifactId = "snyk-maven-plugin";
String mojoVersion = "2.0.0";
String mojoGoal = "monitor";
MavenProject project = newMavenProject(pomXmlPath);
ExecutionEvent executionEvent =
newMojoStartedExecutionEvent(project, mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal);
SnykMonitorHandler snykTestHandler = new SnykMonitorHandler();
List<MavenGoal> supportedGoals = snykTestHandler.getSupportedGoals();
assertThat(supportedGoals)
.isEqualTo(
Collections.singletonList(MavenGoal.create(mojoGroupId, mojoArtifactId, mojoGoal)));
try (SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().build()) {
SpanBuilder spanBuilder =
sdkTracerProvider.tracerBuilder("test-tracer").build().spanBuilder("snyk:monitor");
snykTestHandler.enrichSpan(spanBuilder, executionEvent);
ReadableSpan span = (ReadableSpan) spanBuilder.startSpan();
assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
assertThat(span.getAttribute(SemanticAttributes.HTTP_URL))
.isEqualTo("https://snyk.io/api/v1/monitor/maven");
assertThat(span.getAttribute(SemanticAttributes.PEER_SERVICE)).isEqualTo("snyk.io");
assertThat(span.getAttribute(SemanticAttributes.RPC_METHOD)).isEqualTo("monitor");
}
}
ExecutionEvent newMojoStartedExecutionEvent(
MavenProject project,
String mojoGroupId,
String mojoArtifactId,
String mojoVersion,
String mojoGoal) {
MojoExecution mojoExecution =
newMojoExecution(mojoGroupId, mojoArtifactId, mojoVersion, mojoGoal, project);
MavenSession session =
new MavenSession(
null, null, new DefaultMavenExecutionRequest(), new DefaultMavenExecutionResult());
session.setCurrentProject(project);
return new MockExecutionEvent(ExecutionEvent.Type.MojoStarted, project, session, mojoExecution);
}
MavenProject newMavenProject(String pomXmlPath)
throws IOException, XmlPullParserException, InvalidRepositoryException {
InputStream pomXmlAsStream =
Thread.currentThread().getContextClassLoader().getResourceAsStream(pomXmlPath);
MavenXpp3Reader mavenXpp3Reader = new MavenXpp3Reader();
Model model = mavenXpp3Reader.read(pomXmlAsStream);
MavenProject project = new MavenProject(model);
project.setArtifact(new ProjectArtifact(project));
DistributionManagement distributionManagement = model.getDistributionManagement();
if (distributionManagement != null) {
project.setSnapshotArtifactRepository(
MavenRepositorySystem.buildArtifactRepository(
distributionManagement.getSnapshotRepository()));
project.setReleaseArtifactRepository(
MavenRepositorySystem.buildArtifactRepository(distributionManagement.getRepository()));
}
return project;
}
MojoExecution newMojoExecution(
String groupId, String artifactId, String version, String goal, MavenProject project) {
PluginDescriptor pluginDescriptor = new PluginDescriptor();
pluginDescriptor.setGroupId(groupId);
pluginDescriptor.setArtifactId(artifactId);
pluginDescriptor.setVersion(version);
MojoDescriptor mojoDescriptor = new MojoDescriptor();
mojoDescriptor.setGoal(goal);
mojoDescriptor.setPluginDescriptor(pluginDescriptor);
MojoExecution mojoExecution = new MojoExecution(mojoDescriptor);
Plugin plugin = new Plugin();
plugin.setGroupId(groupId);
plugin.setArtifactId(artifactId);
plugin.setVersion(version);
final Plugin configuredPlugin = project.getPlugin(plugin.getKey());
if (configuredPlugin != null) {
mojoExecution.setConfiguration((Xpp3Dom) configuredPlugin.getConfiguration());
}
return mojoExecution;
}
static class MockExecutionEvent implements ExecutionEvent {
ExecutionEvent.Type type;
MavenProject project;
MavenSession session;
MojoExecution mojoExecution;
public MockExecutionEvent(
ExecutionEvent.Type type,
MavenProject project,
MavenSession session,
MojoExecution mojoExecution) {
this.type = type;
this.project = project;
this.session = session;
this.mojoExecution = mojoExecution;
}
@Override
public ExecutionEvent.Type getType() {
return type;
}
@Override
public MavenSession getSession() {
return session;
}
@Override
public MavenProject getProject() {
return project;
}
@Override
public MojoExecution getMojoExecution() {
return mojoExecution;
}
@Nullable
@Override
public Exception getException() {
return null;
}
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.opentelemetry.contrib.maven</groupId>
<artifactId>test-jar</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<distributionManagement>
<repository>
<id>releases</id>
<url>https://maven.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>https://maven.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.opentelemetry.contrib.maven</groupId>
<artifactId>test-jib-1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.1.4</version>
<configuration>
<to>
<image>docker.io/john/${project.artifactId}:latest</image>
<tags>
<tag>${project.version}</tag>
</tags>
<auth>
<username>john</username>
<password>doe</password>
</auth>
</to>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>releases</id>
<url>https://maven.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>https://maven.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.opentelemetry.contrib.maven</groupId>
<artifactId>test-jib-2</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.1.4</version>
<configuration>
<to>
<image>gcr.io/my-gcp-project/my-app</image>
<credHelper>gcr</credHelper>
</to>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>releases</id>
<url>https://maven.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>https://maven.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.opentelemetry.contrib.maven</groupId>
<artifactId>test-snyk-1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.snyk</groupId>
<artifactId>snyk-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<apiToken>${snyk.token}</apiToken>
<args>
<arg>--all-projects</arg>
</args>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>releases</id>
<url>https://maven.example.com/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>https://maven.example.com/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.opentelemetry.contrib.maven.test</groupId>
<artifactId>springboot-test-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/john/${project.artifactId}</name>
</image>
<publish>true</publish>
<docker>
<publishRegistry>
<username>john</username>
<password>xxxx</password>
<url>https://docker.io</url>
</publishRegistry>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.maven.test.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.maven.test.springboot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootTestApplicationTests {
@Test
void contextLoads() {}
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.opentelemetry.contrib.maven.test</groupId>
<artifactId>springboot-test-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/john/${project.artifactId}:${project.version}</name>
</image>
<publish>true</publish>
<docker>
<publishRegistry>
<username>john</username>
<password>xxxx</password>
<url>https://docker.io</url>
</publishRegistry>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.maven.test.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.contrib.maven.test.springboot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootTestApplicationTests {
@Test
void contextLoads() {}
}