add option to record transferred artifacts (#1875)

Co-authored-by: Cyrille Le Clerc <cyrille.leclerc@grafana.com>
This commit is contained in:
Jan Engehausen 2025-05-15 16:37:48 +02:00 committed by GitHub
parent b897a0ec76
commit 34937d2e24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 383 additions and 29 deletions

View File

@ -61,14 +61,15 @@ Without this setting, the traces won't be exported and the OpenTelemetry Maven E
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 <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. |
| 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. |
| `otel.instrumentation.maven.transfer.enabled` <br /> `OTEL_INSTRUMENTATION_MAVEN_TRANSFER_ENABLED` | `false` | Whether to create spans for artifact transfers, `true` or `false`. Can be activated to understand impact of artifact transfers on performances. |
The `service.name` is set to `maven` and the `service.version` to the version of the Maven runtime in use.

View File

@ -6,7 +6,7 @@ plugins {
}
// NOTE
// `META-INF/sis/javax.inject.Named` is manually handled under src/main/resources because there is
// `META-INF/sisu/javax.inject.Named` is manually handled under src/main/resources because there is
// no Gradle equivalent to the Maven plugin `org.eclipse.sisu:sisu-maven-plugin`
description = "Maven extension to observe Maven builds with distributed traces using OpenTelemetry SDK"
@ -24,7 +24,7 @@ dependencies {
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
implementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
annotationProcessor("com.google.auto.value:auto-value")
compileOnly("com.google.auto.value:auto-value-annotations")

View File

@ -0,0 +1,70 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferListener;
/**
* Util class to chain multiple {@link TransferListener} as Maven APIs don't offer this capability.
*/
final class ChainedTransferListener implements TransferListener {
private final List<TransferListener> listeners;
/**
* @param listeners {@code null} values are filtered
*/
ChainedTransferListener(TransferListener... listeners) {
this.listeners = Arrays.stream(listeners).filter(e -> e != null).collect(Collectors.toList());
}
@Override
public void transferInitiated(TransferEvent event) throws TransferCancelledException {
for (TransferListener listener : this.listeners) {
listener.transferInitiated(event);
}
}
@Override
public void transferStarted(TransferEvent event) throws TransferCancelledException {
for (TransferListener listener : this.listeners) {
listener.transferStarted(event);
}
}
@Override
public void transferProgressed(TransferEvent event) throws TransferCancelledException {
for (TransferListener listener : this.listeners) {
listener.transferProgressed(event);
}
}
@Override
public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
for (TransferListener listener : this.listeners) {
listener.transferCorrupted(event);
}
}
@Override
public void transferSucceeded(TransferEvent event) {
for (TransferListener listener : this.listeners) {
listener.transferSucceeded(event);
}
}
@Override
public void transferFailed(TransferEvent event) {
for (TransferListener listener : this.listeners) {
listener.transferFailed(event);
}
}
}

View File

@ -49,10 +49,12 @@ public final class OpenTelemetrySdkService implements Closeable {
private final boolean mojosInstrumentationEnabled;
private final boolean transferInstrumentationEnabled;
private boolean disposed;
public OpenTelemetrySdkService() {
logger.debug(
logger.info(
"OpenTelemetry: Initialize OpenTelemetrySdkService v{}...",
MavenOtelSemanticAttributes.TELEMETRY_DISTRO_VERSION_VALUE);
@ -76,6 +78,8 @@ public final class OpenTelemetrySdkService implements Closeable {
this.mojosInstrumentationEnabled =
configProperties.getBoolean("otel.instrumentation.maven.mojo.enabled", true);
this.transferInstrumentationEnabled =
configProperties.getBoolean("otel.instrumentation.maven.transfer.enabled", false);
this.tracer = openTelemetrySdk.getTracer("io.opentelemetry.contrib.maven", VERSION);
}
@ -154,4 +158,8 @@ public final class OpenTelemetrySdkService implements Closeable {
public boolean isMojosInstrumentationEnabled() {
return mojosInstrumentationEnabled;
}
public boolean isTransferInstrumentationEnabled() {
return transferInstrumentationEnabled;
}
}

View File

@ -17,12 +17,9 @@ 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.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.maven.execution.AbstractExecutionListener;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.ExecutionListener;
@ -338,19 +335,4 @@ public final class OtelExecutionListener extends AbstractExecutionListener {
logger.debug("OpenTelemetry: Maven session ended, end root span");
spanRegistry.removeRootSpan().end();
}
private static class ToUpperCaseTextMapGetter implements TextMapGetter<Map<String, String>> {
@Override
public Iterable<String> keys(Map<String, String> environmentVariables) {
return environmentVariables.keySet();
}
@Override
@Nullable
public String get(@Nullable Map<String, String> environmentVariables, @Nonnull String key) {
return environmentVariables == null
? null
: environmentVariables.get(key.toUpperCase(Locale.ROOT));
}
}
}

View File

@ -11,6 +11,9 @@ import javax.inject.Singleton;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.execution.ExecutionListener;
import org.apache.maven.execution.MavenSession;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.transfer.TransferListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -25,6 +28,8 @@ public final class OtelLifecycleParticipant extends AbstractMavenLifecyclePartic
private final OtelExecutionListener otelExecutionListener;
private final OtelTransferListener otelTransferListener;
/**
* Manually instantiate {@link OtelExecutionListener} and hook it in the Maven build lifecycle
* because Maven Sisu doesn't load it when Maven Plexus did.
@ -34,6 +39,14 @@ public final class OtelLifecycleParticipant extends AbstractMavenLifecyclePartic
OpenTelemetrySdkService openTelemetrySdkService, SpanRegistry spanRegistry) {
this.openTelemetrySdkService = openTelemetrySdkService;
this.otelExecutionListener = new OtelExecutionListener(spanRegistry, openTelemetrySdkService);
this.otelTransferListener = new OtelTransferListener(spanRegistry, openTelemetrySdkService);
}
@Override
public void afterSessionStart(MavenSession session) {
if (openTelemetrySdkService.isTransferInstrumentationEnabled()) {
registerTransferListener(session);
}
}
/**
@ -43,6 +56,10 @@ public final class OtelLifecycleParticipant extends AbstractMavenLifecyclePartic
*/
@Override
public void afterProjectsRead(MavenSession session) {
registerExecutionListener(session);
}
void registerExecutionListener(MavenSession session) {
ExecutionListener initialExecutionListener = session.getRequest().getExecutionListener();
if (initialExecutionListener instanceof ChainedExecutionListener
|| initialExecutionListener instanceof OtelExecutionListener) {
@ -64,6 +81,40 @@ public final class OtelLifecycleParticipant extends AbstractMavenLifecyclePartic
}
}
void registerTransferListener(MavenSession session) {
RepositorySystemSession repositorySession = session.getRepositorySession();
TransferListener initialTransferListener = repositorySession.getTransferListener();
if (initialTransferListener instanceof ChainedTransferListener
|| initialTransferListener instanceof OtelTransferListener) {
// already initialized
logger.debug(
"OpenTelemetry: OpenTelemetry extension already registered as transfer listener, skip.");
} else if (initialTransferListener == null) {
setTransferListener(this.otelTransferListener, repositorySession, session);
logger.debug(
"OpenTelemetry: OpenTelemetry extension registered as transfer listener. No transfer listener initially defined");
} else {
setTransferListener(
new ChainedTransferListener(this.otelTransferListener, initialTransferListener),
repositorySession,
session);
logger.debug(
"OpenTelemetry: OpenTelemetry extension registered as transfer listener. InitialTransferListener: {}",
initialTransferListener);
}
}
void setTransferListener(
TransferListener transferListener,
RepositorySystemSession repositorySession,
MavenSession session) {
if (repositorySession instanceof DefaultRepositorySystemSession) {
((DefaultRepositorySystemSession) repositorySession).setTransferListener(transferListener);
} else {
logger.warn("OpenTelemetry: Cannot set transfer listener");
}
}
@Override
public void afterSessionEnd(MavenSession session) {
// Workaround https://issues.apache.org/jira/browse/MNG-8217

View File

@ -0,0 +1,166 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
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.maven.semconv.MavenOtelSemanticAttributes;
import io.opentelemetry.semconv.HttpAttributes;
import io.opentelemetry.semconv.ServerAttributes;
import io.opentelemetry.semconv.UrlAttributes;
import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes;
import io.opentelemetry.semconv.incubating.UrlIncubatingAttributes;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.maven.execution.ExecutionListener;
import org.apache.maven.execution.MavenSession;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.TransferEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Don't mark this class as {@link javax.inject.Named} and {@link javax.inject.Singleton} because
* Maven Sisu doesn't automatically load instance of {@link ExecutionListener} as Maven Extension
* hooks the same way Maven Plexus did so we manually hook this instance of {@link
* ExecutionListener} through the {@link OtelLifecycleParticipant#afterProjectsRead(MavenSession)}.
*/
public final class OtelTransferListener extends AbstractTransferListener {
private static final Logger logger = LoggerFactory.getLogger(OtelTransferListener.class);
private final SpanRegistry spanRegistry;
private final OpenTelemetrySdkService openTelemetrySdkService;
private final Map<String, Optional<URI>> repositoryUriMapping = new ConcurrentHashMap<>();
OtelTransferListener(SpanRegistry spanRegistry, OpenTelemetrySdkService openTelemetrySdkService) {
this.spanRegistry = spanRegistry;
this.openTelemetrySdkService = openTelemetrySdkService;
}
@Override
public void transferInitiated(TransferEvent event) {
logger.debug("OpenTelemetry: OtelTransferListener#transferInitiated({})", event);
String httpRequestMethod;
switch (event.getRequestType()) {
case PUT:
httpRequestMethod = "PUT";
break;
case GET:
httpRequestMethod = "GET";
break;
case GET_EXISTENCE:
httpRequestMethod = "HEAD";
break;
default:
logger.warn(
"OpenTelemetry: Unknown request type {} for event {}", event.getRequestType(), event);
httpRequestMethod = event.getRequestType().name();
}
String urlTemplate =
event.getResource().getRepositoryUrl()
+ "$groupId/$artifactId/$version/$artifactId-$version.$classifier";
String spanName = httpRequestMethod + " " + urlTemplate;
// Build an HTTP client span as the http call itself is not instrumented.
SpanBuilder spanBuilder =
this.openTelemetrySdkService
.getTracer()
.spanBuilder(spanName)
.setSpanKind(SpanKind.CLIENT)
.setAttribute(HttpAttributes.HTTP_REQUEST_METHOD, httpRequestMethod)
.setAttribute(
UrlAttributes.URL_PATH,
event.getResource().getRepositoryUrl() + event.getResource().getResourceName())
.setAttribute(UrlIncubatingAttributes.URL_TEMPLATE, urlTemplate)
.setAttribute(
MavenOtelSemanticAttributes.MAVEN_TRANSFER_TYPE, event.getRequestType().name())
.setAttribute(
MavenOtelSemanticAttributes.MAVEN_RESOURCE_NAME,
event.getResource().getResourceName());
repositoryUriMapping
.computeIfAbsent(
event.getResource().getRepositoryUrl(),
str -> {
try {
return str.isEmpty() ? Optional.empty() : Optional.of(new URI(str));
} catch (URISyntaxException e) {
return Optional.empty();
}
})
.ifPresent(
uri -> {
spanBuilder.setAttribute(ServerAttributes.SERVER_ADDRESS, uri.getHost());
if (uri.getPort() != -1) {
spanBuilder.setAttribute(ServerAttributes.SERVER_PORT, uri.getPort());
}
// prevent ever increasing size
if (repositoryUriMapping.size() > 128) {
repositoryUriMapping.clear();
}
});
spanRegistry.putSpan(spanBuilder.startSpan(), event);
}
@Override
public void transferSucceeded(TransferEvent event) {
logger.debug("OpenTelemetry: OtelTransferListener#transferSucceeded({})", event);
Optional.ofNullable(spanRegistry.removeSpan(event))
.ifPresent(
span -> {
span.setStatus(StatusCode.OK);
finish(span, event);
});
}
@Override
public void transferFailed(TransferEvent event) {
logger.debug("OpenTelemetry: OtelTransferListener#transferFailed({})", event);
Optional.ofNullable(spanRegistry.removeSpan(event)).ifPresent(span -> fail(span, event));
}
@Override
public void transferCorrupted(TransferEvent event) {
logger.debug("OpenTelemetry: OtelTransferListener#transferCorrupted({})", event);
Optional.ofNullable(spanRegistry.removeSpan(event)).ifPresent(span -> fail(span, event));
}
void finish(Span span, TransferEvent event) {
switch (event.getRequestType()) {
case PUT:
span.setAttribute(
HttpIncubatingAttributes.HTTP_REQUEST_BODY_SIZE, event.getTransferredBytes());
break;
case GET:
case GET_EXISTENCE:
span.setAttribute(
HttpIncubatingAttributes.HTTP_RESPONSE_BODY_SIZE, event.getTransferredBytes());
break;
}
span.end();
}
void fail(Span span, TransferEvent event) {
span.setStatus(
StatusCode.ERROR,
Optional.ofNullable(event.getException()).map(Exception::getMessage).orElse("n/a"));
finish(span, event);
}
}

View File

@ -17,6 +17,9 @@ import javax.inject.Singleton;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,6 +38,7 @@ public final class SpanRegistry {
private final Map<MojoExecutionKey, Span> mojoExecutionKeySpanMap = new ConcurrentHashMap<>();
private final Map<MavenProjectKey, Span> mavenProjectKeySpanMap = new ConcurrentHashMap<>();
private final Map<TransferKey, Span> transferKeySpanMap = new ConcurrentHashMap<>();
@Nullable private Span rootSpan;
/**
@ -113,6 +117,15 @@ public final class SpanRegistry {
}
}
public void putSpan(Span span, TransferEvent event) {
TransferKey key = TransferKey.fromTransferEvent(event);
logger.debug("OpenTelemetry: putSpan({})", key);
Span previousSpanForKey = transferKeySpanMap.put(key, span);
if (previousSpanForKey != null) {
logger.warn("A span has already been started for " + key);
}
}
public Span removeSpan(MavenProject mavenProject) {
logger.debug("OpenTelemetry: removeSpan({})", mavenProject);
MavenProjectKey key = MavenProjectKey.fromMavenProject(mavenProject);
@ -136,6 +149,17 @@ public final class SpanRegistry {
return span;
}
public Span removeSpan(TransferEvent event) {
TransferKey key = TransferKey.fromTransferEvent(event);
logger.debug("OpenTelemetry: removeSpan({})", key);
Span span = transferKeySpanMap.remove(key);
if (span == null) {
logger.warn("No span found for " + key);
return Span.getInvalid();
}
return span;
}
@AutoValue
abstract static class MavenProjectKey {
abstract String groupId();
@ -185,4 +209,18 @@ public final class SpanRegistry {
MavenProjectKey.fromMavenProject(project));
}
}
@AutoValue
abstract static class TransferKey {
abstract String resourceName();
abstract String sessionId();
public static TransferKey fromTransferEvent(@Nonnull TransferEvent event) {
TransferResource resource = event.getResource();
RepositorySystemSession session = event.getSession();
return new AutoValue_SpanRegistry_TransferKey(
resource.getResourceName(), "session-" + System.identityHashCode(session));
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.maven;
import io.opentelemetry.context.propagation.TextMapGetter;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
final class ToUpperCaseTextMapGetter implements TextMapGetter<Map<String, String>> {
@Override
public Set<String> keys(Map<String, String> environmentVariables) {
return environmentVariables.keySet();
}
@Override
@Nullable
public String get(@Nullable Map<String, String> environmentVariables, @Nonnull String key) {
return environmentVariables == null
? null
: environmentVariables.get(key.toUpperCase(Locale.ROOT));
}
}

View File

@ -11,6 +11,7 @@ import static io.opentelemetry.api.common.AttributeKey.stringKey;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.maven.OpenTelemetrySdkService;
import java.util.List;
import org.eclipse.aether.transfer.TransferEvent;
/**
* Semantic attributes for Maven executions.
@ -36,6 +37,7 @@ public class MavenOtelSemanticAttributes {
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");
@ -53,6 +55,14 @@ public class MavenOtelSemanticAttributes {
public static final AttributeKey<String> MAVEN_PROJECT_VERSION =
stringKey("maven.project.version");
/** See {@link TransferEvent.RequestType}. */
public static final AttributeKey<String> MAVEN_TRANSFER_TYPE =
AttributeKey.stringKey("maven.transfer.type");
/** See {@link org.eclipse.aether.transfer.TransferResource}. */
public static final AttributeKey<String> MAVEN_RESOURCE_NAME =
AttributeKey.stringKey("maven.resource.name");
public static final String SERVICE_NAME_VALUE = "maven";
// inlined incubating attribute to prevent direct dependency on incubating semconv