[PATCH 08/26] lucid: log per-item command and output for loops
Daniel Gomez <[email protected]> Tue, 19 May 2026 15:28:05 +0200
| Newsgroups | dev.linux.lists.kdevops |
|---|---|
| Message-ID | <[email protected]> |
From: Daniel Gomez <[email protected]> Ansible reports per-iteration results for with_items and loop tasks through the v2_runner_item_on_ok, v2_runner_item_on_failed, and v2_runner_item_on_skipped callbacks, but lucid implemented none of them. A 200-iteration loop therefore left only a single aggregated entry in the log file, making it impossible to tell which iteration ran which command or produced which output when something went wrong. Implement the three item callbacks and a shared _log_item_result helper that writes the command, stdout, stderr, and msg for each item to the log file with a label derived from the iteration variable. Screen output at the default verbosity is unchanged so large loops do not flood the console. The failed callback additionally stashes a structured record of each failing item on self.failed_items, which is reset at the start of every task; a later change promotes that buffer to the screen so users see the failing iteration inline. Generated-by: Claude AI Signed-off-by: Daniel Gomez <[email protected]> --- callback_plugins/lucid.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/callback_plugins/lucid.py b/callback_plugins/lucid.py index 856a918b..e1d14e62 100644 --- a/callback_plugins/lucid.py +++ b/callback_plugins/lucid.py @@ -110,6 +110,9 @@ class CallbackModule(CallbackBase): self.update_thread_stop: Optional[threading.Event] = None self.task_lock = threading.Lock() + # Failed loop items for display on task failure + self.failed_items: list[dict] = [] + # Will be set in set_options() self.output_mode = "auto" self.log_file_path: Optional[str] = None @@ -446,6 +449,7 @@ class CallbackModule(CallbackBase): self._flush_play_header() self.current_task_name = task_name + self.failed_items = [] # Initialize with play hosts so display is stable from the start self.current_task_hosts = list(self.play_hosts) if self.play_hosts else [] @@ -495,6 +499,56 @@ class CallbackModule(CallbackBase): """Host unreachable""" self._handle_result(result, "unreachable") + def v2_runner_item_on_ok(self, result): + """Loop item succeeded — log per-item command and output""" + self._log_item_result(result) + + def v2_runner_item_on_failed(self, result): + """Loop item failed — log per-item command and output""" + self._log_item_result(result) + res = result._result + item = res.get("_ansible_item_label") + if item is None: + loop_var = res.get("ansible_loop_var", "item") + item = res.get(loop_var, "") + if isinstance(item, dict): + item = item.get("name", item.get("group", str(item))) + self.failed_items.append({ + "item": item, + "stderr": res.get("stderr", ""), + "stdout": res.get("stdout", ""), + "msg": res.get("msg", ""), + "cmd": self._get_task_command(result), + }) + + def v2_runner_item_on_skipped(self, result): + """Loop item skipped""" + pass + + def _log_item_result(self, result): + """Log per-item command and output for loop tasks""" + res = result._result + item = res.get("_ansible_item_label") + if item is None: + loop_var = res.get("ansible_loop_var", "item") + item = res.get(loop_var, "") + if isinstance(item, dict): + item = item.get("name", item.get("group", str(item))) + + command = self._get_task_command(result) + if command: + self._write_to_log(f" [{item}] $ {command}") + + if res.get("stdout"): + self._write_to_log(f" [{item}] stdout: {res['stdout']}") + if res.get("stderr"): + self._write_to_log(f" [{item}] stderr: {res['stderr']}") + if res.get("msg") and not res.get("stdout"): + msg = res["msg"] + if isinstance(msg, (list, dict)): + msg = json.dumps(msg, indent=2) + self._write_to_log(f" [{item}] msg: {msg}") + def v2_runner_retry(self, result): """Task is being retried after failure""" host = result._host.name -- 2.53.0