[PATCH 16/26] lucid: honor show_task_path_on_failure option

Daniel Gomez <[email protected]> Tue, 19 May 2026 15:28:13 +0200
Newsgroups dev.linux.lists.kdevops
Message-ID <[email protected]>
From: Daniel Gomez <[email protected]>

When a task fails, knowing the source file and line of the failing task
makes diagnosis dramatically faster, which is why the default Ansible
stdout callback exposes show_task_path_on_failure. Lucid silently
ignored the option, forcing users to re-run with higher verbosity or to
grep through roles by hand when something broke. This change caches the
option in set_options and, for failed or unreachable results, appends a
task path line to the displayed output when the option is set or when
verbosity is at least two. The path is included with the existing
stdout, stderr, msg, and exception blocks so it routes with the rest of
the failure output, including to stderr when display_failed_stderr is
on. When there is no body to display the path is still surfaced on its
own so the user is not left guessing.

The log file also gains the task path for every failure regardless of
the option, because the log is intentionally maximum fidelity and
should remain self-sufficient for post-hoc debugging.

Generated-by: Claude AI
Signed-off-by: Daniel Gomez <[email protected]>
---
 callback_plugins/lucid.py | 40 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 38 insertions(+), 2 deletions(-)

diff --git a/callback_plugins/lucid.py b/callback_plugins/lucid.py
index 443f314a..68663f13 100644
--- a/callback_plugins/lucid.py
+++ b/callback_plugins/lucid.py
@@ -142,6 +142,7 @@ class CallbackModule(CallbackBase):
         self.display_failed_stderr = self.get_option("display_failed_stderr")
         self.check_mode_markers = self.get_option("check_mode_markers")
         self.show_custom_stats = self.get_option("show_custom_stats")
+        self.show_task_path_on_failure = self.get_option("show_task_path_on_failure")
 
         # Determine display mode based on configuration
         is_interactive = self._detect_interactive()
@@ -764,11 +765,30 @@ class CallbackModule(CallbackBase):
                         stderr=use_stderr,
                     )
 
+        # Resolve the failing task's source path when we are meant to show
+        # it: either the user opted in via show_task_path_on_failure, or they
+        # asked for -vv+ verbosity which always exposes the path in the
+        # reference default callback.
+        task_path = ""
+        if status in ("failed", "unreachable") and (
+            self.show_task_path_on_failure or self._display.verbosity >= 2
+        ):
+            try:
+                task_path = result._task.get_path() or ""
+            except AttributeError:
+                task_path = ""
+
         # Show output if conditions met
         if show_output:
-            self._display_output(result, stderr=use_stderr)
+            self._display_output(result, stderr=use_stderr, task_path=task_path)
+        elif task_path:
+            # Still surface the failing task's source location even when the
+            # body is suppressed (e.g. no stdout/stderr/msg to display).
+            self._display_message(
+                f"task path: {task_path}", color=C.COLOR_VERBOSE, stderr=use_stderr
+            )
 
-    def _display_output(self, result, stderr: bool = False):
+    def _display_output(self, result, stderr: bool = False, task_path: str = ""):
         """Display stdout/stderr/msg from task result"""
         output = []
         res = result._result
@@ -793,6 +813,11 @@ class CallbackModule(CallbackBase):
         if "exception" in res and res["exception"]:
             output.append(f"\nEXCEPTION:\n{res['exception']}")
 
+        # Task source path for failures, mirroring the default callback's
+        # show_task_path_on_failure behavior.
+        if task_path:
+            output.append(f"\ntask path: {task_path}")
+
         if output:
             with self.output_lock:
                 self._display.display("".join(output), stderr=stderr)
@@ -838,6 +863,17 @@ class CallbackModule(CallbackBase):
         if status == "failed" and "exception" in res:
             self._write_to_log(f"\nEXCEPTION:\n{res['exception']}\n")
 
+        # Always record the failing task's source path in the log, regardless
+        # of the show_task_path_on_failure option or verbosity, so the log
+        # stays self-sufficient for post-hoc debugging of failures.
+        if status in ("failed", "unreachable"):
+            try:
+                task_path = result._task.get_path()
+            except AttributeError:
+                task_path = None
+            if task_path:
+                self._write_to_log(f"task path: {task_path}")
+
     def _display_recap(self, stats):
         """Display final statistics"""
         self._display_message("\nPLAY RECAP", C.COLOR_HIGHLIGHT)

-- 
2.53.0