diff --git a/README.md b/README.md index cd95429a..0970b3f1 100644 --- a/README.md +++ b/README.md @@ -27,100 +27,149 @@ Troubleshoot Jenkins performances with distributed tracing of HTTPs requests: Jenkins HTTP request trace with Jaeger

Example Jenkins HTTP trace

- ## Architecture -Using the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector-contrib/releases), you can use many monitoring backends to monitor Jenkins such as Jaeger, Zipkin, Prometheus, Elastic Observability and many others listed [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter). +Using the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector-contrib/releases), you can +use many monitoring backends to monitor Jenkins such as Jaeger, Zipkin, Prometheus, Elastic Observability and many +others listed [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter). Here are example architectures with Elastic, Jaeger, and Prometheus: -| CI/CD Observability with Jaeger and Prometheus | CI/CD Observability with Elastic | -|------------------------------------------------|----------------------------------| -| Jenkins monitoring with Jaeger and Prometheus | Jenkins monitoring with Elastic Observability | +| CI/CD Observability with Jaeger and Prometheus | CI/CD Observability with Elastic | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Jenkins monitoring with Jaeger and Prometheus | Jenkins monitoring with Elastic Observability | ## Getting started -* Set up an OpenTelemetry endpoint such as the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector-contrib) +* Set up an OpenTelemetry endpoint such as + the [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector-contrib) * Install the Jenkins OpenTelemetry plugin -* Configure the Jenkins OpenTelemetry plugin navigating to the "Manage Jenkins / Configure System" screen. In the OpenTelemetry section define: - * "OTLP Endpoint": the hostname and port of the OpenTelemetry GRPC Protocol (OTLP GRPC) endpoint, typically an OpenTelemetry Collector or directly an Observability backend that supports the OTLP GRPC protocol +* Configure the Jenkins OpenTelemetry plugin navigating to the "Manage Jenkins / Configure System" screen. In the + OpenTelemetry section define: + * "OTLP Endpoint": the hostname and port of the OpenTelemetry GRPC Protocol (OTLP GRPC) endpoint, typically an + OpenTelemetry Collector or directly an Observability backend that supports the OTLP GRPC protocol * "Authentication": authentication mechanism used by your OTLP Endpoint * "Header Authentication" : name of the authentication header if header based authentication is used. - * "Bearer Token Authentication": Bearer token when using header based authentication. Note that Elastic APM token authentication uses a "Bearer Token Authentication". + * "Bearer Token Authentication": Bearer token when using header based authentication. Note that Elastic APM + token authentication uses a "Bearer Token Authentication". * "No Authentication" - * Check "Export OpenTelemetry configuration as environment variables" to easily integrate visibility in other build tools (see the otel-cli, the OpenTelemetry Maven extension, the OpenTelemetry Ansible integration...) + * Check "Export OpenTelemetry configuration as environment variables" to easily integrate visibility in other build + tools (see the otel-cli, the OpenTelemetry Maven extension, the OpenTelemetry Ansible integration...) * Visualization: add the backend used to visualize job executions as traces. * Elastic Observability * Jaeger * Zipkin * Custom Observability backend for other visualization solutions -* Set up Jenkins health dashboards on your OpenTelemetry metrics visualization solution. See details including guidance for Elastic Kibana [here](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/monitoring-metrics.md). +* Set up Jenkins health dashboards on your OpenTelemetry metrics visualization solution. See details including guidance + for Elastic Kibana [here](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/monitoring-metrics.md). Sample Configuration

Example Jenkins OpenTelemetry configuration

- ## Setup and Configuration -For details to set up Jenkins with Elastic, Jaeger or Prometheus, to configure the integration including using Jenkins Configuration as Code, see [Setup and Configuration](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/setup-and-configuration.md). - +For details to set up Jenkins with Elastic, Jaeger or Prometheus, to configure the integration including using Jenkins +Configuration as Code, +see [Setup and Configuration](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/setup-and-configuration.md). ## Troubleshooting and Optimizing Jenkins Jobs and Pipelines Using Tracing on the Builds -For details on how to explore and troubleshoot jobs and pipelines builds as traces, see [Traces of Jobs and Pipeline Builds](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/job-traces.md). +For details on how to explore and troubleshoot jobs and pipelines builds as traces, +see [Traces of Jobs and Pipeline Builds](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/job-traces.md). SpringBootPipeline Execution Trace

Example pipeline execution trace of a SpringBoot app built with Maven going through security checks with Snyk, deployed on a Maven repository and published as a Docker image

+## Troubleshooting pipeline plugins and the execution on the Jenkins build agents + +For details on the execution of pipeline plugin steps on the Jenkins build agents, +activate tracing in the Jenkins build agents using: + +``` +otel.instrumentation.jenkins.agent.enabled=true +``` + +To activate detailed traces of the communication from the Jenkins Controller to the Jenkins Agents, activate the +instrumentation of Jenkins remoting with: + +``` +otel.instrumentation.jenkins.remoting.enabled=true +``` + +Note that the instrumentation of Jenkins remoting is not feature complete and may not capture all the communication +between the Jenkins Controller and the Jenkins Agents. + + + ## Troubleshooting Jenkins Performances Using Tracing on the HTTP Requests of the Jenkins Controller -For details to set up Jenkins with Elastic, Jaeger or Prometheus, to configure the integration including using Jenkins Configuration as Code, see [Setup and Configuration](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/setup-and-configuration.md). +For details to set up Jenkins with Elastic, Jaeger or Prometheus, to configure the integration including using Jenkins +Configuration as Code, +see [Setup and Configuration](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/setup-and-configuration.md). ## Jenkins Security Monitor access to Jenkins to detect anomalous behaviours. -For details, see the security logs, metrics, and trace attributes [here](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/security_obs.md). +For details, see the security logs, metrics, and trace +attributes [here](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/security_obs.md). ## Storing Jenkins Pipeline Logs in an Observability Backend -For details on how to store Jenkins pipelines build logs in an Observability backend like Elastic, see [Storing Jenkins Pipeline Logs in an Observability Backend though OpenTelemetry](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/build-logs.md). +For details on how to store Jenkins pipelines build logs in an Observability backend like Elastic or Loki, +see [Storing Jenkins Pipeline Logs in an Observability Backend though OpenTelemetry](https://github.com/jenkinsci/opentelemetry-plugin/blob/master/docs/build-logs.md). Storing Jenkins pipeline logs in Elasticsearch and visualizing logs both in Kibana and through Jenkins GUI

