diff --git a/pom.xml b/pom.xml index 9ce46c5..f39b2ff 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,16 @@ workflow-step-api + + org.jenkins-ci.plugins.workflow + workflow-api + + + + org.jenkins-ci.plugins.workflow + workflow-job + + io.jenkins.plugins @@ -99,12 +109,6 @@ test - - org.jenkins-ci.plugins.workflow - workflow-job - test - - org.jenkins-ci.plugins.workflow workflow-durable-task-step diff --git a/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java b/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java index 8c53156..f4b2249 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java @@ -6,9 +6,12 @@ import jenkins.model.RunAction2; import java.io.IOException; import java.io.PrintWriter; +import java.util.List; import java.util.logging.Logger; import javax.servlet.ServletException; import net.sf.json.JSONObject; + +import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.kohsuke.stapler.StaplerRequest2; import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -86,7 +89,7 @@ public void doExplainConsoleError(StaplerRequest2 req, StaplerResponse2 rsp) thr } // Fetch the last N lines of the log - java.util.List logLines = run.getLog(maxLines); + List logLines = PipelineLogExtractor.getFailedStepLog(run, maxLines); String errorText = String.join("\n", logLines); ErrorExplainer explainer = new ErrorExplainer(); diff --git a/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java b/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java index 476fe11..370614f 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java @@ -11,6 +11,7 @@ import java.util.logging.Logger; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; /** * Service class responsible for explaining errors using AI. @@ -60,7 +61,7 @@ public void explainError(Run run, TaskListener listener, String logPattern } private String extractErrorLogs(Run run, String logPattern, int maxLines) throws IOException { - List logLines = run.getLog(maxLines); + List logLines = PipelineLogExtractor.getFailedStepLog(run, maxLines); if (StringUtils.isBlank(logPattern)) { // Return last few lines if no pattern specified diff --git a/src/main/java/io/jenkins/plugins/explain_error/PipelineLogExtractor.java b/src/main/java/io/jenkins/plugins/explain_error/PipelineLogExtractor.java new file mode 100644 index 0000000..ad1c76b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/explain_error/PipelineLogExtractor.java @@ -0,0 +1,88 @@ +package io.jenkins.plugins.explain_error; + +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + +import edu.umd.cs.findbugs.annotations.NonNull; + +import org.jenkinsci.plugins.workflow.flow.FlowExecution; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.graph.FlowGraphWalker; +import org.jenkinsci.plugins.workflow.actions.ErrorAction; +import org.jenkinsci.plugins.workflow.actions.LogAction; +import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleNote; +import hudson.model.Run; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class PipelineLogExtractor { + + private static List readLimitedLog(AnnotatedLargeText logText, + int maxLines) { + StringWriter writer = new StringWriter(); + try { + long offset = logText.writeLogTo(0, writer); + if (offset <= 0) { + return Collections.emptyList(); + } + String cleanLog = ConsoleNote.removeNotes(writer.toString()); + BufferedReader reader = new BufferedReader(new StringReader(cleanLog)); + LinkedList queue = new LinkedList<>(); + String line; + while ((line = reader.readLine()) != null) { + if (queue.size() >= maxLines) { + queue.removeFirst(); + } + line = line.replace("\n", "").replace("\r", ""); + queue.add(line); + } + return new ArrayList<>(queue); + } catch (Exception e) { + e.printStackTrace(); + } + return Collections.emptyList(); + } + + /** + * Extracts the log output of the specific step that caused the pipeline failure. + * @param run The pipeline build + * @return The log text of the failed step, or null if no failed step with a log is found. + * @throws IOException + */ + public static List getFailedStepLog(@NonNull Run run, int maxLines) throws IOException { + + if (run instanceof WorkflowRun) { + FlowExecution execution = ((WorkflowRun) run).getExecution(); + + FlowGraphWalker walker = new FlowGraphWalker(execution); + for (FlowNode node : walker) { + ErrorAction errorAction = node.getAction(ErrorAction.class); + if (errorAction != null) { + FlowNode nodeThatThrewException = ErrorAction.findOrigin(errorAction.getError(), execution); + if (nodeThatThrewException == null) { + continue; + } + LogAction logAction = nodeThatThrewException.getAction(LogAction.class); + if (logAction != null) { + AnnotatedLargeText logText = logAction.getLogText(); + List result = readLimitedLog(logText, maxLines); + if (result == null || result.isEmpty()) + { + continue; + } + return result; + } + } + } + } + + return run.getLog(maxLines); + } +}