Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enrich SCM in freestyle jobs #292

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -338,11 +339,21 @@ private Descriptor<? extends Describable> getStepDescriptor(@Nonnull FlowNode no
return descriptor;
}

@Nullable
private Descriptor<? extends Describable> getScmDescriptor(@Nonnull SCM scm) {
return Jenkins.get().getDescriptor((Class<? extends Describable>) scm.getClass());
}

@Nullable
private Descriptor<? extends Describable> getBuildStepDescriptor(@Nonnull BuildStep buildStep) {
return Jenkins.get().getDescriptor((Class<? extends Describable>) 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));
Expand Down Expand Up @@ -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<? extends Describable> descriptor) {
String value = buildStepName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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{}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -456,33 +474,29 @@ public static class RunIdentifier implements Comparable<RunIdentifier> {
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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SCM> KEY = ContextKey.named(ScmContextKey.class.getName());

private ScmContextKey(){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.<GitSCMExtension>singletonList(new DisableRemotePoll()));
project.setScm(scm);
project.getBuildersList().add(new CaptureEnvironmentBuilder());

jenkinsRule.buildAndAssertSuccess(project);

Tree<SpanDataWrapper> 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
}
}