Storing Jenkins pipeline logs in Elasticsearch and visualizing logs both in Kibana and through Jenkins GUI

## Other CI/CD Tools supporting OpenTelemetry traces -List of other CI/CD tools that support OpenTelemetry traces and integrate with the Jenkins OpenTelemetryPlugin creating a distributed traces providing end to end visibility. +List of other CI/CD tools that support OpenTelemetry traces and integrate with the Jenkins OpenTelemetryPlugin creating +a distributed traces providing end to end visibility. ### OpenTelemetry Maven Extension -The [OpenTelemetry Maven Extension](https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/maven-extension/) is a Maven extension to instrument with traces steps of Maven builds, including capturing details of the produced artifacts for traceability. +The [OpenTelemetry Maven Extension](https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/maven-extension/) +is a Maven extension to instrument with traces steps of Maven builds, including capturing details of the produced +artifacts for traceability. -ℹ️ For seamless and turnkey integration of the trace of the Maven builds that use the OpenTelemetry Maven Extension with the Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment variables". +ℹ️ For seamless and turnkey integration of the trace of the Maven builds that use the OpenTelemetry Maven Extension with +the Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment +variables". ### OpenTelemetry Ansible Plugin -The [OpenTelemetry Ansible Plugin](https://docs.ansible.com/ansible/latest/collections/community/general/opentelemetry_callback.html) is an Ansible callback to instrument with traces the tasks of Ansible playbooks. +The [OpenTelemetry Ansible Plugin](https://docs.ansible.com/ansible/latest/collections/community/general/opentelemetry_callback.html) +is an Ansible callback to instrument with traces the tasks of Ansible playbooks. -ℹ️ For seamless and turnkey integration of the trace of the Ansible playbooks that use the OpenTelemetry plugin with the Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment variables". +ℹ️ For seamless and turnkey integration of the trace of the Ansible playbooks that use the OpenTelemetry plugin with the +Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment +variables". ### pytest-otel -The [PyTest Otel Plugin](https://pypi.org/project/pytest-otel/) is a PyTest plugin to report each PyTest test as a span of a trace. +The [PyTest Otel Plugin](https://pypi.org/project/pytest-otel/) is a PyTest plugin to report each PyTest test as a span +of a trace. -ℹ️ For seamless and turnkey integration of the trace of the PyTest tests that use the OpenTelemetry plugin with the Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment variables". +ℹ️ For seamless and turnkey integration of the trace of the PyTest tests that use the OpenTelemetry plugin with the +Jenkins trace, consider in the Jenkins configuration to enable "Export OpenTelemetry configuration as environment +variables". ### 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. +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. ## FAQ ### Enrich your pipeline `sh`, `bat`, and `powershell` steps with meaningful explanation thanks to labels -If you use Jenkins pipelines in conjunction with the `sh`, `bat`, `powershell` steps, then it's highly recommended using the `label` argument to add a meaningful explanation thanks to step labels. Example: +If you use Jenkins pipelines in conjunction with the `sh`, `bat`, `powershell` steps, then it's highly recommended using +the `label` argument to add a meaningful explanation thanks to step labels. Example: ```groovy node { @@ -130,7 +179,8 @@ node { ### Using the OpenTelemetry OTLP/HTTP rather than OTLP/GRPC protocol -Navigate to the Jenkins OpenTelemetry Plugin configuration, in the "Advanced" section, add to the "Configuration Properties text area the following: +Navigate to the Jenkins OpenTelemetry Plugin configuration, in the "Advanced" section, add to the "Configuration +Properties text area the following: ``` otel.exporter.otlp.protocol=http/protobuf @@ -138,15 +188,21 @@ otel.exporter.otlp.protocol=http/protobuf ### Support for disabling the Groovy Sandbox and accessing the Jenkins pipeline logs APIs while enabling the Jenkins OpenTelemetry Plugin -No test have been done on disabling the Groovy Sandbox and accessing the Jenkins pipeline logs APIs while enabling the Jenkins OpenTelemetry Plugin for the following reasons: +No test have been done on disabling the Groovy Sandbox and accessing the Jenkins pipeline logs APIs while enabling the +Jenkins OpenTelemetry Plugin for the following reasons: + * Disabling the Groovy Sandbox is a very advanced use case due to the security implications of doing so -* The surface of Jenkins pipeline logs capabilities exposed by disabling the Groovy sandbox is very broad and goes way beyond the OpenTelemetyr plugin +* The surface of Jenkins pipeline logs capabilities exposed by disabling the Groovy sandbox is very broad and goes way + beyond the OpenTelemetyr plugin -If you are limited with the current capabilities of the Jenkins OpenTelemetry Plugin and consider opening up the Groovy sandbox to workaround these limitations, please prefer to reach out to us creating an enhancement request so we can work together at productizing the proper secured solution to your problem. +If you are limited with the current capabilities of the Jenkins OpenTelemetry Plugin and consider opening up the Groovy +sandbox to workaround these limitations, please prefer to reach out to us creating an enhancement request so we can work +together at productizing the proper secured solution to your problem. ## Learn More -* You can look at this video tutorial to get started: [![Tracing Your Jenkins Pipelines With OpenTelemetry and Jaeger](https://img.youtube.com/vi/3XzVOxvNpGM/0.jpg)](https://www.youtube.com/watch?v=3XzVOxvNpGM) +* You can look at this video tutorial to get + started: [![Tracing Your Jenkins Pipelines With OpenTelemetry and Jaeger](https://img.youtube.com/vi/3XzVOxvNpGM/0.jpg)](https://www.youtube.com/watch?v=3XzVOxvNpGM) * [DevOpsWorld 2021 - Embracing Observability in Jenkins with OpenTelemetry](https://www.devopsworld.com/agenda/session/581459) ## Demos diff --git a/docs/images/jenkins-remoting-instrumentation.png b/docs/images/jenkins-remoting-instrumentation.png new file mode 100644 index 00000000..01d7e3d7 Binary files /dev/null and b/docs/images/jenkins-remoting-instrumentation.png differ diff --git a/pom.xml b/pom.xml index 566b8530..86f17ba6 100644 --- a/pom.xml +++ b/pom.xml @@ -23,9 +23,10 @@ 2.440.3 jenkinsci/${project.artifactId}-plugin 1.40.0 + 1.40.0-32.v65c59076e638 2.6.0 1.25.0-alpha - 1.36.0-alpha + 1.37.0-alpha true 8.14.3 2.30.0 @@ -76,6 +77,16 @@ pom import + + io.opentelemetry.semconv + opentelemetry-semconv + ${opentelemetry-semconv.version} + + + io.opentelemetry.semconv + opentelemetry-semconv-incubating + ${opentelemetry-semconv.version} + jakarta.json jakarta.json-api @@ -104,7 +115,7 @@ io.jenkins.plugins opentelemetry-api - 1.40.0-24.v83ee9a_c6e8d9 + ${jenkins-opentelemetry.version} io.opentelemetry @@ -127,14 +138,12 @@ io.opentelemetry.semconv opentelemetry-semconv - ${opentelemetry-semconv.version} provided io.opentelemetry.semconv opentelemetry-semconv-incubating - ${opentelemetry-semconv.version} provided diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java index 329490e1..77f8bdcc 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java @@ -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; @@ -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"); + } LOGGER.log(Level.FINE, "Configured"); return true; } @@ -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; } @@ -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(", ")); } @NonNull diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryConfiguration.java b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryConfiguration.java index 792dfe22..af271ba7 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryConfiguration.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryConfiguration.java @@ -167,6 +167,22 @@ public Resource toOpenTelemetryResource() { return resourceBuilder.build(); } + /** + * @see io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder#addResourceCustomizer(BiFunction) + */ + @NonNull + public Map toOpenTelemetryResourceAsMap() { + Map 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) { diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java new file mode 100644 index 00000000..69b56d95 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryConfigurerComputerListener.java @@ -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; + +/** + *

