diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java index 8df382bfc..6acd0c617 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java @@ -10,6 +10,7 @@ import hudson.PluginWrapper; import hudson.model.Describable; import hudson.model.Descriptor; +import hudson.scm.SCM; import hudson.tasks.BuildStep; import hudson.util.FormValidation; import io.jenkins.plugins.opentelemetry.authentication.NoAuthentication; @@ -338,11 +339,21 @@ private Descriptor getStepDescriptor(@Nonnull FlowNode no return descriptor; } + @Nullable + private Descriptor getScmDescriptor(@Nonnull SCM scm) { + return Jenkins.get().getDescriptor((Class) scm.getClass()); + } + @Nullable private Descriptor getBuildStepDescriptor(@Nonnull BuildStep buildStep) { return Jenkins.get().getDescriptor((Class) buildStep.getClass()); } + @Nonnull + public StepPlugin findStepPluginOrDefault(@Nonnull String scmName, @Nonnull SCM scm) { + return findStepPluginOrDefault(scmName, getScmDescriptor(scm)); + } + @Nonnull public StepPlugin findStepPluginOrDefault(@Nonnull String buildStepName, @Nonnull BuildStep buildStep) { return findStepPluginOrDefault(buildStepName, getBuildStepDescriptor(buildStep)); @@ -383,6 +394,11 @@ public String findSymbolOrDefault(@Nonnull String buildStepName, @Nonnull BuildS return findSymbolOrDefault(buildStepName, getBuildStepDescriptor(buildStep)); } + @Nonnull + public String findSymbolOrDefault(@Nonnull String scmName, @Nonnull SCM scm) { + return findSymbolOrDefault(scmName, getScmDescriptor(scm)); + } + @Nonnull public String findSymbolOrDefault(@Nonnull String buildStepName, @Nullable Descriptor descriptor) { String value = buildStepName; diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringBuildStepListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringBuildStepListener.java index 5dc11a753..cecdf2fcf 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringBuildStepListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringBuildStepListener.java @@ -23,7 +23,6 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; -import jenkins.model.Jenkins; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringSCMListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringSCMListener.java new file mode 100644 index 000000000..4e159a841 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringSCMListener.java @@ -0,0 +1,107 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job; + +import com.google.errorprone.annotations.MustBeClosed; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.FilePath; +import hudson.model.AbstractBuild; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.model.listeners.SCMListener; +import hudson.scm.SCM; +import hudson.scm.SCMRevisionState; +import hudson.tasks.BuildStep; +import io.jenkins.plugins.opentelemetry.JenkinsOpenTelemetryPluginConfiguration; +import io.jenkins.plugins.opentelemetry.OtelUtils; +import io.jenkins.plugins.opentelemetry.job.opentelemetry.context.RunContextKey; +import io.jenkins.plugins.opentelemetry.job.opentelemetry.context.ScmContextKey; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.google.common.base.Verify.verifyNotNull; +import static io.jenkins.plugins.opentelemetry.OtelUtils.JENKINS_CORE; + +@Extension +public class MonitoringSCMListener extends SCMListener { + + protected static final Logger LOGGER = Logger.getLogger(MonitoringRunListener.class.getName()); + + private OtelTraceService otelTraceService; + private Tracer tracer; + + @Override + public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListener listener, @CheckForNull File changelogFile, @CheckForNull SCMRevisionState pollingBaseline) throws Exception { + + String stepName = JenkinsOpenTelemetryPluginConfiguration.get().findSymbolOrDefault(scm.getClass().getSimpleName(), scm); + + try (Scope ignored = setupContext(build, scm)) { + verifyNotNull(ignored, "%s - No span found for step %s", build, scm); + + SpanBuilder spanBuilder = getTracer().spanBuilder(stepName); + JenkinsOpenTelemetryPluginConfiguration.StepPlugin stepPlugin = JenkinsOpenTelemetryPluginConfiguration.get().findStepPluginOrDefault(stepName, scm); + + final String jenkinsVersion = OtelUtils.getJenkinsVersion(); + spanBuilder + .setParent(Context.current()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_NAME, stepName) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_NAME, stepPlugin.isUnknown() ? JENKINS_CORE : stepPlugin.getName()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION, stepPlugin.isUnknown() ? jenkinsVersion : stepPlugin.getVersion()); + + // TODO: enriche span with the SCM attributes (similar to io.jenkins.plugins.opentelemetry.job.step.GitStepHandler.createSpanBuilder) + Span atomicScmSpan = spanBuilder.startSpan(); + LOGGER.log(Level.FINE, () -> build.getFullDisplayName() + " - > " + stepName + " - begin " + OtelUtils.toDebugString(atomicScmSpan)); + + getTracerService().putSpan(build, atomicScmSpan); + } + } + + /** + * @return {@code null} if no {@link Span} has been created for the {@link AbstractBuild} of the given {@link BuildStep} + */ + @javax.annotation.CheckForNull + @MustBeClosed + protected Scope setupContext(Run build, @Nonnull SCM scm) { + build = verifyNotNull(build, "%s No build found for step %s", build, scm); + Span span = this.otelTraceService.getSpan(build, scm); + + Scope scope = span.makeCurrent(); + Context.current().with(RunContextKey.KEY, build).with(ScmContextKey.KEY, scm); + return scope; + } + + @Inject + public final void setOpenTelemetryTracerService(@Nonnull OtelTraceService otelTraceService) { + this.otelTraceService = otelTraceService; + this.tracer = this.otelTraceService.getTracer(); + } + + @Nonnull + public OtelTraceService getTracerService() { + return otelTraceService; + } + + @Nonnull + public Tracer getTracer() { + return tracer; + } + + @Override + public String toString() { + return "MonitoringBuildStepListener{}"; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java index 6aeb7f50e..7562cbb53 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java @@ -13,7 +13,9 @@ import com.google.errorprone.annotations.MustBeClosed; import hudson.Extension; import hudson.model.AbstractBuild; +import hudson.model.Job; import hudson.model.Run; +import hudson.scm.SCM; import hudson.tasks.BuildStep; import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider; import io.jenkins.plugins.opentelemetry.job.opentelemetry.context.RunContextKey; @@ -121,7 +123,7 @@ public Span getSpan(@Nonnull Run run, FlowNode flowNode) { @Nonnull public Span getSpan(@Nonnull AbstractBuild build, @Nonnull BuildStep buildStep) { - RunIdentifier runIdentifier = OtelTraceService.RunIdentifier.fromBuild(build); + RunIdentifier runIdentifier = OtelTraceService.RunIdentifier.fromAbstractBuild(build); FreestyleRunSpans freestyleRunSpans = freestyleSpansByRun.computeIfAbsent(runIdentifier, runIdentifier1 -> new FreestyleRunSpans()); // absent when Jenkins restarts during build LOGGER.log(Level.FINEST, () -> "getSpan(" + build.getFullDisplayName() + ", BuildStep[name" + buildStep.getClass().getSimpleName() + ") - " + freestyleRunSpans); @@ -134,6 +136,22 @@ public Span getSpan(@Nonnull AbstractBuild build, @Nonnull BuildStep buildStep) return span; } + @Nonnull + public Span getSpan(@Nonnull Run build, @Nonnull SCM scm) { + + RunIdentifier runIdentifier = OtelTraceService.RunIdentifier.fromBuild(build); + FreestyleRunSpans freestyleRunSpans = freestyleSpansByRun.computeIfAbsent(runIdentifier, runIdentifier1 -> new FreestyleRunSpans()); // absent when Jenkins restarts during build + LOGGER.log(Level.FINEST, () -> "getSpan(" + build.getFullDisplayName() + ", ScmStep[name" + scm.getClass().getSimpleName() + ") - " + freestyleRunSpans); + + final Span span = Iterables.getLast(freestyleRunSpans.runPhasesSpans, null); + if (span == null) { + LOGGER.log(Level.FINE, () -> "No span found for run " + build.getFullDisplayName() + ", Jenkins server may have restarted"); + return noOpTracer.spanBuilder("noop-recovery-run-span-for-" + build.getFullDisplayName()).startSpan(); + } + LOGGER.log(Level.FINEST, () -> "span: " + span.getSpanContext().getSpanId()); + return span; + } + /** * Return the chain of enclosing flowNodes including the given flow node. If the given flow node is a step end node, * the associated step start node is also added. @@ -246,7 +264,7 @@ public void removeJobPhaseSpan(@Nonnull Run run, @Nonnull Span span) { } public void removeBuildStepSpan(@Nonnull AbstractBuild build, @Nonnull BuildStep buildStep, @Nonnull Span span) { - RunIdentifier runIdentifier = RunIdentifier.fromBuild(build); + RunIdentifier runIdentifier = RunIdentifier.fromAbstractBuild(build); FreestyleRunSpans freestyleRunSpans = this.freestyleSpansByRun.computeIfAbsent(runIdentifier, runIdentifier1 -> new FreestyleRunSpans()); // absent when Jenkins restarts during build Span lastSpan = Iterables.getLast(freestyleRunSpans.runPhasesSpans, null); @@ -277,7 +295,7 @@ public void purgeRun(@Nonnull Run run) { } public void putSpan(@Nonnull AbstractBuild build, @Nonnull Span span) { - RunIdentifier runIdentifier = RunIdentifier.fromBuild(build); + RunIdentifier runIdentifier = RunIdentifier.fromAbstractBuild(build); FreestyleRunSpans runSpans = freestyleSpansByRun.computeIfAbsent(runIdentifier, runIdentifier1 -> new FreestyleRunSpans()); runSpans.runPhasesSpans.add(span); @@ -456,33 +474,29 @@ public static class RunIdentifier implements Comparable { final int runNumber; static RunIdentifier fromRun(@Nonnull Run run) { - try { - return new RunIdentifier(run.getParent().getFullName(), run.getNumber()); - } catch (StackOverflowError e) { - // TODO remove when https://github.com/jenkinsci/opentelemetry-plugin/issues/87 is fixed - try { - final String jobName = run.getParent().getName(); - LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for " + jobName + "#" + run.getNumber()); - return new RunIdentifier("#StackOverflowError#_" + jobName, run.getNumber()); - } catch (StackOverflowError e2) { - LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for unknown job #" + run.getNumber()); - return new RunIdentifier("#StackOverflowError#", run.getNumber()); - } - } + return fromBuild(run.getParent(), run.getNumber()); + } + + static RunIdentifier fromAbstractBuild(@Nonnull AbstractBuild build) { + return fromBuild(build.getParent(), build.getNumber()); + } + + static RunIdentifier fromBuild(@Nonnull Run build) { + return fromBuild(build.getParent(), build.getNumber()); } - static RunIdentifier fromBuild(@Nonnull AbstractBuild build) { + static RunIdentifier fromBuild(@Nonnull Job parent, int number) { try { - return new RunIdentifier(build.getParent().getFullName(), build.getNumber()); + return new RunIdentifier(parent.getFullName(), number); } catch (StackOverflowError e) { // TODO remove when https://github.com/jenkinsci/opentelemetry-plugin/issues/87 is fixed try { - final String jobName = build.getParent().getName(); - LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for " + jobName + "#" + build.getNumber()); - return new RunIdentifier("#StackOverflowError#_" + jobName, build.getNumber()); + final String jobName = parent.getName(); + LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for " + jobName + "#" + number); + return new RunIdentifier("#StackOverflowError#_" + jobName, number); } catch (StackOverflowError e2) { - LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for unknown job #" + build.getNumber()); - return new RunIdentifier("#StackOverflowError#", build.getNumber()); + LOGGER.log(Level.WARNING, "Issue #87: StackOverflowError getting job name for unknown job #" + number); + return new RunIdentifier("#StackOverflowError#", number); } } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/context/ScmContextKey.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/context/ScmContextKey.java new file mode 100644 index 000000000..72b96d206 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/context/ScmContextKey.java @@ -0,0 +1,22 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job.opentelemetry.context; + +import hudson.scm.SCM; +import hudson.tasks.BuildStep; +import io.opentelemetry.context.ContextKey; + +import javax.annotation.concurrent.Immutable; + +/** + * See {@code io.opentelemetry.api.trace.SpanContextKey} + */ +@Immutable +public final class ScmContextKey { + public static final ContextKey KEY = ContextKey.named(ScmContextKey.class.getName()); + + private ScmContextKey(){} +} diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java index 394e9494b..50cc28262 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java @@ -11,6 +11,7 @@ import hudson.ExtensionList; import hudson.model.AbstractBuild; import hudson.model.Run; +import hudson.plugins.git.AbstractGitRepository; import hudson.util.LogTaskListener; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; @@ -56,7 +57,7 @@ import static com.google.common.base.Verify.verify; import static org.junit.Assert.fail; -public class BaseIntegrationTest { +public class BaseIntegrationTest extends AbstractGitRepository { private static final Logger LOGGER = Logger.getLogger(Run.class.getName()); static { diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginFreestyleIntegrationTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginFreestyleIntegrationTest.java index 6a0e7f60e..a5e1fc1f0 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginFreestyleIntegrationTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginFreestyleIntegrationTest.java @@ -11,6 +11,10 @@ import hudson.model.FreeStyleProject; import hudson.model.Node; import hudson.model.Result; +import hudson.plugins.git.BranchSpec; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.DisableRemotePoll; import hudson.tasks.Ant; import hudson.tasks.ArtifactArchiver; import hudson.tasks.Shell; @@ -24,6 +28,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; +import org.jvnet.hudson.test.CaptureEnvironmentBuilder; import org.jvnet.hudson.test.FailureBuilder; import org.jvnet.hudson.test.FakeChangeLogSCM; import org.jvnet.hudson.test.FlagRule; @@ -32,6 +37,7 @@ import org.jvnet.hudson.test.recipes.WithPlugin; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static io.jenkins.plugins.opentelemetry.OtelUtils.JENKINS_CORE; @@ -239,4 +245,28 @@ public void testFreestyleJob_with_culprits() throws Exception { assertFreestyleJobMetadata(build, spans); } + + @Test + public void testFreestyleJob_with_git() throws Exception { + assumeFalse(SystemUtils.IS_OS_WINDOWS); + + final String jobName = "test-freestyle-git-" + jobNameSuffix.incrementAndGet(); + FreeStyleProject project = jenkinsRule.createFreeStyleProject(jobName); + GitSCM scm = new GitSCM(remoteConfigs(), Collections.singletonList(new BranchSpec("main")), + null, null, + Collections.singletonList(new DisableRemotePoll())); + project.setScm(scm); + project.getBuildersList().add(new CaptureEnvironmentBuilder()); + + jenkinsRule.buildAndAssertSuccess(project); + + Tree spans = getGeneratedSpans(); + checkChainOfSpans(spans, "Phase: Start"); + checkChainOfSpans(spans, "Phase: Run"); + checkChainOfSpans(spans, "GitSCM"); + MatcherAssert.assertThat(spans.cardinality(), CoreMatchers.is(6L)); + + // TODO: verify if the git scm span exists + // TODO: verify span attributes for the scm span once it's supported + } }