Skip to content

Commit

Permalink
Merge pull request #908 from jenkinsci/context-propagation-to-build-a…
Browse files Browse the repository at this point in the history
…gents

Propagate trace context to Jenkins build agents
  • Loading branch information
cyrille-leclerc authored Aug 30, 2024
2 parents 50772a4 + 3ddf10e commit bf1dcbe
Show file tree
Hide file tree
Showing 14 changed files with 568 additions and 301 deletions.
120 changes: 88 additions & 32 deletions README.md

Large diffs are not rendered by default.

Binary file added docs/images/jenkins-remoting-instrumentation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 13 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
<jenkins.version>2.440.3</jenkins.version>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<opentelemetry.version>1.40.0</opentelemetry.version>
<jenkins-opentelemetry.version>1.40.0-32.v65c59076e638</jenkins-opentelemetry.version>
<opentelemetry-instrumentation.version>2.6.0</opentelemetry-instrumentation.version>
<opentelemetry-semconv.version>1.25.0-alpha</opentelemetry-semconv.version>
<opentelemetry-contrib.version>1.36.0-alpha</opentelemetry-contrib.version>
<opentelemetry-contrib.version>1.37.0-alpha</opentelemetry-contrib.version>
<useBeta>true</useBeta>
<elasticstack.version>8.14.3</elasticstack.version>
<error-prone.version>2.30.0</error-prone.version>
Expand Down Expand Up @@ -76,6 +77,16 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>${opentelemetry-semconv.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv-incubating</artifactId>
<version>${opentelemetry-semconv.version}</version>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
Expand Down Expand Up @@ -104,7 +115,7 @@
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.40.0-24.v83ee9a_c6e8d9</version>
<version>${jenkins-opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
Expand All @@ -127,14 +138,12 @@
<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>${opentelemetry-semconv.version}</version>
<!-- provided by io.jenkins.plugins:opentelemetry-api -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry.semconv</groupId>
<artifactId>opentelemetry-semconv-incubating</artifactId>
<version>${opentelemetry-semconv.version}</version>
<!-- provided by io.jenkins.plugins:opentelemetry-api -->
<scope>provided</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes;
import io.jenkins.plugins.opentelemetry.semconv.OTelEnvironmentVariablesConventions;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.ServiceAttributes;
import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes;
Expand Down Expand Up @@ -175,8 +176,13 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti
// stapler oddity, empty lists coming from the HTTP request are not set on bean by `req.bindJSON(this, json)`
this.observabilityBackends = req.bindJSONToList(ObservabilityBackend.class, json.get("observabilityBackends"));
this.endpoint = sanitizeOtlpEndpoint(this.endpoint);
initializeOpenTelemetry();
save();
try {
configureOpenTelemetrySdk();
save();
} catch (ConfigurationException e) {
LOGGER.log(Level.WARNING, "Exception configuring OpenTelemetry SDK", e);
throw new FormException("Exception configuring OpenTelemetry SDK: " + e.getMessage(), e, "endpoint");
}

Check warning on line 185 in src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 180-185 are not covered by tests
LOGGER.log(Level.FINE, "Configured");
return true;
}
Expand Down Expand Up @@ -225,13 +231,16 @@ public OpenTelemetryConfiguration toOpenTelemetryConfiguration() {
*/
@Initializer(after = InitMilestone.SYSTEM_CONFIG_ADAPTED, before = InitMilestone.JOB_LOADED)
@SuppressWarnings("MustBeClosedChecker")
public void initializeOpenTelemetry() {
LOGGER.log(Level.FINE, "Initialize Jenkins OpenTelemetry Plugin...");
public void configureOpenTelemetrySdk() {
LOGGER.log(Level.FINE, "Configure OpenTelemetry SDK...");
OpenTelemetryConfiguration newOpenTelemetryConfiguration = toOpenTelemetryConfiguration();
if (Objects.equals(this.currentOpenTelemetryConfiguration, newOpenTelemetryConfiguration)) {
LOGGER.log(Level.FINE, "Configuration didn't change, skip reconfiguration");
} else {
openTelemetry.configure(newOpenTelemetryConfiguration.toOpenTelemetryProperties(), newOpenTelemetryConfiguration.toOpenTelemetryResource());
openTelemetry.configure(
newOpenTelemetryConfiguration.toOpenTelemetryProperties(),
newOpenTelemetryConfiguration.toOpenTelemetryResource(),
true);
this.currentOpenTelemetryConfiguration = newOpenTelemetryConfiguration;
}

Expand Down Expand Up @@ -406,7 +415,7 @@ public void setJenkinsLocationConfiguration(@NonNull JenkinsLocationConfiguratio
*/
@NonNull
public String getVisualisationObservabilityBackendsString() {
return "Visualisation observability backends: " + ObservabilityBackend.allDescriptors().stream().sorted().map(d -> d.getDisplayName()).collect(Collectors.joining(", "));
return "Visualisation observability backends: " + ObservabilityBackend.allDescriptors().stream().sorted().map(Descriptor::getDisplayName).collect(Collectors.joining(", "));

Check warning on line 418 in src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 418 is not covered by tests
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,22 @@ public Resource toOpenTelemetryResource() {

return resourceBuilder.build();
}
/**
* @see io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder#addResourceCustomizer(BiFunction)
*/
@NonNull
public Map<String, String> toOpenTelemetryResourceAsMap() {
Map<String, String> resourceMap = new HashMap<>();
this.getServiceName().ifPresent(serviceName ->
resourceMap.put(ServiceAttributes.SERVICE_NAME.getKey(), serviceName));

this.getServiceNamespace().ifPresent(serviceNamespace ->
resourceMap.put(ServiceIncubatingAttributes.SERVICE_NAMESPACE.getKey(), serviceNamespace));

resourceMap.put(JenkinsOtelSemanticAttributes.JENKINS_OPEN_TELEMETRY_PLUGIN_VERSION.getKey(), OtelUtils.getOpentelemetryPluginVersion());

return resourceMap;
}

@Override
public boolean equals(Object o) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright The Original Author or Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.jenkins.plugins.opentelemetry.jenkins;

import hudson.Extension;
import hudson.FilePath;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.slaves.ComputerListener;
import io.jenkins.plugins.opentelemetry.JenkinsOpenTelemetryPluginConfiguration;
import io.jenkins.plugins.opentelemetry.OpenTelemetryConfiguration;
import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener;
import io.jenkins.plugins.opentelemetry.api.semconv.JenkinsAttributes;
import io.jenkins.plugins.opentelemetry.opentelemetry.GlobalOpenTelemetrySdk;
import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.semconv.ServiceAttributes;
import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* <p>Instantiate and configure OpenTelemetry SDKs on the Jenkins build agents</p>
* <p>support TODO support disabling OTel SDKs on configuration change, after it has been enabled</p>

Check warning on line 50 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: support disabling OTel SDKs on configuration change, after it has been enabled
*/
@Extension(ordinal = Integer.MAX_VALUE)
public class OpenTelemetryConfigurerComputerListener extends ComputerListener implements OpenTelemetryLifecycleListener {

private static final Logger logger = Logger.getLogger(OpenTelemetryConfigurerComputerListener.class.getName());

final AtomicBoolean buildAgentsInstrumentationEnabled = new AtomicBoolean(false);

JenkinsOpenTelemetryPluginConfiguration jenkinsOpenTelemetryPluginConfiguration;

@Override
public void preOnline(Computer computer, Channel channel, FilePath root, TaskListener listener) {
if (!buildAgentsInstrumentationEnabled.get()) {

Check warning on line 63 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 63 is only partially covered, one branch is missing
return;

Check warning on line 64 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 64 is not covered by tests
}
OpenTelemetryConfiguration openTelemetryConfiguration = jenkinsOpenTelemetryPluginConfiguration.toOpenTelemetryConfiguration();
Map<String, String> otelSdkProperties = openTelemetryConfiguration.toOpenTelemetryProperties();
Map<String, String> otelSdkResourceProperties = openTelemetryConfiguration.toOpenTelemetryResourceAsMap();

try {
Object result = configureOpenTelemetrySdkOnComputer(computer, channel, otelSdkProperties, otelSdkResourceProperties).get();
logger.log(Level.FINE, () -> "Updated OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "' with result: " + result);
} catch (InterruptedException e) {
logger.log(Level.INFO, e, () -> "Failure to update OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "'");
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
logger.log(Level.INFO, e, () -> "Failure to update OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "'");

Check warning on line 77 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 73-77 are not covered by tests
}
}

@Inject
public void setJenkinsOpenTelemetryPluginConfiguration(JenkinsOpenTelemetryPluginConfiguration jenkinsOpenTelemetryPluginConfiguration) {
this.jenkinsOpenTelemetryPluginConfiguration = jenkinsOpenTelemetryPluginConfiguration;
}

/**
* <p>
* Propagate config change to all the build agents.
* </p>
* <p>
* TODO only update build agent configuration if it has changed

Check warning on line 91 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: only update build agent configuration if it has changed
* </p>
*/
@Override
public void afterConfiguration(ConfigProperties configProperties) {

// Update the configuration of the Jenkins build agents
OpenTelemetryConfiguration openTelemetryConfiguration = jenkinsOpenTelemetryPluginConfiguration.toOpenTelemetryConfiguration();

boolean otlpLogsEnabled = "otlp".equals(configProperties.getString("otel.logs.exporter")); // pipeline logs export to OTLP endpoint activated
boolean jenkinsAgentInstrumentationDisabled = "false".equalsIgnoreCase(configProperties.getString(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_AGENTS_ENABLED));
this.buildAgentsInstrumentationEnabled.set(otlpLogsEnabled || !jenkinsAgentInstrumentationDisabled);

Check warning on line 102 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 102 is only partially covered, one branch is missing
if (!buildAgentsInstrumentationEnabled.get()) {

Check warning on line 103 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 103 is only partially covered, one branch is missing
return;

Check warning on line 104 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 104 is not covered by tests
}

Map<String, String> otelSdkProperties = openTelemetryConfiguration.toOpenTelemetryProperties();
Map<String, String> otelSdkResourceProperties = openTelemetryConfiguration.toOpenTelemetryResourceAsMap();

Computer[] computers = Jenkins.get().getComputers();
List<Future<Object>> configureAgentResults = new ArrayList<>(computers.length);
Arrays.stream(computers).forEach(computer -> {
Node node = computer.getNode();
VirtualChannel channel = computer.getChannel();

logger.log(Level.FINE, () ->
"Evaluate computer.name: '" + computer.getName() +
"', node: " + Optional.ofNullable(node).map(n -> n.getNodeName() + " / " + n.getClass().getName()));

Check warning on line 118 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 117-118 are not covered by tests

if (node instanceof Jenkins) {

Check warning on line 120 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 120 is only partially covered, one branch is missing
// skip Jenkins controller
} else if (channel == null) {
logger.log(Level.FINE, () -> "Failure to update OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "' as its channel is null, probably offline");
} else {
configureAgentResults.add(configureOpenTelemetrySdkOnComputer(computer, channel, otelSdkProperties, otelSdkResourceProperties));

Check warning on line 125 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 122-125 are not covered by tests
}
});
configureAgentResults.forEach(result -> {
try {
result.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.log(Level.WARNING, e, () -> "Failure to update OpenTelemetry configuration for computer/build-agent");
}
}

Check warning on line 134 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 130-134 are not covered by tests
);
}

/**
* @param channel pass the channel rather than using {@link Computer#getChannel()} to support {@link ComputerListener#preOnline(Computer, Channel, FilePath, TaskListener)} use cases
*/
private Future<Object> configureOpenTelemetrySdkOnComputer(@Nonnull Computer computer, @Nonnull VirtualChannel channel, Map<String, String> otelSdkProperties, Map<String, String> otelSdkResourceProperties) {
Map<String, String> buildAgentOtelSdkProperties;
Map<String, String> buildAgentOtelSdkResourceProperties;
final Set<String> filteredResourceKeys = Set.of(
ServiceAttributes.SERVICE_NAME.getKey(),
ServiceIncubatingAttributes.SERVICE_INSTANCE_ID.getKey()
);
buildAgentOtelSdkProperties = new HashMap<>(otelSdkProperties);
buildAgentOtelSdkResourceProperties = new HashMap<>();
otelSdkResourceProperties
.entrySet()
.stream()
.filter(Predicate.not(entry -> filteredResourceKeys.contains(entry.getKey())))
.forEach(entry -> buildAgentOtelSdkResourceProperties.put(entry.getKey(), entry.getValue()));
// use the same service.name for the Jenkins build agent in order to not break visualization
// of pipeline logs stored externally (Loki, Elasticsearch...) as these visualization logics
// may query on the service name
String serviceName = Optional.ofNullable(otelSdkResourceProperties.get(ServiceAttributes.SERVICE_NAME.getKey())).orElse(JenkinsAttributes.JENKINS);// + "-agent";
buildAgentOtelSdkResourceProperties.put(ServiceAttributes.SERVICE_NAME.getKey(), serviceName);
buildAgentOtelSdkResourceProperties.put(JenkinsOtelSemanticAttributes.JENKINS_COMPUTER_NAME.getKey(), computer.getName());
buildAgentOtelSdkResourceProperties.put(JenkinsOtelSemanticAttributes.JENKINS_COMPUTER_NAME.getKey(), computer.getName());

OpenTelemetryConfigurerMasterToSlaveCallable callable;
callable = new OpenTelemetryConfigurerMasterToSlaveCallable(buildAgentOtelSdkProperties, buildAgentOtelSdkResourceProperties);

try {
logger.log(Level.FINE, () -> "Updating OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "'...");
return channel.callAsync(callable);
} catch (IOException | RuntimeException e) {
logger.log(Level.INFO, e, () -> "Failure to update OpenTelemetry configuration for computer/build-agent '" + computer.getName() + "'");
return CompletableFuture.completedFuture(e);

Check warning on line 171 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 169-171 are not covered by tests
}
}

public static class OpenTelemetryConfigurerMasterToSlaveCallable extends MasterToSlaveCallable<Object, RuntimeException> {
static final Logger logger = Logger.getLogger(OpenTelemetryConfigurerMasterToSlaveCallable.class.getName());

final Map<String, String> otelSdkConfigurationProperties;
final Map<String, String> otelSdkResource;

public OpenTelemetryConfigurerMasterToSlaveCallable(Map<String, String> otelSdkConfigurationProperties, Map<String, String> otelSdkResource) {
this.otelSdkConfigurationProperties = otelSdkConfigurationProperties;
this.otelSdkResource = otelSdkResource;
}

@Override
public Object call() throws RuntimeException {
logger.log(Level.FINE, () -> "Configure OpenTelemetry SDK with properties: " + otelSdkConfigurationProperties + ", resource:" + otelSdkResource);
GlobalOpenTelemetrySdk.configure(otelSdkConfigurationProperties, otelSdkResource, true);
return null;

Check warning on line 190 in src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 188-190 are not covered by tests
}
}
}
Loading

0 comments on commit bf1dcbe

Please sign in to comment.