diff --git a/src/main/java/io/jenkins/plugins/explain_error/AIService.java b/src/main/java/io/jenkins/plugins/explain_error/AIService.java index 16c4f89..3d50c45 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/AIService.java +++ b/src/main/java/io/jenkins/plugins/explain_error/AIService.java @@ -1,5 +1,6 @@ package io.jenkins.plugins.explain_error; +import hudson.model.Run; import java.io.IOException; import java.util.logging.Logger; @@ -43,4 +44,15 @@ private BaseAIService createServiceForProvider(GlobalConfigurationImpl config) { public String explainError(String errorLogs) throws IOException { return delegate.explainError(errorLogs); } + + /** + * Explain error logs using the configured AI provider with job context for logging. + * @param errorLogs the error logs to explain + * @param run the run context for logging purposes + * @return the AI explanation + * @throws IOException if there's a communication error + */ + public String explainError(String errorLogs, Run run) throws IOException { + return delegate.explainError(errorLogs, run); + } } \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/explain_error/BaseAIService.java b/src/main/java/io/jenkins/plugins/explain_error/BaseAIService.java index 92ae037..2643686 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/BaseAIService.java +++ b/src/main/java/io/jenkins/plugins/explain_error/BaseAIService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import hudson.ProxyConfiguration; +import hudson.model.Run; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -33,10 +34,22 @@ public BaseAIService(GlobalConfigurationImpl config) { * @throws IOException if there's a communication error */ public String explainError(String errorLogs) throws IOException { + return explainError(errorLogs, null); + } + + /** + * Explain error logs using the configured AI provider with job context for logging. + * @param errorLogs the error logs to explain + * @param run the run context for logging purposes + * @return the AI explanation + * @throws IOException if there's a communication error + */ + public String explainError(String errorLogs, Run run) throws IOException { if (StringUtils.isBlank(errorLogs)) { return "No error logs provided for explanation."; } + String jobContext = run != null ? JobContextUtil.createJobContext(run) + " " : ""; String prompt = buildPrompt(errorLogs); String requestBody = buildRequestBody(prompt); @@ -59,11 +72,11 @@ public String explainError(String errorLogs) throws IOException { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); String responseBody = response.body(); - LOGGER.fine("Response body length: " + responseBody.length()); - LOGGER.fine("Response body preview: " + responseBody.substring(0, Math.min(500, responseBody.length()))); + LOGGER.fine(jobContext + "Response body length: " + responseBody.length()); + LOGGER.fine(jobContext + "Response body preview: " + responseBody.substring(0, Math.min(500, responseBody.length()))); if (response.statusCode() != 200) { - LOGGER.severe("AI API request failed with status " + response.statusCode() + ": " + responseBody); + LOGGER.severe(jobContext + "AI API request failed with status " + response.statusCode() + ": " + responseBody); return "Failed to get explanation from AI service. Status: " + response.statusCode() + ". Please check your API configuration and key."; } @@ -72,10 +85,10 @@ public String explainError(String errorLogs) throws IOException { } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.severe("AI API request was interrupted: " + e.getMessage()); + LOGGER.severe(jobContext + "AI API request was interrupted: " + e.getMessage()); return "Request was interrupted: " + e.getMessage(); } catch (Exception e) { - LOGGER.severe("AI API request failed: " + e.getMessage()); + LOGGER.severe(jobContext + "AI API request failed: " + e.getMessage()); return "Failed to communicate with AI service: " + e.getMessage(); } } 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 f780960..1fabe39 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorAction.java @@ -84,8 +84,9 @@ public void doExplainConsoleError(StaplerRequest2 req, StaplerResponse2 rsp) thr writeJsonResponse(rsp, "Error: Could not generate explanation. Please check your AI API configuration."); } } catch (Exception e) { - LOGGER.severe("=== EXPLAIN ERROR REQUEST FAILED ==="); - LOGGER.severe("Error explaining console error: " + e.getMessage()); + String jobContext = JobContextUtil.createJobContext(run); + LOGGER.severe(jobContext + " === EXPLAIN ERROR REQUEST FAILED ==="); + LOGGER.severe(jobContext + " Error explaining console error: " + e.getMessage()); writeJsonResponse(rsp, "Error: " + e.getMessage()); } } @@ -118,7 +119,8 @@ public void doCheckExistingExplanation(StaplerRequest2 req, StaplerResponse2 rsp writer.flush(); } catch (Exception e) { - LOGGER.severe("Error checking existing explanation: " + e.getMessage()); + String jobContext = JobContextUtil.createJobContext(run); + LOGGER.severe(jobContext + " Error checking existing explanation: " + e.getMessage()); rsp.setStatus(500); } } diff --git a/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorActionFactory.java b/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorActionFactory.java index 47e76db..a773392 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorActionFactory.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ConsoleExplainErrorActionFactory.java @@ -33,7 +33,8 @@ public Collection createFor(@Nonnull Run run) { ConsoleExplainErrorAction action = new ConsoleExplainErrorAction(run); return Collections.singletonList(action); } catch (Exception e) { - LOGGER.severe("Failed to create ConsoleExplainErrorAction for run: " + run.getFullDisplayName() + ". Error: " + e.getMessage()); + String jobContext = JobContextUtil.createJobContext(run); + LOGGER.severe(jobContext + " Failed to create ConsoleExplainErrorAction. Error: " + e.getMessage()); return Collections.emptyList(); } } diff --git a/src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java b/src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java index e40f892..8540aef 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ConsolePageDecorator.java @@ -2,7 +2,10 @@ import hudson.Extension; import hudson.model.PageDecorator; +import hudson.model.Run; import hudson.util.Secret; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest2; /** * Page decorator to add "Explain Error" functionality to console output pages. @@ -33,4 +36,24 @@ public boolean isExplainErrorEnabled() { // If API URL is set to a non-empty value, that's also valid return true; } + + /** + * Get job context for the current request if it's a console page. + * @return job context string like "[JobName #BuildNumber]" or empty string if not applicable + */ + public String getJobContext() { + StaplerRequest2 request = Stapler.getCurrentRequest2(); + if (request == null) { + return ""; + } + + // Get the current object from the request + Object ancestor = request.findAncestorObject(Run.class); + if (ancestor instanceof Run) { + Run run = (Run) ancestor; + return JobContextUtil.createJobContext(run); + } + + return ""; + } } 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 9ed956c..fb5f2e3 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ErrorExplainer.java @@ -16,6 +16,8 @@ public class ErrorExplainer { private static final Logger LOGGER = Logger.getLogger(ErrorExplainer.class.getName()); public void explainError(Run run, TaskListener listener, String logPattern, int maxLines) { + String jobContext = JobContextUtil.createJobContext(run); + try { GlobalConfigurationImpl config = GlobalConfigurationImpl.get(); @@ -38,9 +40,9 @@ public void explainError(Run run, TaskListener listener, String logPattern return; } - // Get AI explanation + // Get AI explanation with job context AIService aiService = new AIService(config); - String explanation = aiService.explainError(errorLogs); + String explanation = aiService.explainError(errorLogs, run); // Store explanation in build action ErrorExplanationAction action = new ErrorExplanationAction(explanation, errorLogs); @@ -49,7 +51,7 @@ public void explainError(Run run, TaskListener listener, String logPattern // Explanation is now available on the job page, no need to clutter console output } catch (Exception e) { - LOGGER.severe("Failed to explain error: " + e.getMessage()); + LOGGER.severe(jobContext + " Failed to explain error: " + e.getMessage()); listener.getLogger().println("Failed to explain error: " + e.getMessage()); } } @@ -79,37 +81,38 @@ private String extractErrorLogs(Run run, String logPattern, int maxLines) * Used for console output error explanation. */ public String explainErrorText(String errorText, Run run) { + String jobContext = JobContextUtil.createJobContext(run); try { GlobalConfigurationImpl config = GlobalConfigurationImpl.get(); if (!config.isEnableExplanation()) { - LOGGER.warning("AI error explanation is disabled in global configuration"); - return "AI error explanation is disabled in global configuration."; + LOGGER.warning(jobContext + " AI error explanation is disabled in global configuration"); + return jobContext + " AI error explanation is disabled in global configuration."; } if (config.getApiKey() == null || StringUtils.isBlank(config.getApiKey().getPlainText())) { - LOGGER.warning("API key is not configured"); + LOGGER.warning(jobContext + " API key is not configured"); return "ERROR: API key is not configured. Please configure it in Jenkins global settings."; } if (StringUtils.isBlank(errorText)) { - LOGGER.warning("No error text provided"); + LOGGER.warning(jobContext + " No error text provided"); return "No error text provided to explain."; } - // Get AI explanation + // Get AI explanation with job context AIService aiService = new AIService(config); - String explanation = aiService.explainError(errorText); + String explanation = aiService.explainError(errorText, run); - LOGGER.fine("Explanation length: " + (explanation != null ? explanation.length() : 0)); + LOGGER.fine(jobContext + " Explanation length: " + (explanation != null ? explanation.length() : 0)); return explanation; } catch (Exception e) { - LOGGER.severe("Failed to explain error text: " + e.getMessage()); + LOGGER.severe(jobContext + " Failed to explain error text: " + e.getMessage()); e.printStackTrace(); - return "Failed to explain error: " + e.getMessage(); + return jobContext + " Failed to explain error: " + e.getMessage(); } } } diff --git a/src/main/java/io/jenkins/plugins/explain_error/ErrorExplanationAction.java b/src/main/java/io/jenkins/plugins/explain_error/ErrorExplanationAction.java index b1e790a..927b4b6 100644 --- a/src/main/java/io/jenkins/plugins/explain_error/ErrorExplanationAction.java +++ b/src/main/java/io/jenkins/plugins/explain_error/ErrorExplanationAction.java @@ -29,6 +29,17 @@ public String getDisplayName() { return "AI Error Explanation"; } + /** + * Get job context for display in card titles. + * @return job context string like "[JobName #BuildNumber]" or empty string if no run + */ + public String getJobContext() { + if (run != null) { + return JobContextUtil.createJobContext(run); + } + return ""; + } + @Override public String getUrlName() { return "error-explanation"; diff --git a/src/main/java/io/jenkins/plugins/explain_error/JobContextUtil.java b/src/main/java/io/jenkins/plugins/explain_error/JobContextUtil.java new file mode 100644 index 0000000..7c2b8d2 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/explain_error/JobContextUtil.java @@ -0,0 +1,48 @@ +package io.jenkins.plugins.explain_error; + +import hudson.model.Run; + +/** + * Utility class for creating job context information for logging. + * This helps identify which specific job encountered an error when multiple jobs + * are running concurrently and using the AI service. + */ +public class JobContextUtil { + + /** + * Create a standardized job context string for logging. + * Format: [JobName #BuildNumber] + * + * @param run the build run + * @return formatted job context string + */ + public static String createJobContext(Run run) { + if (run == null) { + return "[Unknown Job]"; + } + + String jobName = run.getParent().getFullName(); + int buildNumber = run.getNumber(); + + return String.format("[%s #%d]", jobName, buildNumber); + } + + /** + * Create a job context string with additional details for logging. + * Format: [JobName #BuildNumber - DisplayName] + * + * @param run the build run + * @return formatted detailed job context string + */ + public static String createDetailedJobContext(Run run) { + if (run == null) { + return "[Unknown Job]"; + } + + String jobName = run.getParent().getFullName(); + int buildNumber = run.getNumber(); + String displayName = run.getDisplayName(); + + return String.format("[%s #%d - %s]", jobName, buildNumber, displayName); + } +} diff --git a/src/main/resources/io/jenkins/plugins/explain_error/ConsolePageDecorator/footer.jelly b/src/main/resources/io/jenkins/plugins/explain_error/ConsolePageDecorator/footer.jelly index 11be277..10968b1 100644 --- a/src/main/resources/io/jenkins/plugins/explain_error/ConsolePageDecorator/footer.jelly +++ b/src/main/resources/io/jenkins/plugins/explain_error/ConsolePageDecorator/footer.jelly @@ -3,7 +3,7 @@