From 04f126c0d36470f1d75e24f41c4a73f631084a5b Mon Sep 17 00:00:00 2001 From: Michael Trimarchi Date: Tue, 16 Dec 2025 09:49:43 +0100 Subject: [PATCH] Add log extraction from FlowNode instead the entire log In complex pipeline we can find the node that generate the error and just get that one back to the ErrorExplainer. Signed-off-by: Michael Trimarchi --- pom.xml | 16 ++-- .../ConsoleExplainErrorAction.java | 5 +- .../plugins/explain_error/ErrorExplainer.java | 3 +- .../explain_error/PipelineLogExtractor.java | 88 +++++++++++++++++++ 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/explain_error/PipelineLogExtractor.java 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); + } +}