Skip to content
Open
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ post {
| **logPattern** | Regex pattern to filter relevant log lines | `''` (no filtering) |
| **language** | Language for the explanation | `'English'` |
| **customContext** | Additional instructions or context for the AI. Overrides global custom context if specified. | Uses global configuration |
| **collectDownstreamLogs** | Whether to include logs from failed downstream jobs discovered via the `build` step or `Cause.UpstreamCause` | `false` |
| **downstreamJobPattern** | Regular expression matched against downstream job full names. Used only when downstream collection is enabled. | `''` (collect none) |

```groovy
explainError(
Expand All @@ -255,6 +257,18 @@ explainError(
)
```

To include downstream failures, opt in explicitly and limit collection with a regex:

```groovy
explainError(
collectDownstreamLogs: true,
downstreamJobPattern: 'team-folder/.*/deploy-.*'
)
```

This keeps the default behavior fast and predictable on large controllers. Only downstream jobs
whose full name matches `downstreamJobPattern` are scanned and included in the AI analysis.

Output appears in the sidebar of the failed job.

![Side Panel - AI Error Explanation](docs/images/side-panel.png)
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@
<optional>true</optional>
</dependency>

<!-- Pipeline build step for downstream build tracking (optional) -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>pipeline-build-step</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>ionicons-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import hudson.model.Result;
import hudson.model.Run;
import jenkins.model.RunAction2;
import jenkins.model.Jenkins;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
Expand Down Expand Up @@ -88,7 +89,8 @@ public void doExplainConsoleError(StaplerRequest2 req, StaplerResponse2 rsp) thr
}

// Fetch the last N lines of the log
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines);
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines, Jenkins.getAuthentication2(),
false, null);
List<String> logLines = logExtractor.getFailedStepLog();
this.urlString = logExtractor.getUrl();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
import hudson.util.LogTaskListener;
import io.jenkins.plugins.explain_error.provider.BaseAIProvider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;

/**
* Service class responsible for explaining errors using AI.
*/
public class ErrorExplainer {
static final String DOWNSTREAM_SECTION_START = "### Downstream Job: ";
static final String DOWNSTREAM_SECTION_END = "### END OF DOWNSTREAM JOB: ";

private String providerName;
private String urlString;
Expand All @@ -30,14 +34,26 @@
}

public String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int maxLines) {
return explainError(run, listener, logPattern, maxLines, null, null);
return explainError(run, listener, logPattern, maxLines, null, null, false, null, null);
}

public String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int maxLines, String language) {
return explainError(run, listener, logPattern, maxLines, language, null);
return explainError(run, listener, logPattern, maxLines, language, null, false, null, null);
}

public String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int maxLines, String language, String customContext) {
return explainError(run, listener, logPattern, maxLines, language, customContext, false, null, null);
}

public String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int maxLines, String language,
String customContext, boolean collectDownstreamLogs, String downstreamJobPattern) {
return explainError(run, listener, logPattern, maxLines, language, customContext,

Check warning on line 50 in src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 41-50 are not covered by tests
collectDownstreamLogs, downstreamJobPattern, null);
}

String explainError(Run<?, ?> run, TaskListener listener, String logPattern, int maxLines, String language,
String customContext, boolean collectDownstreamLogs, String downstreamJobPattern,
Authentication authentication) {
String jobInfo = run != null ? ("[" + run.getParent().getFullName() + " #" + run.getNumber() + "]") : "[unknown]";
try {
// Check if explanation is enabled (folder-level or global)
Expand All @@ -54,7 +70,8 @@
}

// Extract error logs
String errorLogs = extractErrorLogs(run, logPattern, maxLines);
String errorLogs = extractErrorLogs(run, logPattern, maxLines, collectDownstreamLogs,
downstreamJobPattern, authentication);

// Use step-level customContext if provided, otherwise fallback to global
String effectiveCustomContext = StringUtils.isNotBlank(customContext) ? customContext : GlobalConfigurationImpl.get().getCustomContext();
Expand Down Expand Up @@ -83,26 +100,49 @@
}
}