Instantiate and configure OpenTelemetry SDKs on the Jenkins build agents

+ *

support TODO 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()) { + return; + } + OpenTelemetryConfiguration openTelemetryConfiguration = jenkinsOpenTelemetryPluginConfiguration.toOpenTelemetryConfiguration(); + Map otelSdkProperties = openTelemetryConfiguration.toOpenTelemetryProperties(); + Map 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() + "'"); + } + } + + @Inject + public void setJenkinsOpenTelemetryPluginConfiguration(JenkinsOpenTelemetryPluginConfiguration jenkinsOpenTelemetryPluginConfiguration) { + this.jenkinsOpenTelemetryPluginConfiguration = jenkinsOpenTelemetryPluginConfiguration; + } + + /** + *

+ * Propagate config change to all the build agents. + *

+ *

+ * TODO only update build agent configuration if it has changed + *

+ */ + @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); + if (!buildAgentsInstrumentationEnabled.get()) { + return; + } + + Map otelSdkProperties = openTelemetryConfiguration.toOpenTelemetryProperties(); + Map otelSdkResourceProperties = openTelemetryConfiguration.toOpenTelemetryResourceAsMap(); + + Computer[] computers = Jenkins.get().getComputers(); + List> 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())); + + if (node instanceof Jenkins) { + // 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)); + } + }); + 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"); + } + } + ); + } + + /** + * @param channel pass the channel rather than using {@link Computer#getChannel()} to support {@link ComputerListener#preOnline(Computer, Channel, FilePath, TaskListener)} use cases + */ + private Future configureOpenTelemetrySdkOnComputer(@Nonnull Computer computer, @Nonnull VirtualChannel channel, Map otelSdkProperties, Map otelSdkResourceProperties) { + Map buildAgentOtelSdkProperties; + Map buildAgentOtelSdkResourceProperties; + final Set 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); + } + } + + public static class OpenTelemetryConfigurerMasterToSlaveCallable extends MasterToSlaveCallable { + static final Logger logger = Logger.getLogger(OpenTelemetryConfigurerMasterToSlaveCallable.class.getName()); + + final Map otelSdkConfigurationProperties; + final Map otelSdkResource; + + public OpenTelemetryConfigurerMasterToSlaveCallable(Map otelSdkConfigurationProperties, Map 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; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryTraceContextPropagatorFileCallableWrapperFactory.java b/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryTraceContextPropagatorFileCallableWrapperFactory.java new file mode 100644 index 00000000..2fce0c88 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/jenkins/OpenTelemetryTraceContextPropagatorFileCallableWrapperFactory.java @@ -0,0 +1,148 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.jenkins; + +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import hudson.FilePath; +import hudson.remoting.DelegatingCallable; +import io.jenkins.plugins.opentelemetry.JenkinsOpenTelemetryPluginConfiguration; +import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; +import io.jenkins.plugins.opentelemetry.opentelemetry.GlobalOpenTelemetrySdk; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.remoting.RoleChecker; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Propagates trace context to Jenkins build agents and, if enabled, create a span on the jenkins agent side for the remoting call. + */ +@Extension +public class OpenTelemetryTraceContextPropagatorFileCallableWrapperFactory extends FilePath.FileCallableWrapperFactory implements OpenTelemetryLifecycleListener { + static final Logger LOGGER = Logger.getLogger(OpenTelemetryTraceContextPropagatorFileCallableWrapperFactory.class.getName()); + + final AtomicBoolean remotingTracingEnabled = new AtomicBoolean(false); + final AtomicBoolean buildAgentsInstrumentationEnabled = new AtomicBoolean(false); + + @Override + public DelegatingCallable wrap(DelegatingCallable callable) { + if (buildAgentsInstrumentationEnabled.get()) { + return new OTelDelegatingCallable<>(callable, remotingTracingEnabled.get()); + } else { + return callable; + } + } + + @Inject + public void setJenkinsOpenTelemetryPluginConfiguration(JenkinsOpenTelemetryPluginConfiguration jenkinsOpenTelemetryPluginConfiguration) { + ConfigProperties configProperties = jenkinsOpenTelemetryPluginConfiguration.getConfigProperties(); + this.buildAgentsInstrumentationEnabled.set(configProperties.getBoolean(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_AGENTS_ENABLED, false)); + this.remotingTracingEnabled.set(configProperties.getBoolean(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_REMOTING_ENABLED, false)); + } + + @Override + public void afterConfiguration(ConfigProperties configProperties) { + this.buildAgentsInstrumentationEnabled.set(configProperties.getBoolean(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_AGENTS_ENABLED, false)); + this.remotingTracingEnabled.set(configProperties.getBoolean(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_REMOTING_ENABLED, false)); + } + + static class OTelDelegatingCallable implements DelegatingCallable { + private static final long serialVersionUID = 1L; + final DelegatingCallable callable; + final Map w3cTraceContext; + final boolean remotingTracingEnabled; + + public OTelDelegatingCallable(DelegatingCallable callable, boolean remotingTracingEnabled) { + this.callable = callable; + this.w3cTraceContext = new HashMap<>(); + W3CTraceContextPropagator.getInstance().inject(Context.current(), w3cTraceContext, (carrier, key, value) -> { + assert carrier != null; + carrier.put(key, value); + }); + this.remotingTracingEnabled = remotingTracingEnabled; + LOGGER.log(Level.FINER, () -> "Wrap " + callable + " to propagate trace context " + w3cTraceContext); + } + + @Override + public ClassLoader getClassLoader() { + return callable.getClassLoader(); + } + + @Override + public V call() throws T { + if (!GlobalOpenTelemetrySdk.isInitialized()) { + LOGGER.log(Level.INFO, () -> "Call " + callable + " before OpenTelemetry SDK was initialized. " + w3cTraceContext); + return callable.call(); + } + Context callerContext = W3CTraceContextPropagator.getInstance().extract(Context.current(), w3cTraceContext, new TextMapGetter<>() { + @Override + public Iterable keys(@Nonnull Map carrier) { + return carrier.keySet(); + } + + @Nullable + @Override + public String get(@Nullable Map carrier, @Nonnull String key) { + assert carrier != null; + return carrier.get(key); + } + }); + LOGGER.log(Level.FINER, () -> "Call " + callable + " with trace context " + w3cTraceContext); + Span span; + if (remotingTracingEnabled) { + String spanName; + String callableToString = callable.toString(); + if ("hudson.FilePath$FileCallableWrapper".equals(callable.getClass().getName()) && StringUtils.contains(callableToString, "@")) { + spanName = StringUtils.substringBefore(callableToString, "@"); + } else { + spanName = "Call"; + } + span = GlobalOpenTelemetry + .getTracer(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME) + .spanBuilder(spanName) + .setParent(callerContext) + .setSpanKind(SpanKind.SERVER) + .setAttribute("jenkins.remoting.callable", callableToString) + .setAttribute("jenkins.remoting.callable.class", callable.getClass().getName()) + .startSpan(); + } else { + span = Span.fromContext(callerContext); + } + + try (Scope scope = span.makeCurrent()) { + return callable.call(); + } catch (Throwable t) { + span.setStatus(StatusCode.ERROR, t.getMessage()); + span.recordException(t); + throw t; + } finally { + span.end(); + } + } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + callable.checkRoles(checker); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogSenderBuildListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogSenderBuildListener.java index 90d77364..4f6b4f8b 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogSenderBuildListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogSenderBuildListener.java @@ -23,7 +23,6 @@ import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,8 +41,6 @@ abstract class OtelLogSenderBuildListener implements BuildListener, OutputStream protected final static Logger LOGGER = Logger.getLogger(OtelLogSenderBuildListener.class.getName()); final RunTraceContext runTraceContext; - final Map otelConfigProperties; - final Map otelResourceAttributes; /** * Timestamps of the logs emitted by the Jenkins Agents must be chronologically ordered with the timestamps of * the logs & traces emitted on the Jenkins controller even if the system clock are not perfectly synchronized @@ -57,10 +54,8 @@ abstract class OtelLogSenderBuildListener implements BuildListener, OutputStream @CheckForNull transient PrintStream logger; - public OtelLogSenderBuildListener(@NonNull RunTraceContext runTraceContext, @NonNull Map otelConfigProperties, @NonNull Map otelResourceAttributes) { + public OtelLogSenderBuildListener(@NonNull RunTraceContext runTraceContext) { this.runTraceContext = runTraceContext; - this.otelConfigProperties = otelConfigProperties; - this.otelResourceAttributes = otelResourceAttributes; this.clock = Clocks.monotonicClock(); // Constructor must always be invoked on the Jenkins Controller. // Instantiation on the Jenkins Agents is done via deserialization. @@ -96,8 +91,8 @@ static final class OtelLogSenderBuildListenerOnController extends OtelLogSenderB private final static Logger logger = Logger.getLogger(OtelLogSenderBuildListenerOnController.class.getName()); - public OtelLogSenderBuildListenerOnController(@NonNull RunTraceContext runTraceContext, @NonNull Map otelConfigProperties, @NonNull Map otelResourceAttributes) { - super(runTraceContext, otelConfigProperties, otelResourceAttributes); + public OtelLogSenderBuildListenerOnController(@NonNull RunTraceContext runTraceContext) { + super(runTraceContext); logger.log(Level.FINEST, () -> "new OtelLogSenderBuildListenerOnController()"); JenkinsJVM.checkJenkinsJVM(); } @@ -118,7 +113,7 @@ public io.opentelemetry.api.logs.Logger getOtelLogger() { private Object writeReplace() throws IOException { logger.log(Level.FINEST, () -> "writeReplace()"); JenkinsJVM.checkJenkinsJVM(); - return new OtelLogSenderBuildListenerOnAgent(runTraceContext, otelConfigProperties, otelResourceAttributes); + return new OtelLogSenderBuildListenerOnAgent(runTraceContext); } } @@ -141,8 +136,8 @@ private static class OtelLogSenderBuildListenerOnAgent extends OtelLogSenderBuil /** * Intended to be exclusively called on the Jenkins Controller by {@link OtelLogSenderBuildListenerOnController#writeReplace()}. */ - private OtelLogSenderBuildListenerOnAgent(@NonNull RunTraceContext runTraceContext, @NonNull Map otelConfigProperties, @NonNull Map otelResourceAttributes) { - super(runTraceContext, otelConfigProperties, otelResourceAttributes); + private OtelLogSenderBuildListenerOnAgent(@NonNull RunTraceContext runTraceContext) { + super(runTraceContext); logger.log(Level.FINEST, () -> "new OtelLogSenderBuildListenerOnAgent()"); JenkinsJVM.checkJenkinsJVM(); } @@ -186,15 +181,6 @@ private Object readResolve() { ); this.clock = Clocks.monotonicOffsetClock(offsetInNanosOnJenkinsAgent); } - - // Setup OTel - GlobalOpenTelemetrySdk.configure( - otelConfigProperties, - otelResourceAttributes, - /* the JVM shutdown hook is too late to flush the Otel signals as the OTel classes have been unloaded */ - false ); - // TODO find the right lifecycle event to shutdown the Otel SDK on agent shutdown - // hudson.remoting.EngineListener doesn't seem to be the right event return this; } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogStorage.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogStorage.java index 55235656..fcf09fb6 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogStorage.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/log/OtelLogStorage.java @@ -8,7 +8,6 @@ import hudson.model.TaskListener; import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry; import io.jenkins.plugins.opentelemetry.JenkinsOpenTelemetryPluginConfiguration; -import io.jenkins.plugins.opentelemetry.OpenTelemetryConfiguration; import io.jenkins.plugins.opentelemetry.job.MonitoringAction; import io.jenkins.plugins.opentelemetry.job.OtelTraceService; import io.jenkins.plugins.opentelemetry.job.log.util.TeeBuildListener; @@ -30,8 +29,6 @@ import java.io.IOException; import java.io.OutputStream; import java.time.Instant; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -72,12 +69,7 @@ public OtelLogStorage(@NonNull Run run, @NonNull OtelTraceService otelTraceServi @NonNull @Override public BuildListener overallListener() throws IOException { - OpenTelemetryConfiguration otelConfiguration = JenkinsOpenTelemetryPluginConfiguration.get().toOpenTelemetryConfiguration(); - Map otelConfigurationProperties = otelConfiguration.toOpenTelemetryProperties(); - Map otelResourceAttributes = new HashMap<>(); - otelConfiguration.toOpenTelemetryResource().getAttributes().asMap().forEach((k, v) -> otelResourceAttributes.put(k.getKey(), v.toString())); - - OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(runTraceContext, otelConfigurationProperties, otelResourceAttributes); + OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(runTraceContext); BuildListener result; if (JenkinsControllerOpenTelemetry.get().isOtelLogsMirrorToDisk()) { @@ -110,14 +102,9 @@ public BuildListener overallListener() throws IOException { @NonNull @Override public BuildListener nodeListener(@NonNull FlowNode flowNode) throws IOException { - OpenTelemetryConfiguration otelConfiguration = JenkinsOpenTelemetryPluginConfiguration.get().toOpenTelemetryConfiguration(); - Map otelConfigurationProperties = otelConfiguration.toOpenTelemetryProperties(); - Map otelResourceAttributes = new HashMap<>(); - otelConfiguration.toOpenTelemetryResource().getAttributes().asMap().forEach((k, v) -> otelResourceAttributes.put(k.getKey(), v.toString())); - Span span = otelTraceService.getSpan(run, flowNode); FlowNodeTraceContext flowNodeTraceContext = FlowNodeTraceContext.newFlowNodeTraceContext(run, flowNode, span); - OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(flowNodeTraceContext, otelConfigurationProperties, otelResourceAttributes); + OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(flowNodeTraceContext); BuildListener result; if (JenkinsControllerOpenTelemetry.get().isOtelLogsMirrorToDisk()) { diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdk.java b/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdk.java index b873f741..5c920aad 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdk.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdk.java @@ -5,254 +5,105 @@ package io.jenkins.plugins.opentelemetry.opentelemetry; +import com.google.common.annotations.VisibleForTesting; +import io.jenkins.plugins.opentelemetry.api.ReconfigurableOpenTelemetry; import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider; -import io.opentelemetry.api.logs.LoggerProvider; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.api.trace.TracerProvider; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; -import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; -import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.instrumentation.resources.ContainerResourceProvider; +import io.opentelemetry.instrumentation.resources.HostIdResourceProvider; +import io.opentelemetry.instrumentation.resources.HostResourceProvider; +import io.opentelemetry.instrumentation.resources.OsResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.resources.Resource; -import net.jcip.annotations.GuardedBy; +import io.opentelemetry.sdk.resources.ResourceBuilder; +import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; /** * Global singleton similar to the {@link io.opentelemetry.api.GlobalOpenTelemetry} in order to also have a * static accessor to the {@link io.opentelemetry.sdk.logs.SdkLoggerProvider} - *

- * TODO handle reconfiguration */ public final class GlobalOpenTelemetrySdk { private final static Logger logger = Logger.getLogger(GlobalOpenTelemetrySdk.class.getName()); - private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final static ReconfigurableOpenTelemetry openTelemetry; - /** - * Used for unit tests - */ - final static AtomicInteger configurationCounter = new AtomicInteger(); + private final static io.opentelemetry.api.logs.Logger otelLogger; - @GuardedBy("readWriteLock") - private static volatile OpenTelemetrySdkState openTelemetrySdkState = new NoopOpenTelemetrySdkState(); + @VisibleForTesting + static OtelSdkConfiguration currentSdkConfiguration = new OtelSdkConfiguration(Collections.emptyMap(), Collections.emptyMap()); + + static { + openTelemetry = ReconfigurableOpenTelemetry.get(); + otelLogger = openTelemetry + .getLogsBridge() + .loggerBuilder(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME) + .build(); + } /** - * Configure if configuration has changed + * Used for unit tests */ - public static void configure(Map configurationProperties, Map resourceAttributes, boolean registerShutDownHook) { - SdkConfigurationParameters sdkConfigurationParameters = new SdkConfigurationParameters(configurationProperties, resourceAttributes); - Lock readLock = readWriteLock.readLock(); - readLock.lock(); - try { - // VERIFY IF CONFIGURATION HAS CHANGED - if (Objects.equals(sdkConfigurationParameters, openTelemetrySdkState.getSdkConfigurationParameters())) { - logger.log(Level.FINEST, () -> "OpenTelemetry SDK configuration has NOT changed, don't reinitialize SDK"); - return; - } - } finally { - readLock.unlock(); - } - Lock writeLock = readWriteLock.writeLock(); - writeLock.lock(); - try { - if (Objects.equals(sdkConfigurationParameters, openTelemetrySdkState.getSdkConfigurationParameters())) { - logger.log(Level.FINEST, () -> "OpenTelemetry SDK configuration has NOT changed"); - return; - } - logger.log(Level.FINEST, () -> "Initialize/reinitialize OpenTelemetry SDK..."); - shutdown(); - AutoConfiguredOpenTelemetrySdkBuilder builder = AutoConfiguredOpenTelemetrySdk.builder() - .addPropertiesSupplier(sdkConfigurationParameters::getConfigurationProperties) - .addResourceCustomizer((resource, configProperties) -> { - AttributesBuilder attributesBuilder = Attributes.builder(); - sdkConfigurationParameters.getResourceAttributes().forEach(attributesBuilder::put); - Attributes attributes = attributesBuilder.build(); - - return Resource.builder() - .putAll(resource) - .putAll(attributes) - .build(); - }); - - if (!registerShutDownHook) { - builder.disableShutdownHook(); - } - OpenTelemetrySdk openTelemetrySdk = builder.build().getOpenTelemetrySdk(); - openTelemetrySdkState = new OpenTelemetrySdkStateImpl(openTelemetrySdk, sdkConfigurationParameters); - logger.log(Level.INFO, () -> "OpenTelemetry SDK initialized"); // TODO dump config details - configurationCounter.incrementAndGet(); - } finally { - writeLock.unlock(); - } + final static AtomicInteger configurationCounter = new AtomicInteger(); + @Nonnull + public static ReconfigurableOpenTelemetry get() { + return GlobalOpenTelemetrySdk.openTelemetry; } - public static CompletableResultCode shutdown() { - Lock writeLock = readWriteLock.writeLock(); - writeLock.lock(); - try { - CompletableResultCode result = openTelemetrySdkState.shutDown(); - openTelemetrySdkState = new NoopOpenTelemetrySdkState(); - return result; - } finally { - writeLock.unlock(); - } + public static boolean isInitialized() { + return GlobalOpenTelemetrySdk.openTelemetry != null; } + @Nonnull public static io.opentelemetry.api.logs.Logger getOtelLogger() { - Lock readLock = readWriteLock.readLock(); - readLock.lock(); - try { - return openTelemetrySdkState.getOtelLogger(); - } finally { - readLock.unlock(); - } - } - - public static Meter getMeter() { - Lock readLock = readWriteLock.readLock(); - readLock.lock(); - try { - return openTelemetrySdkState.getMeter(); - } finally { - readLock.unlock(); - } + return otelLogger; } - public static Tracer getTracer() { - Lock readLock = readWriteLock.readLock(); - readLock.lock(); - try { - return openTelemetrySdkState.getTracer(); - } finally { - readLock.unlock(); - } - } - - private interface OpenTelemetrySdkState { - io.opentelemetry.api.logs.Logger getOtelLogger(); - - Meter getMeter(); - - Tracer getTracer(); - - SdkConfigurationParameters getSdkConfigurationParameters(); - - CompletableResultCode shutDown(); - } - - private static class NoopOpenTelemetrySdkState implements OpenTelemetrySdkState { - @Override - public io.opentelemetry.api.logs.Logger getOtelLogger() { - return LoggerProvider.noop().get(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME); - } - - @Override - public Meter getMeter() { - return MeterProvider.noop().get(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME); - } - - @Override - public Tracer getTracer() { - return TracerProvider.noop().get(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME); - } - - - @Override - public SdkConfigurationParameters getSdkConfigurationParameters() { - return new SdkConfigurationParameters(Collections.emptyMap(), Collections.emptyMap()); - } - - @Override - public CompletableResultCode shutDown() { - return CompletableResultCode.ofSuccess(); - } - } - - private static class OpenTelemetrySdkStateImpl implements OpenTelemetrySdkState { - private final OpenTelemetrySdk openTelemetrySdk; - - private final io.opentelemetry.api.logs.Logger otelLogger; - private final Meter meter; - private final Tracer tracer; - private final SdkConfigurationParameters sdkConfigurationParameters; - - - OpenTelemetrySdkStateImpl(OpenTelemetrySdk openTelemetrySdk, SdkConfigurationParameters sdkConfigurationParameters) { - this.openTelemetrySdk = openTelemetrySdk; - this.sdkConfigurationParameters = sdkConfigurationParameters; - String jenkinsPluginVersion = Optional.ofNullable( - sdkConfigurationParameters.resourceAttributes.get(JenkinsOtelSemanticAttributes.JENKINS_OPEN_TELEMETRY_PLUGIN_VERSION.getKey())) - .orElse("#unknown#"); - this.otelLogger = openTelemetrySdk - .getSdkLoggerProvider() - .loggerBuilder(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME) - .setInstrumentationVersion(jenkinsPluginVersion) - .build(); - this.tracer = openTelemetrySdk - .getTracerProvider().tracerBuilder(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME) - .setInstrumentationVersion(jenkinsPluginVersion) - .build(); - this.meter = openTelemetrySdk - .getMeterProvider() - .meterBuilder(JenkinsOtelSemanticAttributes.INSTRUMENTATION_NAME) - .setInstrumentationVersion(jenkinsPluginVersion) - .build(); - } - - @Override - public io.opentelemetry.api.logs.Logger getOtelLogger() { - return otelLogger; - } - - @Override - public Meter getMeter() { - return meter; - } - - @Override - public Tracer getTracer() { - return tracer; - } - - @Override - public SdkConfigurationParameters getSdkConfigurationParameters() { - return sdkConfigurationParameters; - } - - @Override - public CompletableResultCode shutDown() { - logger.log(Level.FINE, "Shutdown OpenTelemetry..."); // TODO dump config details - CompletableResultCode result = openTelemetrySdk.shutdown(); - GlobalOpenTelemetry.resetForTest(); - GlobalEventLoggerProvider.resetForTest(); - - return result; - } + /** + * Configure if configuration has changed + */ + public static synchronized void configure(Map configurationProperties, Map resourceAttributes, boolean registerShutDownHook) { + OtelSdkConfiguration newOtelSdkConfiguration = new OtelSdkConfiguration(configurationProperties, resourceAttributes); + // VERIFY IF CONFIGURATION HAS CHANGED + if (Objects.equals(newOtelSdkConfiguration, currentSdkConfiguration)) { + logger.log(Level.FINEST, () -> "OpenTelemetry SDK configuration has NOT changed, don't reconfigure SDK"); + return; + } + logger.log(Level.FINEST, () -> "Configure OpenTelemetry SDK..."); + + ConfigProperties configProperties = DefaultConfigProperties.create(configurationProperties); + ResourceBuilder resourceBuilder = Resource.builder(); + resourceBuilder.putAll(new HostResourceProvider().createResource(configProperties)); + resourceBuilder.putAll(new HostIdResourceProvider().createResource(configProperties)); + resourceBuilder.putAll(new ContainerResourceProvider().createResource(configProperties)); + // resourceBuilder.putAll(new ProcessResourceProvider().createResource(configProperties)); + resourceBuilder.putAll(new OsResourceProvider().createResource(configProperties)); + resourceAttributes + .entrySet().stream() + .filter(Predicate.not(entry -> Objects.equals(ServiceIncubatingAttributes.SERVICE_INSTANCE_ID.getKey(), entry.getKey()))) + .forEach(entry -> resourceBuilder.put(entry.getKey(), entry.getValue())); + Resource resource = resourceBuilder.build(); + + get().configure(configurationProperties, resource, registerShutDownHook); + currentSdkConfiguration = newOtelSdkConfiguration; + configurationCounter.incrementAndGet(); } - static class SdkConfigurationParameters { + static class OtelSdkConfiguration { private final Map configurationProperties; private final Map resourceAttributes; - public SdkConfigurationParameters(Map configurationProperties, Map resourceAttributes) { + public OtelSdkConfiguration(Map configurationProperties, Map resourceAttributes) { this.configurationProperties = configurationProperties; this.resourceAttributes = resourceAttributes; } @@ -269,7 +120,7 @@ public Map getResourceAttributes() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - SdkConfigurationParameters that = (SdkConfigurationParameters) o; + OtelSdkConfiguration that = (OtelSdkConfiguration) o; return Objects.equals(configurationProperties, that.configurationProperties) && Objects.equals(resourceAttributes, that.resourceAttributes); } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java index 4723c6c9..f03d3199 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java @@ -135,6 +135,14 @@ public final class JenkinsOtelSemanticAttributes extends JenkinsAttributes { public static final String OTEL_INSTRUMENTATION_JENKINS_WEB_ENABLED = "otel.instrumentation.jenkins.web.enabled"; public static final String OTEL_INSTRUMENTATION_JENKINS_REMOTE_SPAN_ENABLED = "otel.instrumentation.jenkins.remote.span.enabled"; + /** + * Instrument Jenkins Remoting from the Jenkins controller to Jenkins build agents + */ + public static final String OTEL_INSTRUMENTATION_JENKINS_REMOTING_ENABLED = "otel.instrumentation.jenkins.remoting.enabled"; + /** + * Instrument Jenkins build agents + */ + public static final String OTEL_INSTRUMENTATION_JENKINS_AGENTS_ENABLED = "otel.instrumentation.jenkins.agent.enabled"; /** * https://opentelemetry.io/docs/zero-code/java/agent/configuration/#capturing-servlet-request-parameters */ diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsControllerOpenTelemetryTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsControllerOpenTelemetryTest.java index 93f63515..19aa863e 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsControllerOpenTelemetryTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsControllerOpenTelemetryTest.java @@ -58,7 +58,7 @@ private void testDefaultConfigurationOverwrite(String serviceNameDefinedInConfig Optional.empty(), configurationProperties); - ReconfigurableOpenTelemetry reconfigurableOpenTelemetry = new ReconfigurableOpenTelemetry(); + ReconfigurableOpenTelemetry reconfigurableOpenTelemetry = ReconfigurableOpenTelemetry.get(); JenkinsControllerOpenTelemetry jenkinsControllerOpenTelemetry = new JenkinsControllerOpenTelemetry(); jenkinsControllerOpenTelemetry.openTelemetry = reconfigurableOpenTelemetry; jenkinsControllerOpenTelemetry.initialize(openTelemetryConfiguration); diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/backend/elastic/ElasticStack.java b/src/test/java/io/jenkins/plugins/opentelemetry/backend/elastic/ElasticStack.java index 075f5c8c..f1f4254b 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/backend/elastic/ElasticStack.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/backend/elastic/ElasticStack.java @@ -130,7 +130,7 @@ public void createLogIndex() throws IOException { public void configureElasticBackEnd() { // initialize ReconfigurableOpenTelemetry and set it as GlobalOpenTelemetry instance - ReconfigurableOpenTelemetry reconfigurableOpenTelemetry = ExtensionList.lookupSingleton(ReconfigurableOpenTelemetry.class); + ReconfigurableOpenTelemetry reconfigurableOpenTelemetry = ReconfigurableOpenTelemetry.get(); GlobalOpenTelemetry.resetForTest(); GlobalOpenTelemetry.set(reconfigurableOpenTelemetry); @@ -144,7 +144,7 @@ public void configureElasticBackEnd() { elasticBackendConfiguration.setKibanaBaseUrl(getKibanaUrl()); elasticStackConfiguration.setElasticsearchUrl(getEsUrl()); // FIXME the configuration is not applied if you not save the configuration - configuration.initializeOpenTelemetry(); + configuration.configureOpenTelemetrySdk(); elasticsearchRetriever = configuration.getLogStorageRetriever(); } diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdkTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdkTest.java index 57ea8639..0baf3c79 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdkTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/opentelemetry/GlobalOpenTelemetrySdkTest.java @@ -5,11 +5,14 @@ package io.jenkins.plugins.opentelemetry.opentelemetry; +import io.jenkins.plugins.opentelemetry.api.ReconfigurableOpenTelemetry; +import io.opentelemetry.api.logs.Logger; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.semconv.ServiceAttributes; import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes; import org.junit.Test; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -17,19 +20,20 @@ public class GlobalOpenTelemetrySdkTest { + @Test public void testNotSdkConfigured() { try { - GlobalOpenTelemetrySdk.getOtelLogger(); - GlobalOpenTelemetrySdk.getMeter(); - GlobalOpenTelemetrySdk.getTracer(); + ReconfigurableOpenTelemetry openTelemetry = GlobalOpenTelemetrySdk.get(); + Logger otelLogger = GlobalOpenTelemetrySdk.getOtelLogger(); } finally { - GlobalOpenTelemetrySdk.shutdown(); + GlobalOpenTelemetrySdk.get().close(); } } @Test public void testSdkSetConfigurationOnce() { + GlobalOpenTelemetrySdk.currentSdkConfiguration = new GlobalOpenTelemetrySdk.OtelSdkConfiguration(Collections.emptyMap(), Collections.emptyMap()); try { Map config = new HashMap<>(); config.put("otel.traces.exporter", "none"); @@ -48,7 +52,7 @@ public void testSdkSetConfigurationOnce() { GlobalOpenTelemetrySdk.configure(config, resourceAttributes, false); assertEquals("Configuration counter", configurationCountBefore + 1, GlobalOpenTelemetrySdk.configurationCounter.get()); } finally { - GlobalOpenTelemetrySdk.shutdown(); + GlobalOpenTelemetrySdk.get().close(); } } @@ -83,7 +87,7 @@ public void testSdkSetConfiguration_twice_with_same_configProperties_and_resourc assertEquals("Configuration counter", configurationCountBefore + 1, GlobalOpenTelemetrySdk.configurationCounter.get()); } } finally { - GlobalOpenTelemetrySdk.shutdown(); + GlobalOpenTelemetrySdk.get().close(); } } @@ -128,7 +132,7 @@ public void testSdkSetConfiguration_twice_with_same_configProperties_and_differe assertEquals("Configuration counter", configurationCountBefore + 2, GlobalOpenTelemetrySdk.configurationCounter.get()); } } finally { - GlobalOpenTelemetrySdk.shutdown(); + GlobalOpenTelemetrySdk.get().close(); } } @@ -172,7 +176,7 @@ public void testSdkSetConfiguration_twice_with_different_configProperties_and_sa assertEquals("Configuration counter", configurationCountBefore + 2, GlobalOpenTelemetrySdk.configurationCounter.get()); } } finally { - GlobalOpenTelemetrySdk.shutdown(); + GlobalOpenTelemetrySdk.get().close(); } } } \ No newline at end of file