private String extractErrorLogs(Run<?, ?> run, String logPattern, int maxLines) throws IOException {
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines);
private String extractErrorLogs(Run<?, ?> run, String logPattern, int maxLines,
boolean collectDownstreamLogs, String downstreamJobPattern,
Authentication authentication) throws IOException {
PipelineLogExtractor logExtractor = new PipelineLogExtractor(run, maxLines, authentication,
collectDownstreamLogs, downstreamJobPattern);
List<String> logLines = logExtractor.getFailedStepLog();
this.urlString = logExtractor.getUrl();

return filterErrorLogs(logLines, logPattern);
}

String filterErrorLogs(List<String> logLines, String logPattern) {
if (StringUtils.isBlank(logPattern)) {
// Return last few lines if no pattern specified
return String.join("\n", logLines);
}

Pattern pattern = Pattern.compile(logPattern, Pattern.CASE_INSENSITIVE);
StringBuilder errorLogs = new StringBuilder();
List<String> filteredLines = new ArrayList<>();
boolean inDownstreamSection = false;

for (String line : logLines) {
if (pattern.matcher(line).find()) {
errorLogs.append(line).append("\n");
if (isDownstreamSectionStart(line)) {
inDownstreamSection = true;
}

if (inDownstreamSection || pattern.matcher(line).find()) {
filteredLines.add(line);
}

if (inDownstreamSection && isDownstreamSectionEnd(line)) {
inDownstreamSection = false;
}
}

return errorLogs.toString();
return String.join("\n", filteredLines);
}

private boolean isDownstreamSectionStart(String line) {
return line != null && line.startsWith(DOWNSTREAM_SECTION_START);

Check warning on line 141 in src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 141 is only partially covered, one branch is missing
}

private boolean isDownstreamSectionEnd(String line) {
return line != null && line.startsWith(DOWNSTREAM_SECTION_END);

Check warning on line 145 in src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 145 is only partially covered, one branch is missing
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import java.util.Set;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
Expand All @@ -21,12 +22,17 @@
private int maxLines;
private String language;
private String customContext;
private boolean collectDownstreamLogs;
private String downstreamJobPattern;

@DataBoundConstructor
public ExplainErrorStep() {
this.logPattern = "";
this.maxLines = 100;
this.language = "";
this.customContext = "";
this.collectDownstreamLogs = false;
this.downstreamJobPattern = "";
}

public String getLogPattern() {
Expand Down Expand Up @@ -65,6 +71,24 @@
this.customContext = customContext != null ? customContext : "";
}

public boolean isCollectDownstreamLogs() {
return collectDownstreamLogs;
}

@DataBoundSetter
public void setCollectDownstreamLogs(boolean collectDownstreamLogs) {
this.collectDownstreamLogs = collectDownstreamLogs;
}

public String getDownstreamJobPattern() {
return downstreamJobPattern;
}

@DataBoundSetter
public void setDownstreamJobPattern(String downstreamJobPattern) {
this.downstreamJobPattern = downstreamJobPattern != null ? downstreamJobPattern : "";

Check warning on line 89 in src/main/java/io/jenkins/plugins/explain_error/ExplainErrorStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 89 is only partially covered, one branch is missing
}

@Override
public StepExecution start(StepContext context) throws Exception {
return new ExplainErrorStepExecution(context, this);
Expand Down Expand Up @@ -105,7 +129,9 @@
TaskListener listener = getContext().get(TaskListener.class);

ErrorExplainer explainer = new ErrorExplainer();
String explanation = explainer.explainError(run, listener, step.getLogPattern(), step.getMaxLines(), step.getLanguage(), step.getCustomContext());
String explanation = explainer.explainError(run, listener, step.getLogPattern(), step.getMaxLines(),
step.getLanguage(), step.getCustomContext(), step.isCollectDownstreamLogs(),
step.getDownstreamJobPattern(), Jenkins.getAuthentication2());

return explanation;
}
Expand Down
Loading
Loading