Repository: aristanetworks/avd
Pull request #5341: Fix(anta_runner): Update Ansible task failed criteria and various logging issues
Author: login: carl-baillargeon
Labels: name: role: build_output_folders
description: issue related to build_output_folders role
name: state: CI Updated
description: CI scenario have been updated in the PR
name: state: Documentation role Updated
name: one approval
description: This PR has one approval and is only missing one more.
name: rn: Fix(anta_runner)
Created: 2025-04-29T12:59:41Z | Merged: 2025-05-21T13:29:31Z

## Change Summary

Fixes: #5346 #5345

1- Update Ansible task failed/changed criteria:

a) No tests ran (outside of a dry run)
```bash
fatal: [DC1-SPINE1 -> localhost]: FAILED! => {
    "anta_test_summary": {
        "devices_with_test_errors": [],
        "devices_with_test_failures": [],
        "tests_error": 0,
        "tests_failed": 0,
        "tests_passed": 0,
        "tests_skipped": 0,
        "tests_unset": 0,
        "total_tests": 0
    },
    "changed": false,
    "msg": "Errors detected during ANTA workflow execution. No ANTA tests were run."
}
```
b) Errors were logged by the plugin, PyAVD or ANTA
```bash
ERROR! [anta-run-647f090e] Command 'ping vrf default 10.3.255.3 source 10.3.255.2 size 100 repeat 1' failed on DC1-BL1: Invalid input (privileged mode required)
fatal: [DC1-SPINE1 -> localhost]: FAILED! => {
    "anta_test_summary": {
        "devices_with_test_errors": [
            "DC1-BL1"
        ],
        "devices_with_test_failures": [],
        "tests_error": 1,
        "tests_failed": 0,
        "tests_passed": 0,
        "tests_skipped": 0,
        "tests_unset": 0,
        "total_tests": 1
    },
    "changed": false,
    "msg": "Errors detected during ANTA workflow execution. ANTA tests reported failures/errors."
}
```

c) Any test failed or errored
```bash
fatal: [DC1-SPINE1 -> localhost]: FAILED! => {
    "anta_test_summary": {
        "devices_with_test_errors": [],
        "devices_with_test_failures": [
            "DC1-BL1",
            "DC1-BL2",
            "DC1-LEAF1A",
            "DC1-LEAF1B",
            "DC1-LEAF1C",
            "DC1-LEAF2A",
            "DC1-LEAF2B",
            "DC1-SPINE1",
            "DC1-SPINE2",
            "DC2-BL1",
            "DC2-BL2",
            "DC2-LEAF1A",
            "DC2-LEAF1B",
            "DC2-SPINE1",
            "DC2-SPINE2"
        ],
        "tests_error": 0,
        "tests_failed": 58,
        "tests_passed": 307,
        "tests_skipped": 60,
        "tests_unset": 0,
        "total_tests": 425
    },
    "changed": false,
    "msg": "Task failed due to ANTA test failures/errors."
}
```

d) All good
```bash
ok: [DC1-SPINE1 -> localhost] => {
    "anta_test_summary": {
        "devices_with_test_errors": [],
        "devices_with_test_failures": [],
        "tests_error": 0,
        "tests_failed": 0,
        "tests_passed": 4,
        "tests_skipped": 0,
        "tests_unset": 0,
        "total_tests": 4
    },
    "changed": false,
    "msg": "ANTA tests completed without reported failures/errors."
}
```

2- All logs coming from the plugin, PyAVD or ANTA are now prefixed with a unique ID per run for easy identification between processes.

```bash
[anta-workflow] No user-defined ANTA catalogs found in directory: /home/cbaillar/git_projects/ansible-avd-examples/dual-dc/anta/user_catalogs
[anta-run-4126dcd6] Starting ANTA run for devices: DC2-SPINE1, DC2-SPINE2, DC2-LEAF1A, DC2-LEAF1B, DC2-BL1
[anta-run-e6cfd694] Starting ANTA run for devices: DC2-BL2, DC1-SPINE1, DC1-SPINE2, DC1-LEAF1A, DC1-LEAF1B
[anta-run-71598cbd] Starting ANTA run for devices: DC1-LEAF2A, DC1-LEAF2B, DC1-BL1, DC1-BL2, DC1-LEAF1C
[anta-run-4126dcd6] ANTA run completed for devices: DC2-SPINE1, DC2-SPINE2, DC2-LEAF1A, DC2-LEAF1B, DC2-BL1
[anta-run-e6cfd694] ANTA run completed for devices: DC2-BL2, DC1-SPINE1, DC1-SPINE2, DC1-LEAF1A, DC1-LEAF1B
[anta-run-71598cbd] ANTA run completed for devices: DC1-LEAF2A, DC1-LEAF2B, DC1-BL1, DC1-BL2, DC1-LEAF1C
[anta-workflow] Generating CSV report at /home/cbaillar/git_projects/ansible-avd-examples/dual-dc/anta/reports/anta_report.csv
[anta-workflow] Generating Markdown report at /home/cbaillar/git_projects/ansible-avd-examples/dual-dc/anta/reports/anta_report.md
[anta-workflow] Generating JSON report at /home/cbaillar/git_projects/ansible-avd-examples/dual-dc/anta/reports/anta_report.json
```

3- ANTA warning logs or above are now displayed properly in the Ansible console and will fail the task (see above) when errors are logged.

4- Update Ansible result with test summary information and proper message (see above)

## Component(s) name

`arista.avd.anta_runner`

## Proposed changes

See Change Summary.

## How to test
1- Create a playbook with the following vars:
```yaml
---
- name: Run ANTA
  hosts: GLOBAL
  connection: local
  gather_facts: false
  tasks:
    - name: Run ANTA on EOS devices
      import_role:
        name: arista.avd.anta_runner
      vars:
        # ansible_user: wrong_user
        # anta_runner_timeout: 0.1
```
2- Uncomment `ansible_user` with a wrong user. All devices should be unreachable. You should see logs in the console and the task must fail.

3- Uncomment `anta_runner_timeout`. You should receive timeout errors from ANTA and the task must fail.

4- Make sure the task fails if there is a failed test in your run.

5- Try to run the playbook with different verbosity level (-v and -vvv), make sure logs are displayed properly on the console

6- Configure `log_path=./ansible.log` in your `ansible.cfg` file. Run the playbook and make sure console logs are saved to the Ansible log file with NO duplicates.

## Checklist

### Testing Checklist  @joelbreton2 

- [x] Wrong ansible_user [Run#808](https://github.com/aristanetworks/avd-internal/actions/runs/14908775855)  - [Change](https://github.com/aristanetworks/avd-internal/commit/4d32050ce0adb773afbd4c444c46fa8ab14fea23)
```
ERROR! [anta-run-31c5acc3] An error occurred while issuing an eAPI request to campus-LEAF3D
HTTPStatusError: Client error '401 Unauthorized' for url 'https://10.90.231.185/command-api'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
[WARNING]: [anta-run-31c5acc3] Cannot get hardware information from device
campus-LEAF3D
```
  - [x] Low anta_runner timeout [Run#810](https://github.com/aristanetworks/avd-internal/actions/runs/14910576114) - [Change](https://github.com/aristanetworks/avd-internal/commit/da1fb276c919b10c976865992a3197976527fbff)
```
ERROR! [anta-run-e6239a5b] ConnectTimeout occurred while sending a command to campus-LEAF3A. Consider increasing the timeout.
Current timeouts: Connect: 0.1 | Read: 0.1 | Write: 0.1 | Pool: 0.1
```
  - [x] Verify failed task [Run#811](https://github.com/aristanetworks/avd-internal/actions/runs/14911741649/job/41889259255) - [Change](https://github.com/aristanetworks/avd-internal/commit/787cf0ad5a054c5ff516c00fe21a2a7849cd2d8b) 
```
fatal: [campus-SPINE1 -> localhost]: FAILED! => {"anta_test_summary": {"devices_with_test_errors": [], "devices_with_test_failures": ["campus-LEAF1A", "campus-LEAF1B", "campus-LEAF2A", "campus-LEAF3A", "campus-LEAF3B", "campus-LEAF3C", "campus-LEAF3D", "campus-LEAF3E", "campus-SPINE1", "campus-SPINE2"], "tests_error": 0, "tests_failed": 36, "tests_passed": 180, "tests_skipped": 0, "tests_unset": 0, "total_tests": 216}, "changed": false, "msg": "Task failed due to ANTA test failures/errors."}
```
  - [x] Verbosity -v [Run#812](https://github.com/aristanetworks/avd-internal/actions/runs/14913296094) - [Change](https://github.com/aristanetworks/avd-internal/commit/8cf913c7aec68e4f5d13d765b3d209483b769a44)
  - [x] Verbosity -vvv [Run#814](https://github.com/aristanetworks/avd-internal/actions/workflows/avd-internal-ci-root.yml) - [Change](https://github.com/aristanetworks/avd-internal/commit/83e27b657e2bb76f2263276f6a4510ed3ed9fa8e)
  - [ ] log_path=./ansible.log`

### User Checklist
<!-- Add your own checklist using MD syntax and by replacing N/A -->
- [x] Update README.
- [X] Update PR description with examples.

### Repository Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [X] My code has been rebased from devel before I start
- [X] I have read the [**CONTRIBUTING**](https://avd.arista.com/stable/docs/contribution/overview.html) document.
- [X] My change requires a change to the documentation and documentation have been updated accordingly.
- [X] I have updated [molecule CI](https://github.com/aristanetworks/avd/tree/devel/ansible_collections/arista/avd/molecule) testing accordingly. (check the box if not applicable)



Diff:
diff --git a/ansible_collections/arista/avd/molecule/anta_runner/destroy.yml b/ansible_collections/arista/avd/molecule/anta_runner/destroy.yml
index 0e66fb07bc0..90cf931be13 100644
--- a/ansible_collections/arista/avd/molecule/anta_runner/destroy.yml
+++ b/ansible_collections/arista/avd/molecule/anta_runner/destroy.yml
@@ -19,4 +19,3 @@
         - intended
         - anta/avd_catalogs
         - anta/reports
-        - anta/logs
diff --git a/ansible_collections/arista/avd/plugins/action/anta_workflow.py b/ansible_collections/arista/avd/plugins/action/anta_workflow.py
index f5dd9ac9975..cd4404c47f7 100644
--- a/ansible_collections/arista/avd/plugins/action/anta_workflow.py
+++ b/ansible_collections/arista/avd/plugins/action/anta_workflow.py
@@ -5,19 +5,21 @@
 
 import json
 import logging
+import os
+import sys
 from asyncio import run
 from concurrent.futures import ProcessPoolExecutor
-from datetime import datetime, timezone
 from logging.handlers import QueueHandler, QueueListener
-from multiprocessing import Queue, current_process, get_context
+from multiprocessing import Queue, get_context
 from pathlib import Path
 from typing import TYPE_CHECKING, Any
+from uuid import uuid4
 
 import yaml
 from ansible.errors import AnsibleActionFail
 from ansible.plugins.action import ActionBase, display
 
-from ansible_collections.arista.avd.plugins.plugin_utils.utils import ActionPluginVars, AntaLoggingFilter, PythonToAnsibleHandler
+from ansible_collections.arista.avd.plugins.plugin_utils.utils import ActionPluginVars, AntaWorkflowFilter, AntaWorkflowHandler
 
 if TYPE_CHECKING:
     from collections.abc import Iterator
@@ -88,7 +90,6 @@
             "batch_size": {"type": "int", "default": 5},
             "tags": {"type": "list", "elements": "str"},
             "dry_run": {"type": "bool", "default": False},
-            "logs_dir": {"type": "str"},
         },
     },
     "report": {
@@ -109,15 +110,16 @@
             },
         },
     },
-    "strict_mode": {"type": "bool", "default": False},
 }
 
 # Global variables to share data between processes. Since the plugin is forked, these variables are inherited by child processes.
+# TODO: Consider aggregating some of them into a SHARED_VARS dict or use multiprocessing.Manager()
 STRUCTURED_CONFIGS: dict[str, dict[str, Any]] | None = None
 MINIMAL_STRUCTURED_CONFIGS: dict[str, MinimalStructuredConfig] | None = None
 PLUGIN_ARGS: dict[str, Any] | None = None
 ANSIBLE_VARS: dict[str, dict[str, Any]] | None = None
 USER_CATALOG: AntaCatalog | None = None
+LOG_QUEUE: Queue = Queue()
 
 
 class ActionModule(ActionBase):
@@ -137,9 +139,9 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
             raise AnsibleActionFail(msg)
 
         # Setup the module logging using a logging queue with a listener
-        queue = Queue()
-        setup_module_logging(queue)
-        listener = setup_queue_listener(result, queue)
+        has_errors_ref = [False]
+        listener = setup_queue_listener(LOG_QUEUE, has_errors_ref)
+        setup_parent_process_logging(LOG_QUEUE, display.verbosity)
 
         ansible_forks = task_vars.get("ansible_forks", 5)
 
@@ -182,7 +184,7 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
             if user_catalog_dir is not None:
                 USER_CATALOG = load_user_catalogs(user_catalog_dir)
                 if not generate_avd_catalogs and not USER_CATALOG.tests:
-                    LOGGER.warning("no tests found in the user-defined ANTA catalogs, exiting")
+                    LOGGER.warning("No tests found in the user-defined ANTA catalogs, exiting")
                     return result
 
             # Load the structured configs and build the minimal structured configs if needed
@@ -195,10 +197,10 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
                 batches = [deployed_devices[i : i + batch_size] for i in range(0, len(deployed_devices), batch_size)]
                 batch_results = executor.map(run_anta, batches)
 
-            # Build the ANTA reports
-            result_manager = build_reports(batch_results, get(PLUGIN_ARGS, "report"))
+            # Build the ANTA reports and summary
+            anta_tests_summary = build_reports(batch_results, report_settings=get(PLUGIN_ARGS, "report"))
 
-            result = update_ansible_result(result, result_manager, strict_mode=get(PLUGIN_ARGS, "strict_mode"))
+            result = update_ansible_result(result, anta_tests_summary, has_errors_ref)
 
         except Exception as error:
             # Recast errors as AnsibleActionFail
@@ -213,38 +215,29 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
 
 def run_anta(devices: list[str]) -> ResultManager:
     """Run ANTA."""
-    joined_devices = ", ".join(devices)
-    process_name = current_process().name
+    # Generate a unique ID for this child process run
+    unique_id = f"anta-run-{str(uuid4())[:8]}"
 
-    # Setup process logging and get warning tracker
-    has_warnings_ref, anta_log_filename = setup_process_logging()
+    # Setup child process logging
+    setup_child_process_logging(LOG_QUEUE, display.verbosity, unique_id)
 
     # Build the objects required to run ANTA
     result_manager, inventory, catalog = build_anta_runner_objects(devices)
     tags = set(get(PLUGIN_ARGS, "runner.tags", default=[])) or None
+    dry_run = get(PLUGIN_ARGS, "runner.dry_run")
+    run_mode = "dry-run" if dry_run else "run"
 
     # Run ANTA
-    LOGGER.info("running ANTA in process %s for devices: %s", process_name, joined_devices)
-    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=get(PLUGIN_ARGS, "runner.dry_run")))
-
-    # Check if warnings/errors occurred in ANTA and notify via main logger
-    # ANTA errors are typically handled properly within ANTA by marking impacted
-    # tests as errors or failures, so we don't need to fail the task here
-    if has_warnings_ref[0]:
-        base_message = f"ANTA warnings/errors detected that could impact test results for devices {joined_devices}. "
-        if anta_log_filename:
-            base_message += f"See '{anta_log_filename}' for details."
-            LOGGER.warning(base_message)
-        else:
-            base_message += "No log directory was provided to save ANTA logs."
-            LOGGER.warning(base_message)
-
-    LOGGER.info("ANTA process %s completed for devices: %s", process_name, joined_devices)
+    joined_devices = ", ".join(devices)
+    LOGGER.info("Starting ANTA %s for devices: %s", run_mode, joined_devices)
+    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=dry_run))
+
+    LOGGER.info("ANTA %s completed for devices: %s", run_mode, joined_devices)
     return result_manager
 
 
-def build_reports(batch_results: Iterator[ResultManager], report_settings: dict) -> ResultManager:
-    """Build the ANTA reports from the batch results and return the aggregated results."""
+def build_reports(batch_results: Iterator[ResultManager], report_settings: dict[str, Any]) -> dict[str, Any]:
+    """Build the ANTA reports from the batch results and return a summary dictionary containing ANTA test statistics."""
     hide_statuses = get(report_settings, "filters.hide_statuses")
     csv_output_path = get(report_settings, "csv_output")
     md_output_path = get(report_settings, "md_output")
@@ -265,57 +258,93 @@ def build_reports(batch_results: Iterator[ResultManager], report_settings: dict)
 
     # TODO: Consider using multiprocessing to generate reports in parallel
     if csv_output_path:
-        LOGGER.info("generating CSV report at %s", csv_output_path)
+        LOGGER.info("Generating CSV report at %s", csv_output_path)
         path = Path(csv_output_path)
         report_csv = ReportCsv()
         report_csv.generate(result_manager, path)
 
     if md_output_path:
-        LOGGER.info("generating Markdown report at %s", md_output_path)
+        LOGGER.info("Generating Markdown report at %s", md_output_path)
         path = Path(md_output_path)
         md_report = MDReportGenerator()
         md_report.generate(result_manager, path)
 
     if json_output_path:
-        LOGGER.info("generating JSON report at %s", json_output_path)
+        LOGGER.info("Generating JSON report at %s", json_output_path)
         path = Path(json_output_path)
         with path.open("w", encoding="UTF-8") as file:
             file.write(result_manager.json)
 
-    return result_manager
-
-
-def update_ansible_result(result: dict[str, Any], result_manager: ResultManager, *, strict_mode: bool = False) -> dict[str, Any]:
-    """
-    Update the Ansible result dictionary from the aggregated ANTA results.
-
-    If a test failed or errored, the Ansible task is set to 'failed' if `strict_mode`
-    is True or 'changed' if False.
-    """
-    anta_summary = {
+    # Build a summary with ANTA test stats
+    tests_summary = {
         "total_tests": result_manager.get_total_results(),
-        "passed": result_manager.get_total_results({"success"}),
-        "failed": result_manager.get_total_results({"failure"}),
-        "error": result_manager.get_total_results({"error"}),
-        "skipped": result_manager.get_total_results({"skipped"}),
-        "unset": result_manager.get_total_results({"unset"}),
-        "devices_with_failures": [],
-        "devices_with_errors": [],
+        "tests_passed": result_manager.get_total_results({"success"}),
+        "tests_failed": result_manager.get_total_results({"failure"}),
+        "tests_error": result_manager.get_total_results({"error"}),
+        "tests_skipped": result_manager.get_total_results({"skipped"}),
+        "tests_unset": result_manager.get_total_results({"unset"}),
+        "devices_with_test_failures": [],
+        "devices_with_test_errors": [],
     }
     for device, stat in result_manager.device_stats.items():
         if stat.tests_failure_count:
-            anta_summary["devices_with_failures"].append(device)
+            tests_summary["devices_with_test_failures"].append(device)
         if stat.tests_error_count:
-            anta_summary["devices_with_errors"].append(device)
+            tests_summary["devices_with_test_errors"].append(device)
+
+    return tests_summary
+
 
-    result["anta_summary"] = anta_summary
+def update_ansible_result(result: dict[str, Any], anta_tests_summary: dict[str, Any], has_errors_ref: list[bool]) -> dict[str, Any]:
+    """
+    Update the Ansible result dictionary from aggregated ANTA test results and workflow log errors.
+
+    Ansible task is set to `failed` if any of the following occurs:
+        - No tests ran (outside of a dry run)
+        - Errors were logged by the plugin, PyAVD or ANTA
+        - Any test failed or errored
+
+    Args:
+        result: The Ansible result dictionary to update.
+        anta_tests_summary: The dictionary created from `build_reports` containing aggregated test statistics.
+        has_errors_ref: The boolean list passed to the AntaWorkflowHandler to keep track of error logs.
 
-    # Update task status based on test failures and errors
-    if result_manager.get_status() in {"failure", "error"}:
-        if strict_mode:
-            result["failed"] = True
-        else:
-            result["changed"] = True
+    Returns:
+        dict: The updated Ansible result dictionary.
+    """
+    workflow_log_msg = ""
+    test_result_msg = ""
+
+    # Process workflow errors first
+    failed_by_logs = has_errors_ref[0]
+    if failed_by_logs:
+        workflow_log_msg = "Errors detected during ANTA workflow execution."
+        result["failed"] = True
+
+    # Intermediate flags for test outcomes
+    has_test_issues = anta_tests_summary["tests_failed"] > 0 or anta_tests_summary["tests_error"] > 0
+    no_tests_run = anta_tests_summary["total_tests"] == 0
+
+    # Fail the task if no tests were run
+    if no_tests_run:
+        test_result_msg = "No ANTA tests were run."
+        result["failed"] = True
+    # Fail the task if tests have issues
+    elif has_test_issues:
+        test_result_msg = "Task failed due to ANTA test failures/errors." if not failed_by_logs else "ANTA tests reported failures/errors."
+        result["failed"] = True
+
+    # Tests ran, no issues found, and no log errors
+    elif not failed_by_logs:
+        test_result_msg = "ANTA tests completed without reported failures/errors."
+
+    # Combine messages
+    final_msg = " ".join(filter(None, [workflow_log_msg, test_result_msg]))
+    if final_msg:
+        result["msg"] = final_msg
+
+    # Populate final result dictionary directly from summary
+    result["anta_tests_summary"] = anta_tests_summary
 
     return result
 
@@ -330,7 +359,7 @@ def get_ansible_vars(device_list: list[str], action_plugin_vars: ActionPluginVar
         # Since we can run ANTA without any structured configs, i.e., only using user-defined catalogs,
         # we honor the `is_deployed` flag in the hostvars to skip devices that are not deployed.
         if get(device_vars, "is_deployed", default=True) is False:
-            LOGGER.info("skipping %s - device marked as not deployed", device)
+            LOGGER.info("<%s> Device marked as not deployed - Skipping all tests", device)
             continue
 
         # Adding the Ansible connection variables following the HTTPAPI connection plugin settings
@@ -379,7 +408,7 @@ def build_anta_runner_objects(devices: list[str]) -> tuple[ResultManager, AntaIn
     return result_manager, inventory, catalog
 
 
-def get_device_catalog_filters(device: str, avd_catalogs_filters: list[dict]) -> dict[str, list[str]]:
+def get_device_catalog_filters(device: str, avd_catalogs_filters: list[dict[str, list[str]]]) -> dict[str, list[str]]:
     """
     Get the test filters for a device from the provided AVD catalogs filters.
 
@@ -397,12 +426,12 @@ def get_device_catalog_filters(device: str, avd_catalogs_filters: list[dict]) ->
 
             # Override previous filters if new ones are specified
             if run_tests is not None:
-                if final_filters["run_tests"] is not None:
+                if final_filters["run_tests"]:
                     LOGGER.debug("<%s> run_tests overridden from %s to %s", device, final_filters["run_tests"], run_tests)
                 final_filters["run_tests"] = list(set(run_tests))
 
             if skip_tests is not None:
-                if final_filters["skip_tests"] is not None:
+                if final_filters["skip_tests"]:
                     LOGGER.debug("<%s> skip_tests overridden from %s to %s", device, final_filters["skip_tests"], skip_tests)
                 final_filters["skip_tests"] = list(set(skip_tests))
 
@@ -460,15 +489,15 @@ def load_user_catalogs(catalogs_dir: str) -> AntaCatalog:
 
         file_format = supported_formats.get(path_obj.suffix.lower())
         if not file_format:
-            LOGGER.warning("skipped catalog file %s - unsupported format", path_obj)
+            LOGGER.warning("Skipped user-defined ANTA catalog file %s - unsupported format", path_obj)
             continue
 
-        LOGGER.info("loading catalog from %s", path_obj)
+        LOGGER.info("Loading user-defined ANTA catalog from %s", path_obj)
         catalog = AntaCatalog.parse(path_obj, file_format)
         catalogs.append(catalog)
 
     if not catalogs:
-        LOGGER.info("no custom ANTA catalogs found in directory: %s", catalogs_dir)
+        LOGGER.info("No user-defined ANTA catalogs found in directory: %s", catalogs_dir)
 
     return AntaCatalog.merge_catalogs(catalogs)
 
@@ -491,95 +520,151 @@ def load_one_structured_config(device: str, structured_config_dir: str, structur
         return json.load(stream)
 
 
-def setup_queue_listener(result: dict[str, Any], queue: Queue) -> QueueListener:
+def setup_queue_listener(log_queue: Queue, has_errors_ref: list[bool]) -> QueueListener:
     """
-    Setup and start the queue listener for centralized log handling.
+    Set up and start the queue listener for centralized log handling.
+
+    The listener handler formats logs with a unique ID for context, displays them in the
+    Ansible console respecting verbosity, and track log errors via the has_errors_ref list.
 
-    1. Creates a handler to convert Python logs to Ansible display format
-    2. Starts a queue listener thread to process logs from all processes, including the main process
+    Args:
+      log_queue: Shared queue used by the QueueListener to receive logs from everyone.
+      has_errors_ref: Mutable boolean list to track error logs and above.
 
-    The queue listener processes logs from the central queue configured in `setup_module_logging`
-    and displays them in the Ansible console.
+    Returns:
+      QueueListener: The started QueueListener instance.
     """
-    python_to_ansible_handler = PythonToAnsibleHandler(result, display)
+    log_handler = AntaWorkflowHandler(has_errors_ref)
 
-    listener = QueueListener(queue, python_to_ansible_handler)
+    listener = QueueListener(log_queue, log_handler)
     listener.start()
     return listener
 
 
-def setup_module_logging(queue: Queue) -> None:
+def setup_parent_process_logging(log_queue: Queue, verbosity: int) -> None:
     """
-    Setup logging configuration for the module to handle logs across multiple processes.
-
-    1. Clears existing handlers for the `pyavd` logger and enables propagation to use root queue handler
-    2. Sets up a queue-based logging system where all logs are sent to a central queue
-    3. Filters all logs from ANTA and its dependencies from the queue handler, ANTA logs are written to per-process files
-    4. Sets appropriate log levels based on Ansible verbosity:
-       - verbosity >= 3: DEBUG level
-       - verbosity > 0: INFO level
-       - verbosity = 0: WARNING level
+    Initialize logging for the parent Ansible plugin process.
+
+    Clear existing handlers from the `pyavd` logger configured by the `verify_requirements`
+    plugin and enable propagation to use the shared `log_queue`.
+
+    Args:
+      log_queue: Shared queue for sending logs to the central listener thread.
+      verbosity: Ansible verbosity level used to set the appropriate log level.
     """
     # Clear handlers of `pyavd` logger and set it to propagate to use the root queue handler
     pyavd_logger = logging.getLogger("pyavd")
     pyavd_logger.handlers.clear()
     pyavd_logger.propagate = True
 
-    # Setup the logging configuration for the root logger
+    # Logs from the plugin itself will be prepended with 'anta-workflow'
+    setup_root_logger(unique_id="anta-workflow", log_queue=log_queue, verbosity=verbosity)
+
+    # Configure ANTA debug mode based on Ansible verbosity
+    setup_anta_debug_mode(verbosity=verbosity)
+
+
+def setup_child_process_logging(log_queue: Queue, verbosity: int, unique_id: str) -> None:
+    """
+    Initialize logging for child processes.
+
+    Since the plugin is forked, root handlers inherited from the parent
+    process must be cleared to avoid conflicts with Ansible handlers.
+
+    Args:
+      log_queue: Shared queue used to send logs from this child process to the listener thread.
+      verbosity: Ansible verbosity level used to set the appropriate log level.
+      unique_id: Identifier for the current run that will be prepended to all logs.
+    """
+    # Clear root handlers inherited from the parent process
     root_logger = logging.getLogger()
+    root_logger.handlers.clear()
 
-    # Create a handler to send logs to the queue and add it to the root logger
-    # This handler only receives non-ANTA logs
-    queue_handler = QueueHandler(queue)
-    queue_handler.addFilter(AntaLoggingFilter(exclude=True))
-    root_logger.addHandler(queue_handler)
+    setup_root_logger(unique_id=unique_id, log_queue=log_queue, verbosity=verbosity)
+
+
+def setup_root_logger(unique_id: str, log_queue: Queue, verbosity: int) -> None:
+    """
+    Set up the root logger for parent (plugin) and child processes.
+
+    Args:
+      unique_id: Identifier for the current context that will be prepended to all logs.
+      log_queue: Shared queue used to send logs from all processes to the listener thread.
+      verbosity: Ansible verbosity level used to set the appropriate log level to different loggers.
+    """
+    root_logger = logging.getLogger()
+
+    # ANTA low-level libraries are always at WARNING level except at full verbosity `-vvvvv`
+    low_level_libraries = ("asyncio", "httpcore", "httpx")
+    for logger_name in low_level_libraries:
+        logging.getLogger(logger_name).setLevel(logging.WARNING)
 
-    # Set the level based on Ansible verbosity
-    verbosity = display.verbosity
-    if verbosity >= 3:
+    if verbosity >= 5:
+        # All loggers (pyavd, anta, ansible_collections.arista.avd) including low-level libraries will be at DEBUG
         root_logger.setLevel(logging.DEBUG)
-    elif verbosity > 0:
+        for logger_name in low_level_libraries:
+            logging.getLogger(logger_name).setLevel(logging.DEBUG)
+    elif verbosity == 4:
+        # All loggers except low-level libraries (WARNING) will be at DEBUG
+        root_logger.setLevel(logging.DEBUG)
+    elif verbosity == 3:
+        # All loggers except anta (INFO) and low-level libraries (WARNING) will be at DEBUG
+        root_logger.setLevel(logging.DEBUG)
+        logging.getLogger("anta").setLevel(logging.INFO)
+    elif verbosity in (1, 2):
+        # All loggers except low-level libraries (WARNING) will be at INFO
         root_logger.setLevel(logging.INFO)
     else:
+        # All loggers will be at WARNING
         root_logger.setLevel(logging.WARNING)
 
+    # Create and configure the QueueHandler to send all logs to the listener thread
+    queue_handler = QueueHandler(log_queue)
+    queue_handler.set_name(f"QueueHandler_{unique_id}")
 
-def setup_process_logging() -> tuple[list[bool], str | None]:
-    """
-    Initialize logging for the child processes.
+    # Create the filter that prepends the unique_id
+    log_filter = AntaWorkflowFilter(unique_id=unique_id)
+    queue_handler.addFilter(log_filter)
+
+    # Add the configured QueueHandler to the root logger
+    root_logger.addHandler(queue_handler)
 
-    The child processes inherit the logging configuration from the parent process,
-    configured by `setup_module_logging`.
 
-    In each child process:
-    1. Non-ANTA logs go to the queue for display in the Ansible console
-    2. All ANTA logs go to a dedicated log file (if `logs_dir` is provided to the plugin)
-    3. Warnings logs and above from ANTA libraries update a warning tracker to signal the main process
+def setup_anta_debug_mode(verbosity: int) -> None:
     """
-    root_logger = logging.getLogger()
-    process_name = current_process().name
+    Aligns ANTA debug mode with Ansible verbosity level.
 
-    # Create a boolean tracker wrapped in a list to make it mutable
-    # because we need a mutable reference that can be updated by the filter
-    has_warnings_ref = [False]
-    anta_log_filename = None
+    Overrides `ANTA_DEBUG` to False if it's True and Ansible verbosity < 3 (-vvv).
+    Also clears `PYTHONASYNCIODEBUG` if set by ANTA in this scenario.
+    ANTA tracebacks require verbosity >= 3, following Ansible behavior.
 
-    # Get log directory settings
-    anta_logs_dir = get(PLUGIN_ARGS, "runner.logs_dir")
+    Args:
+      verbosity: Ansible verbosity level.
+    """
+    anta_logger_module = sys.modules.get("anta.logger")
 
-    # If a logs directory is provided, set up ANTA file handler
-    if anta_logs_dir:
-        timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
+    # This should never happen because of the HAS_PYAVD check at the beginning of the plugin
+    if anta_logger_module is None or not hasattr(anta_logger_module, "__DEBUG__"):
+        msg = (
+            "Cannot find the '__DEBUG__' attribute of the 'anta.logger' module, even though PyAVD dependencies were expected to be loaded. "
+            "This indicates a severe issue with the Python environment or ANTA installation."
+        )
+        raise AnsibleActionFail(msg)
 
-        # Setup ANTA log file that captures all ANTA logs at the current log level
-        anta_log_filename = f"{anta_logs_dir}/anta_{timestamp}_{process_name}.log"
-        anta_handler = logging.FileHandler(anta_log_filename, delay=True)
-        anta_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
-        anta_handler.setFormatter(anta_formatter)
+    current_anta_debug_flag = anta_logger_module.__DEBUG__
+    LOGGER.debug("Initial ANTA_DEBUG value: %s", current_anta_debug_flag)
 
-        # Use AntaLoggingFilter to capture only ANTA library logs and update the warning tracker when warnings occur
-        anta_handler.addFilter(AntaLoggingFilter(exclude=False, has_warnings_ref=has_warnings_ref))
-        root_logger.addHandler(anta_handler)
+    if current_anta_debug_flag is True and verbosity < 3:
+        LOGGER.debug("ANTA_DEBUG is True and Ansible verbosity (%d) < 3. Overriding ANTA_DEBUG to False for this plugin run", verbosity)
+        anta_logger_module.__DEBUG__ = False
 
-    # Return the warning tracker and filename to be used later
-    return has_warnings_ref, anta_log_filename
+        if os.environ.get("PYTHONASYNCIODEBUG") == "1":
+            LOGGER.debug(
+                "ANTA_DEBUG was True (causing PYTHONASYNCIODEBUG=1). "
+                "Since ANTA_DEBUG is now overridden to False by the plugin, deleting PYTHONASYNCIODEBUG environment variable"
+            )
+            del os.environ["PYTHONASYNCIODEBUG"]
+    elif current_anta_debug_flag is True and verbosity >= 3:
+        LOGGER.debug("ANTA_DEBUG is True and Ansible verbosity (%d) >= 3. ANTA debug mode will remain active as per the environment variable", verbosity)
+    else:
+        LOGGER.debug("ANTA_DEBUG is False. Plugin will not change ANTA debug settings")
diff --git a/ansible_collections/arista/avd/plugins/modules/anta_workflow.py b/ansible_collections/arista/avd/plugins/modules/anta_workflow.py
index 8063b4c8131..a0ab3adbbd0 100644
--- a/ansible_collections/arista/avd/plugins/modules/anta_workflow.py
+++ b/ansible_collections/arista/avd/plugins/modules/anta_workflow.py
@@ -99,9 +99,6 @@
           and a report is created to preview the tests that would be run against each device.
         type: bool
         default: false
-      logs_dir:
-        description: Directory where the ANTA debug logs will be stored. Logs are stored per ANTA instance. Debug logs can be created using `-vvv` verbosity.
-        type: str
   report:
     description: ANTA report settings. These settings define the output format and location of the ANTA reports.
     type: dict
@@ -124,12 +121,6 @@
             type: list
             elements: str
             choices: ["success", "failure", "error", "skipped", "unset"]
-  strict_mode:
-    description: |-
-      When `strict_mode` is `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-      Otherwise, the plugin returns `changed: true` in such cases.
-    type: bool
-    default: false
 seealso:
   - name: ANTA website
     description: Documentation for the ANTA test framework
@@ -165,7 +156,6 @@
           # tags:
           #   - leaf
           # dry_run: true
-          logs_dir: "{{ inventory_dir }}/anta/logs"
         report:
           csv_output: "{{ inventory_dir }}/anta/reports/anta_report.csv"
           md_output: "{{ inventory_dir }}/anta/reports/anta_report.md"
@@ -174,5 +164,4 @@
           #   hide_statuses:
           #     - success
           #     - skipped
-        strict_mode: true
 """
diff --git a/ansible_collections/arista/avd/plugins/plugin_utils/utils/__init__.py b/ansible_collections/arista/avd/plugins/plugin_utils/utils/__init__.py
index 7e4efc6f737..5a539bebd24 100644
--- a/ansible_collections/arista/avd/plugins/plugin_utils/utils/__init__.py
+++ b/ansible_collections/arista/avd/plugins/plugin_utils/utils/__init__.py
@@ -4,7 +4,7 @@
 from ansible_collections.arista.avd.plugins.plugin_utils.pyavd_wrappers import RaiseOnUse
 
 from .action_plugin_vars import ActionPluginVars
-from .anta_logging_filter import AntaLoggingFilter
+from .anta_workflow_logging import AntaWorkflowFilter, AntaWorkflowHandler
 from .avd_switch_facts_default_dict import AvdSwitchFactsDefaultDict
 from .compile_searchpath import compile_searchpath
 from .cprofile_decorator import cprofile
@@ -25,7 +25,8 @@
 
 __all__ = [
     "ActionPluginVars",
-    "AntaLoggingFilter",
+    "AntaWorkflowFilter",
+    "AntaWorkflowHandler",
     "AvdSwitchFactsDefaultDict",
     "NoAliasDumper",
     "PythonToAnsibleContextFilter",
diff --git a/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_logging_filter.py b/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_logging_filter.py
deleted file mode 100644
index 09cfe882602..00000000000
--- a/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_logging_filter.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (c) 2025 Arista Networks, Inc.
-# Use of this source code is governed by the Apache License 2.0
-# that can be found in the LICENSE file.
-import logging
-
-
-class AntaLoggingFilter(logging.Filter):
-    """Filter logs for ANTA and its underlying libraries."""
-
-    _anta_libraries = ("anta", "asyncio", "asyncssh", "httpcore", "httpx")
-
-    def __init__(self, has_warnings_ref: list[bool] | None = None, *, exclude: bool = True) -> None:
-        """
-        Initializes the filter.
-
-        Args:
-          has_warnings_ref: Reference to a boolean list to track if any warnings or above are logged.
-          exclude: Whether to exclude ANTA logs or not.
-        """
-        super().__init__()
-        self.exclude = exclude
-        self.has_warnings_ref = has_warnings_ref
-
-    def filter(self, record: logging.LogRecord) -> bool:
-        """Filter logs based on whether they're from ANTA libraries."""
-        is_anta_library = any(record.name.startswith(name) for name in self._anta_libraries)
-
-        # Update the warning tracker if this is a warning or above from an ANTA library
-        if self.has_warnings_ref is not None and is_anta_library and record.levelno >= logging.WARNING:
-            self.has_warnings_ref[0] = True
-
-        # If exclude=True, return True for non-ANTA logs
-        # If exclude=False, return True for ANTA logs
-        return not is_anta_library if self.exclude else is_anta_library
diff --git a/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py b/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
new file mode 100644
index 00000000000..ef9e4f27d53
--- /dev/null
+++ b/ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+import logging
+
+from ansible.utils.display import Display
+
+
+class AntaWorkflowFilter(logging.Filter):
+    """
+    ANTA workflow logging filter.
+
+    Injects a unique ID into log records for context tracking.
+    """
+
+    def __init__(self, unique_id: str) -> None:
+        """
+        Initialize the filter.
+
+        Args:
+          unique_id: Identifier for the current context (e.g., 'anta-workflow'
+                     or 'anta-run-xxxxxxxx'). Added to all records.
+        """
+        super().__init__()
+        self.unique_id = unique_id
+
+    def filter(self, record: logging.LogRecord) -> bool:
+        """
+        Add `unique_id` to the record.
+
+        Args:
+          record: The log record to be processed.
+
+        Returns:
+          bool: Always True.
+        """
+        record.unique_id = self.unique_id
+        return True
+
+
+class AntaWorkflowHandler(logging.Handler):
+    """
+    ANTA workflow logging handler.
+
+    Route log records to Ansible Display based on level and track errors in a
+    boolean list.
+    """
+
+    def __init__(self, has_errors_ref: list[bool]) -> None:
+        """
+        Initialize the handler.
+
+        Args:
+          has_errors_ref: Mutable boolean list to track errors.
+                          It's expected to be a list with one boolean element.
+        """
+        super().__init__()
+        self.display = Display()
+        self.has_errors_ref = has_errors_ref
+
+    def emit(self, record: logging.LogRecord) -> None:
+        """
+        Process a log record.
+
+        Formats the message (including unique_id) and sends it to the appropriate
+        Ansible Display method. Also update the error boolean list when handling error
+        logs and above.
+
+        Args:
+          record: The log record to be processed.
+        """
+        # Get the unique_id injected by the AntaLoggingFilter
+        unique_id = getattr(record, "unique_id", "unknown")
+
+        # Format the message including unique_id
+        base_message = self.format(record)
+        final_message = f"[{unique_id}] {base_message}"
+
+        if record.levelno >= logging.ERROR:
+            self.display.error(final_message, wrap_text=False)
+            self.has_errors_ref[0] = True
+        elif record.levelno == logging.WARNING:
+            self.display.warning(final_message)
+        elif record.levelno == logging.INFO:
+            self.display.v(final_message)
+        elif record.levelno == logging.DEBUG:
+            self.display.vvv(final_message)
diff --git a/ansible_collections/arista/avd/roles/anta_runner/README.md b/ansible_collections/arista/avd/roles/anta_runner/README.md
index 1dd9cf9fb22..4627fe55194 100644
--- a/ansible_collections/arista/avd/roles/anta_runner/README.md
+++ b/ansible_collections/arista/avd/roles/anta_runner/README.md
@@ -58,6 +58,15 @@ This example will generate a per-device test catalog based on the AVD structured
 
 The artifacts generated by the role, including results from ANTA, will be stored in the `anta` folder in the inventory directory.
 
+The `Run ANTA on EOS devices` task will fail if any of the following conditions occur:
+
+- **No tests executed:** If no tests were run against any device (and it's not a `dry_run`)
+- **Execution errors:** If errors arise during the role execution, originating from Ansible, PyAVD, or ANTA
+- **Test failures or errors:** If any ANTA test reports a failure or error status.
+
+!!! tip
+    To continue running the playbook when `anta_runner` fails, use Ansible `ignore_errors`. For more details, refer to the [Ansible documentation](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html).
+
 ## Roadmap
 
 This feature is still under development, and several planned features are not implemented yet:
@@ -179,26 +188,11 @@ anta_runner_batch_size: 5
 
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
-
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
 ```
 
 !!! tip
     The role automatically respects the `ansible_forks` setting to determine the maximum number of worker processes in conjunction with `anta_runner_batch_size`. For large deployment with many devices, consider adjusting these settings to optimize performance while managing resource usage.
 
-### Verbose Mode and Logging
-
-The `anta_runner` role supports default Ansible verbosity levels, controlled by the `-v` flag. With `-vvv`, information about tests being skipped from the AVD-generated catalogs is displayed at the console.
-
-ANTA logs are also controlled by the verbosity level but are stored in per-batch files in the `anta_runner_logs_dir` directory.
-
-To enable verbose logging, use the `-vvv` flag with the `ansible-playbook` command.
-
-!!! tip
-    The `anta_runner_logs_dir` can accumulate logs over time. Implementing custom cleanup strategies or utilizing log rotation tools for this directory is recommended for long-term maintenance.
-
 ### Advanced Filtering Options
 
 Optional filtering mechanisms are available to modify the behavior of the role.
@@ -325,11 +319,6 @@ user_catalogs_dir: "{{ anta_dir }}/{{ user_catalogs_dir_name }}"
 avd_catalogs_dir_name: "avd_catalogs"
 avd_catalogs_dir: "{{ anta_dir }}/{{ avd_catalogs_dir_name }}"
 
-# Directory for the ANTA debug's logs. When running the role with `-vvv`,
-# all ANTA logs will be stored here by batch.
-anta_runner_logs_dir_name: "logs"
-anta_runner_logs_dir: "{{ anta_dir }}/{{ anta_runner_logs_dir_name }}"
-
 # Directory for ANTA reports.
 anta_reports_dir_name: "reports"
 anta_reports_dir: "{{ anta_dir }}/{{ anta_reports_dir_name }}"
@@ -385,6 +374,34 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role uses Ansible standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+```bash
+ansible-playbook playbooks/anta-runner.yml -vvv
+```
+
+!!! tip
+    Running `ansible-playbook` with `-vvv` is particularly helpful to troubleshoot why some specific AVD-generated tests are missing from a device final catalog. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
+
+### Understanding Log Output
+
+`anta_runner` processes devices in parallel batches for improved performance. To help correlate log messages to specific operations or device batches, each log entry is prefixed with a unique identifier:
+
+- `anta-workflow`: Indicates logs from the main Ansible action plugin process.
+- `anta-run-xxxxxxxx`: Indicates logs from a specific ANTA execution run handling a batch of devices.
+
+These prefixes are essential when debugging issues related to a particular device or test batch.
+
+### Managing Log Files
+
+Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially when using high verbosity, consider configuring `log_path` in your `ansible.cfg` file to redirect the primary output stream to the specified log file. For more details, refer to the [Ansible documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
+
 ## License
 
 Project is published under [Apache 2.0 License](https://github.com/aristanetworks/avd/blob/devel/LICENSE).
diff --git a/ansible_collections/arista/avd/roles/anta_runner/defaults/main/directories.yml b/ansible_collections/arista/avd/roles/anta_runner/defaults/main/directories.yml
index 6318494d8b2..4dde88d2cf3 100644
--- a/ansible_collections/arista/avd/roles/anta_runner/defaults/main/directories.yml
+++ b/ansible_collections/arista/avd/roles/anta_runner/defaults/main/directories.yml
@@ -24,10 +24,6 @@ user_catalogs_dir: "{{ anta_dir }}/{{ user_catalogs_dir_name }}"
 avd_catalogs_dir_name: "avd_catalogs"
 avd_catalogs_dir: "{{ anta_dir }}/{{ avd_catalogs_dir_name }}"
 
-# Output for ANTA log files.
-anta_runner_logs_dir_name: "logs"
-anta_runner_logs_dir: "{{ anta_dir }}/{{ anta_runner_logs_dir_name }}"
-
 # Output for ANTA reports.
 anta_reports_dir_name: "reports"
 anta_reports_dir: "{{ anta_dir }}/{{ anta_reports_dir_name }}"
diff --git a/ansible_collections/arista/avd/roles/anta_runner/defaults/main/main.yml b/ansible_collections/arista/avd/roles/anta_runner/defaults/main/main.yml
index 876f3331b19..240e924fec0 100644
--- a/ansible_collections/arista/avd/roles/anta_runner/defaults/main/main.yml
+++ b/ansible_collections/arista/avd/roles/anta_runner/defaults/main/main.yml
@@ -21,9 +21,5 @@ anta_runner_batch_size: 5
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
 
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
-
 # Structured configuration file format. Supported formats: yml, yaml, json.
 avd_structured_config_file_format: "yml"
diff --git a/ansible_collections/arista/avd/roles/anta_runner/tasks/main.yml b/ansible_collections/arista/avd/roles/anta_runner/tasks/main.yml
index cc200c73907..358202217e3 100644
--- a/ansible_collections/arista/avd/roles/anta_runner/tasks/main.yml
+++ b/ansible_collections/arista/avd/roles/anta_runner/tasks/main.yml
@@ -24,7 +24,6 @@
     - "{{ avd_catalogs_dir }}"
     - "{{ user_catalogs_dir }}"
     - "{{ anta_reports_dir }}"
-    - "{{ anta_runner_logs_dir }}"
   delegate_to: localhost
   run_once: true
   check_mode: false
@@ -46,14 +45,12 @@
       batch_size: "{{ anta_runner_batch_size }}"
       tags: "{{ anta_runner_tags | arista.avd.default([]) }}"
       dry_run: "{{ anta_runner_dry_run }}"
-      logs_dir: "{{ anta_runner_logs_dir }}"
     report:
       csv_output: "{{ anta_report_csv_path }}"
       md_output: "{{ anta_report_md_path }}"
       json_output: "{{ anta_report_json_path }}"
       filters:
         hide_statuses: "{{ anta_report_hide_statuses | arista.avd.default([]) }}"
-    strict_mode: "{{ anta_strict_mode }}"
   register: anta
   delegate_to: localhost
   run_once: true
diff --git a/ansible_collections/arista/avd/roles/build_output_folders/defaults/main.yml b/ansible_collections/arista/avd/roles/build_output_folders/defaults/main.yml
index 251712af94d..076dfc86606 100644
--- a/ansible_collections/arista/avd/roles/build_output_folders/defaults/main.yml
+++ b/ansible_collections/arista/avd/roles/build_output_folders/defaults/main.yml
@@ -72,7 +72,3 @@ avd_catalogs_dir: "{{ anta_dir }}/{{ avd_catalogs_dir_name }}"
 # Output for ANTA reports
 anta_reports_dir_name: "reports"
 anta_reports_dir: "{{ anta_dir }}/{{ anta_reports_dir_name }}"
-
-# Output for ANTA log files
-anta_runner_logs_dir_name: "logs"
-anta_runner_logs_dir: "{{ anta_dir }}/{{ anta_runner_logs_dir_name }}"
diff --git a/ansible_collections/arista/avd/roles/build_output_folders/tasks/main.yml b/ansible_collections/arista/avd/roles/build_output_folders/tasks/main.yml
index 4934c5e111e..370f3648f9a 100644
--- a/ansible_collections/arista/avd/roles/build_output_folders/tasks/main.yml
+++ b/ansible_collections/arista/avd/roles/build_output_folders/tasks/main.yml
@@ -158,11 +158,3 @@
     mode: "0o775"
   delegate_to: localhost
   run_once: true
-
-- name: "Create folder {{ anta_runner_logs_dir_name }} for ANTA logs"
-  ansible.builtin.file:
-    path: "{{ anta_runner_logs_dir }}"
-    state: directory
-    mode: "0o775"
-  delegate_to: localhost
-  run_once: true
diff --git a/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/conftest.py b/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/conftest.py
index 0f35524181d..9c5254adc98 100644
--- a/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/conftest.py
+++ b/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/conftest.py
@@ -3,11 +3,12 @@
 # that can be found in the LICENSE file.
 """Fixtures for testing the utils modules."""
 
+import logging
 from pathlib import Path
+from unittest.mock import Mock
 
-import logging
 import pytest
-
+from ansible.cli import Display
 from ansible.inventory.manager import InventoryManager
 from ansible.parsing.dataloader import DataLoader
 from ansible.playbook.block import Block
@@ -35,7 +36,8 @@ def __init__(self, task: Task) -> None:
 
 @pytest.fixture
 def ansible_task(request: pytest.FixtureRequest) -> Task:
-    """Build a dummy Ansible task with parametrized data.
+    """
+    Build a dummy Ansible task with parametrized data.
 
     Parameters can be specified using indirect parametrization:
 
@@ -63,43 +65,25 @@ def ansible_task(request: pytest.FixtureRequest) -> Task:
     return Task.load(task_data, block, variable_manager=variable_manager, loader=loader)
 
 
-@pytest.fixture
-def anta_record() -> logging.LogRecord:
-    """Create a log record from an ANTA library."""
-    return logging.LogRecord(
-        name="anta.runner",
-        level=logging.INFO,
-        pathname="",
-        lineno=0,
-        msg="Hello from ANTA",
-        args=(),
-        exc_info=None
-    )
-
-
-@pytest.fixture
-def non_anta_record() -> logging.LogRecord:
-    """Create a log record from a non-ANTA library."""
-    return logging.LogRecord(
-        name="pyavd",
-        level=logging.INFO,
-        pathname="",
-        lineno=0,
-        msg="Hello from PyAVD",
-        args=(),
-        exc_info=None
+def create_log_record(name: str, level: int, msg: str, unique_id: str | None = None, args: tuple = ()) -> logging.LogRecord:
+    """Helper function to create a LogRecord, optionally adding unique_id."""
+    record = logging.LogRecord(
+        name=name,
+        level=level,
+        pathname="dummy_path",
+        lineno=123,
+        msg=msg,
+        args=args,
+        exc_info=None,
+        func="dummy_func",
     )
+    # Simulate the filter adding the unique_id *before* it reaches the handler
+    if unique_id:
+        record.unique_id = unique_id
+    return record
 
 
 @pytest.fixture
-def warning_record() -> logging.LogRecord:
-    """Create a warning log record from an ANTA library."""
-    return logging.LogRecord(
-        name="anta.runner",
-        level=logging.WARNING,
-        pathname="",
-        lineno=0,
-        msg="Warning from ANTA",
-        args=(),
-        exc_info=None
-    )
+def mock_display() -> Mock:
+    """Fixture for creating a mock Ansible Display object."""
+    return Mock(spec=Display)
diff --git a/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_logging_filter.py b/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_logging_filter.py
deleted file mode 100644
index 93f59462b0f..00000000000
--- a/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_logging_filter.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# Copyright (c) 2025 Arista Networks, Inc.
-# Use of this source code is governed by the Apache License 2.0
-# that can be found in the LICENSE file.
-"""Unit tests for the anta_logging_filter module."""
-import logging
-
-from ansible_collections.arista.avd.plugins.plugin_utils.utils import AntaLoggingFilter
-
-
-class TestAntaLoggingFilter:
-    """Test the AntaLoggingFilter class."""
-
-    def test_exclude_anta_logs(self, anta_record: logging.LogRecord, non_anta_record: logging.LogRecord) -> None:
-        """Test that ANTA library logs are excluded when exclude=True."""
-        filter_obj = AntaLoggingFilter(exclude=True)
-
-        # ANTA library logs should be filtered out (return False)
-        assert filter_obj.filter(anta_record) is False
-
-        # Non-ANTA library logs should be kept (return True)
-        assert filter_obj.filter(non_anta_record) is True
-
-    def test_include_anta_logs(self, anta_record: logging.LogRecord, non_anta_record: logging.LogRecord) -> None:
-        """Test that ANTA library logs are included when exclude=False."""
-        filter_obj = AntaLoggingFilter(exclude=False)
-
-        # ANTA library logs should be kept (return True)
-        assert filter_obj.filter(anta_record) is True
-
-        # Non-ANTA library logs should be filtered out (return False)
-        assert filter_obj.filter(non_anta_record) is False
-
-    def test_warning_tracking(self, anta_record: logging.LogRecord, warning_record: logging.LogRecord) -> None:
-        """Test that warnings from ANTA libraries are tracked."""
-        has_warnings = [False]
-        filter_obj = AntaLoggingFilter(has_warnings_ref=has_warnings)
-
-        # Filter should not change the warning tracker for INFO records
-        filter_obj.filter(anta_record)
-        assert has_warnings[0] is False
-
-        # Filter should update the warning tracker for WARNING records
-        filter_obj.filter(warning_record)
-        assert has_warnings[0] is True
-
-    def test_other_anta_libraries(self) -> None:
-        """Test that logs from other ANTA libraries are filtered correctly."""
-        filter_obj = AntaLoggingFilter(exclude=True)
-
-        # Test each ANTA library
-        for library in AntaLoggingFilter._anta_libraries:
-            record = logging.LogRecord(
-                name=f"{library}.something",
-                level=logging.INFO,
-                pathname="",
-                lineno=0,
-                msg="Test message",
-                args=(),
-                exc_info=None
-            )
-
-            # Should be filtered out in exclude mode
-            assert filter_obj.filter(record) is False
-
-    def test_non_anta_libraries(self) -> None:
-        """Test that non-ANTA libraries are correctly identified."""
-        filter_obj = AntaLoggingFilter(exclude=True)
-
-        # Test some non-ANTA libraries
-        for library in ["pyavd", "ansible_collections.arista.avd"]:
-            record = logging.LogRecord(
-                name=f"{library}.something",
-                level=logging.INFO,
-                pathname="",
-                lineno=0,
-                msg="Test message",
-                args=(),
-                exc_info=None
-            )
-
-            # Should be kept in exclude mode
-            assert filter_obj.filter(record) is True
diff --git a/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_workflow_logging.py b/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_workflow_logging.py
new file mode 100644
index 00000000000..d6af9ce0684
--- /dev/null
+++ b/ansible_collections/arista/avd/tests/unit/plugins/plugin_utils/utils/test_anta_workflow_logging.py
@@ -0,0 +1,147 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Unit tests for the anta_logging_filter module."""
+
+import logging
+from unittest.mock import Mock
+
+import pytest
+from ansible.cli import Display
+
+from ansible_collections.arista.avd.plugins.plugin_utils.utils import AntaWorkflowFilter, AntaWorkflowHandler
+
+from .conftest import create_log_record
+
+
+class TestAntaWorkflowFilter:
+    """Test suite for the AntaWorkflowFilter class."""
+
+    def test_anta_workflow_filter_init(self) -> None:
+        """Test AntaWorkflowFilter initialization."""
+        test_id = "test-filter-id-123"
+        custom_filter = AntaWorkflowFilter(unique_id=test_id)
+        assert custom_filter.unique_id == test_id
+
+    def test_anta_workflow_filter_adds_unique_id(self) -> None:
+        """Test AntaWorkflowFilter adds unique_id to the record."""
+        test_id = "filter-id-for-record"
+        custom_filter = AntaWorkflowFilter(unique_id=test_id)
+        record = create_log_record("test.logger", logging.INFO, "A test message")
+
+        # Ensure unique_id is not present before filtering
+        assert not hasattr(record, "unique_id")
+
+        result = custom_filter.filter(record)
+
+        assert result is True
+        assert hasattr(record, "unique_id")
+        assert record.unique_id == test_id
+
+
+class TestAntaWorkflowHandler:
+    """Test suite for the AntaWorkflowHandler class."""
+
+    def test_anta_workflow_handler_init(self) -> None:
+        """Test AntaWorkflowHandler initialization."""
+        errors_list = [False]
+        handler = AntaWorkflowHandler(has_errors_ref=errors_list)
+        assert handler.display is not None
+        assert isinstance(handler.display, Display)
+        assert handler.has_errors_ref == errors_list
+
+    @pytest.mark.parametrize(
+        ("level", "msg", "expected_display_method_name", "set_error_true"),
+        [
+            pytest.param(logging.DEBUG, "Debug message", "vvv", False, id="debug_level"),
+            pytest.param(logging.INFO, "Info message", "v", False, id="info_level"),
+            pytest.param(logging.WARNING, "Warning message", "warning", False, id="warning_level"),
+            pytest.param(logging.ERROR, "Error message", "error", True, id="error_level"),
+            pytest.param(logging.CRITICAL, "Critical message", "error", True, id="critical_level"),
+        ],
+    )
+    def test_anta_workflow_handler_emit_levels(
+        self, mock_display: Display, level: int, msg: str, expected_display_method_name: str, set_error_true: bool
+    ) -> None:
+        """Test AntaWorkflowHandler emit calls the correct display method and updates errors."""
+        unique_context_id = "ctx-123"
+        errors_list = [False]
+        handler = AntaWorkflowHandler(has_errors_ref=errors_list)
+        handler.display = mock_display
+        # Set a basic formatter to ensure record.getMessage() works as expected
+        handler.setFormatter(logging.Formatter("%(message)s"))
+
+        record = create_log_record("test.emit", level, msg, unique_id=unique_context_id)
+        handler.emit(record)
+
+        expected_message = f"[{unique_context_id}] {msg}"
+
+        # Get the mock method from the display mock
+        display_method_mock = getattr(mock_display, expected_display_method_name)
+
+        if expected_display_method_name == "error":
+            display_method_mock.assert_called_once_with(expected_message, wrap_text=False)
+        else:
+            display_method_mock.assert_called_once_with(expected_message)
+
+        # Verify other display methods were NOT called
+        all_display_methods = {"v", "vvv", "warning", "error"}
+        for method_name in all_display_methods:
+            if method_name != expected_display_method_name:
+                method_to_check = getattr(mock_display, method_name)
+                method_to_check.assert_not_called()
+
+        assert errors_list[0] is set_error_true
+
+    def test_anta_workflow_handler_emit_unknown_unique_id(self, mock_display: Mock) -> None:
+        """Test AntaWorkflowHandler emit with a record that has no unique_id attribute."""
+        errors_list = [False]
+        handler = AntaWorkflowHandler(has_errors_ref=errors_list)
+        handler.display = mock_display
+        handler.setFormatter(logging.Formatter("%(message)s"))
+
+        log_message = "Info message without explicit unique_id"
+        # Create a record without unique_id (it won't be added by the helper in this case)
+        record = logging.LogRecord(
+            name="test.unknown_id",
+            level=logging.INFO,
+            pathname="dummy_path",
+            lineno=123,
+            msg=log_message,
+            args=(),
+            exc_info=None,
+            func="dummy_func",
+        )
+        # Sanity check: ensure unique_id is not on the record
+        assert not hasattr(record, "unique_id")
+
+        handler.emit(record)
+
+        expected_message = f"[unknown] {log_message}"
+        mock_display.v.assert_called_once_with(expected_message)
+        assert errors_list[0] is False
+
+    def test_anta_workflow_handler_emit_message_formatting_with_args(self, mock_display: Mock) -> None:
+        """Test that emit correctly formats messages with arguments."""
+        unique_context_id = "ctx-format"
+        errors_list = [False]
+        handler = AntaWorkflowHandler(has_errors_ref=errors_list)
+        handler.display = mock_display
+        # Using a formatter that includes the message part
+        formatter = logging.Formatter("%(message)s")
+        handler.setFormatter(formatter)
+
+        msg_template = "User %s logged in from %s"
+        msg_args = ("testuser", "192.168.1.100")
+        # The LogRecord will store the template and args separately
+        # record.getMessage() or handler.format(record) will combine them
+        record = create_log_record("test.format", logging.INFO, msg_template, unique_id=unique_context_id, args=msg_args)
+
+        handler.emit(record)
+
+        # The formatter will produce "User testuser logged in from 192.168.1.100"
+        formatted_base_message = msg_template % msg_args
+        expected_final_message = f"[{unique_context_id}] {formatted_base_message}"
+
+        mock_display.v.assert_called_once_with(expected_final_message)
+        assert not errors_list[0]
diff --git a/docs/plugins/Modules_and_action_plugins/anta_workflow.md b/docs/plugins/Modules_and_action_plugins/anta_workflow.md
index ea77d90cb07..59834f3728e 100644
--- a/docs/plugins/Modules_and_action_plugins/anta_workflow.md
+++ b/docs/plugins/Modules_and_action_plugins/anta_workflow.md
@@ -57,14 +57,12 @@ The plugin offers the following capabilities:
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;batch_size</samp> | int | optional | 5 |  | Number of devices to run per ANTA instance. This can be increased based on the available resources of the Ansible runner. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;tags</samp> | any | optional | None |  | List of tags used with user-defined catalogs to filter which tests to run on which devices.<br>These tags are used in conjunction with `anta_tags` variable assigned to devices in the Ansible inventory. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;dry_run</samp> | bool | optional | False |  | Run ANTA in dry-run mode. In this mode, only the test catalogs are generated,<br>and a report is created to preview the tests that would be run against each device. |
-| <samp>&nbsp;&nbsp;&nbsp;&nbsp;logs_dir</samp> | str | optional | None |  | Directory where the ANTA debug logs will be stored. Logs are stored per ANTA instance. Debug logs can be created using `-vvv` verbosity. |
 | <samp>report</samp> | dict | optional | None |  | ANTA report settings. These settings define the output format and location of the ANTA reports. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;csv_output</samp> | str | optional | None |  | Path to the CSV report file. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;md_output</samp> | str | optional | None |  | Path to the Markdown report file. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;json_output</samp> | str | optional | None |  | Path to the JSON report file. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;filters</samp> | dict | optional | None |  | Filters used to hide specific test statuses from the reports. |
 | <samp>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hide_statuses</samp> | list | optional | None | Valid values:<br>- <code>success</code><br>- <code>failure</code><br>- <code>error</code><br>- <code>skipped</code><br>- <code>unset</code> | List of test statuses to hide from the reports. |
-| <samp>strict_mode</samp> | bool | optional | False |  | When `strict_mode` is `true`, the plugin returns `failed: true` if any ANTA tests fail or error.<br>Otherwise, the plugin returns `changed: true` in such cases. |
 
 ## See Also
 
@@ -101,7 +99,6 @@ The plugin offers the following capabilities:
           # tags:
           #   - leaf
           # dry_run: true
-          logs_dir: "{{ inventory_dir }}/anta/logs"
         report:
           csv_output: "{{ inventory_dir }}/anta/reports/anta_report.csv"
           md_output: "{{ inventory_dir }}/anta/reports/anta_report.md"
@@ -110,7 +107,6 @@ The plugin offers the following capabilities:
           #   hide_statuses:
           #     - success
           #     - skipped
-        strict_mode: true
 ```
 
 ## Authors
diff --git a/python-avd/pyavd/_anta/factories.py b/python-avd/pyavd/_anta/factories.py
index 4111c281ee8..8074f300a7a 100644
--- a/python-avd/pyavd/_anta/factories.py
+++ b/python-avd/pyavd/_anta/factories.py
@@ -22,13 +22,10 @@
     from pyavd.api._anta import InputFactorySettings, MinimalStructuredConfig, TestSpec
 
 
-LOGGER = getLogger(__name__)
-
-
 def create_catalog(
     hostname: str,
     structured_config: dict[str, Any],
-    structured_configs: dict[str, MinimalStructuredConfig],
+    minimal_structured_configs: dict[str, MinimalStructuredConfig],
     input_factory_settings: InputFactorySettings,
     test_specs: list[TestSpec],
 ) -> AntaCatalog:
@@ -36,13 +33,12 @@ def create_catalog(
     device_context = DeviceTestContext(
         hostname=hostname,
         structured_config=EosCliConfigGen._load(structured_config),
-        structured_configs=structured_configs,
+        minimal_structured_configs=minimal_structured_configs,
         input_factory_settings=input_factory_settings,
     )
     tests: list[AntaTestDefinition] = []
     for test in test_specs:
-        test_logger = TestLoggerAdapter.create(device=hostname, test=test.test_class.name, logger=LOGGER)
-        test_definitions = create_test_definitions(test, device_context, test_logger)
+        test_definitions = create_test_definitions(test, device_context)
 
         # Skip the test if we couldn't create the test definitions. Logging is done when creating the test definitions
         if test_definitions is None:
@@ -63,20 +59,22 @@ def create_catalog(
     return AntaCatalog(tests=tests)
 
 
-def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> list[AntaTestDefinition] | None:
+def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext) -> list[AntaTestDefinition] | None:
     """Create the AntaTestDefinition's from this TestSpec instance."""
+    logger_adapter = TestLoggerAdapter(logger=getLogger(__name__), extra={"device": device_context.hostname, "test": test_spec.test_class.name})
+
     # Skip the test if the conditional keys are not present in the structured config
     if test_spec.conditional_keys and not all(get_v2(device_context.structured_config, key.value) for key in test_spec.conditional_keys):
         keys = StructuredConfigKey.to_string_list(test_spec.conditional_keys)
-        logger.debug(LogMessage.INPUT_NO_DATA_MODEL, caller=", ".join(keys))
+        logger_adapter.debug(LogMessage.INPUT_NO_DATA_MODELS, data_models=", ".join(keys))
         return None
 
     # Create the test definitions from the input factory if provided
     if test_spec.input_factory is not None:
-        factory = test_spec.input_factory(device_context, logger)
+        factory = test_spec.input_factory(device_context, test_spec.test_class.name)
         results = factory.create()
         if results is None:
-            logger.debug(LogMessage.INPUT_NONE_FOUND)
+            logger_adapter.debug(LogMessage.INPUT_NONE_FOUND)
             return None
         return [AntaTestDefinition(test=test_spec.test_class, inputs=inputs) for inputs in results]
 
diff --git a/python-avd/pyavd/_anta/input_factories/_base_classes.py b/python-avd/pyavd/_anta/input_factories/_base_classes.py
index 662062c22d2..9c874df9eda 100644
--- a/python-avd/pyavd/_anta/input_factories/_base_classes.py
+++ b/python-avd/pyavd/_anta/input_factories/_base_classes.py
@@ -4,14 +4,14 @@
 from __future__ import annotations
 
 from abc import ABC, abstractmethod
+from logging import getLogger
 from typing import TYPE_CHECKING
 
-from pyavd._anta.logs import LogMessage
+from pyavd._anta.logs import LogMessage, TestLoggerAdapter
 
 if TYPE_CHECKING:
     from anta.models import AntaTest
 
-    from pyavd._anta.logs import TestLoggerAdapter
     from pyavd._anta.models import DeviceTestContext
 
 
@@ -28,36 +28,38 @@ class AntaTestInputFactory(ABC):
         The device context for the test.
     structured_config : EosCliConfigGen
         The structured configuration model of the device.
-    structured_configs : dict[str, MinimalStructuredConfig]
+    minimal_structured_configs : dict[str, MinimalStructuredConfig]
         The minimal structured configurations of all devices in the fabric.
-    logger : TestLoggerAdapter
-        Custom logger used for the input factory.
+    logger_adapter : TestLoggerAdapter
+        Custom logger adapter used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
-        self.structured_configs = device_context.structured_configs
-        self.logger = logger
+        self.minimal_structured_configs = device_context.minimal_structured_configs
+
+        # Create the logger adapter for the test input factory
+        self.logger_adapter = TestLoggerAdapter(logger=getLogger(self.__module__), extra={"device": self.device.hostname, "test": test_name})
 
     @abstractmethod
     def create(self) -> list[AntaTest.Input] | None:
         """Create the `AntaTest.Input` models for the `AntaTest`."""
 
-    def is_peer_available(self, peer: str, caller: str) -> bool:
+    def is_peer_available(self, peer: str, identity: str) -> bool:
         """Check if a peer is part of the fabric and is deployed."""
-        if peer not in self.structured_configs or not self.structured_configs[peer].is_deployed:
-            self.logger.debug(LogMessage.PEER_UNAVAILABLE, caller=caller, peer=peer)
+        if peer not in self.minimal_structured_configs or not self.minimal_structured_configs[peer].is_deployed:
+            self.logger_adapter.debug(LogMessage.PEER_UNAVAILABLE, identity=identity, peer=peer)
             return False
         return True
 
-    def get_interface_ip(self, peer: str, peer_interface: str, caller: str) -> str | None:
+    def get_interface_ip(self, peer: str, peer_interface: str, interface: str) -> str | None:
         """Get the IP address of a peer interface."""
-        if not self.is_peer_available(peer, caller=caller):
+        if not self.is_peer_available(peer, identity=interface):
             return None
-        for intf in self.structured_configs[peer].ethernet_interfaces:
+        for intf in self.minimal_structured_configs[peer].ethernet_interfaces:
             if intf.name == peer_interface:
                 return intf.ip_address
-        self.logger.debug(LogMessage.PEER_INTERFACE_NO_IP, caller=caller, peer=peer, peer_interface=peer_interface)
+        self.logger_adapter.debug(LogMessage.PEER_INTERFACE_NO_IP, interface=interface, peer=peer, peer_interface=peer_interface)
         return None
diff --git a/python-avd/pyavd/_anta/input_factories/connectivity.py b/python-avd/pyavd/_anta/input_factories/connectivity.py
index 619119d93a2..6183a22d867 100644
--- a/python-avd/pyavd/_anta/input_factories/connectivity.py
+++ b/python-avd/pyavd/_anta/input_factories/connectivity.py
@@ -35,26 +35,26 @@ def create(self) -> list[VerifyLLDPNeighbors.Input] | None:
         neighbors = []
         for intf in self.structured_config.ethernet_interfaces:
             if intf.validate_state is False or intf.validate_lldp is False:
-                self.logger.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, interface=intf.name)
                 continue
 
             if "." in intf.name:
-                self.logger.debug(LogMessage.INTERFACE_IS_SUBINTERFACE, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_IS_SUBINTERFACE, interface=intf.name)
                 continue
 
             if intf.shutdown or (intf.shutdown is None and self.structured_config.interface_defaults.ethernet.shutdown):
-                self.logger.debug(LogMessage.INTERFACE_SHUTDOWN, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_SHUTDOWN, interface=intf.name)
                 continue
 
             if not intf.peer or not intf.peer_interface:
-                self.logger.debug(LogMessage.INPUT_MISSING_FIELDS, caller=intf.name, fields="peer, peer_interface")
+                self.logger_adapter.debug(LogMessage.INPUT_MISSING_FIELDS, identity=intf.name, fields="peer, peer_interface")
                 continue
 
-            if not self.is_peer_available(intf.peer, caller=intf.name):
+            if not self.is_peer_available(intf.peer, identity=intf.name):
                 continue
 
             # LLDP neighbor is the FQDN when dns domain is set in EOS
-            fqdn = f"{intf.peer}.{dns_domain}" if (dns_domain := self.structured_configs[intf.peer].dns_domain) is not None else intf.peer
+            fqdn = f"{intf.peer}.{dns_domain}" if (dns_domain := self.minimal_structured_configs[intf.peer].dns_domain) is not None else intf.peer
 
             neighbors.append(
                 LLDPNeighbor(
@@ -85,13 +85,13 @@ def create(self) -> list[VerifyReachability.Input] | None:
         inputs: list[VerifyReachability.Input] = []
 
         # Get the P2P reachability inputs
-        with self.logger.context("Point-to-Point Links"):
+        with self.logger_adapter.context("P2P link"):
             p2p_inputs = self._get_p2p_inputs()
             if p2p_inputs.hosts:
                 inputs.append(p2p_inputs)
 
         # Get the BGP neighbor reachability inputs
-        with self.logger.context("BGP Neighbors"):
+        with self.logger_adapter.context("BGP neighbor"):
             bgp_inputs = self._get_bgp_inputs()
             if bgp_inputs.hosts:
                 inputs.append(bgp_inputs)
@@ -105,18 +105,18 @@ def _get_p2p_inputs(self) -> VerifyReachability.Input:
 
         for intf in self.structured_config.ethernet_interfaces:
             if intf.shutdown or (intf.shutdown is None and self.structured_config.interface_defaults.ethernet.shutdown):
-                self.logger.debug(LogMessage.INTERFACE_SHUTDOWN, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_SHUTDOWN, interface=intf.name)
                 continue
 
             if not intf.ip_address or not intf.peer or not intf.peer_interface:
-                self.logger.debug(LogMessage.INPUT_MISSING_FIELDS, caller=intf.name, fields="ip_address, peer, peer_interface")
+                self.logger_adapter.debug(LogMessage.INPUT_MISSING_FIELDS, identity=intf.name, fields="ip_address, peer, peer_interface")
                 continue
 
             if intf.ip_address == "dhcp":
-                self.logger.debug(LogMessage.INTERFACE_USING_DHCP, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_USING_DHCP, interface=intf.name)
                 continue
 
-            if (peer_interface_ip := self.get_interface_ip(intf.peer, intf.peer_interface, caller=intf.name)) is None:
+            if (peer_interface_ip := self.get_interface_ip(intf.peer, intf.peer_interface, intf.name)) is None:
                 continue
 
             hosts.append(
diff --git a/python-avd/pyavd/_anta/input_factories/interfaces.py b/python-avd/pyavd/_anta/input_factories/interfaces.py
index 39072b299da..6982ff1c1d7 100644
--- a/python-avd/pyavd/_anta/input_factories/interfaces.py
+++ b/python-avd/pyavd/_anta/input_factories/interfaces.py
@@ -42,7 +42,7 @@ def create(self) -> list[VerifyInterfacesStatus.Input] | None:
         # Add Ethernet interfaces, considering `validate_state` knob and interface defaults
         for intf in self.structured_config.ethernet_interfaces:
             if intf.validate_state is False:
-                self.logger.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, interface=intf.name)
                 continue
             status = "adminDown" if intf.shutdown or (intf.shutdown is None and self.structured_config.interface_defaults.ethernet.shutdown) else "up"
             interfaces.append(InterfaceState(name=intf.name, status=status))
@@ -50,7 +50,7 @@ def create(self) -> list[VerifyInterfacesStatus.Input] | None:
         # Add Port-Channel interfaces, considering `validate_state` knob
         for intf in self.structured_config.port_channel_interfaces:
             if intf.validate_state is False:
-                self.logger.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, caller=intf.name)
+                self.logger_adapter.debug(LogMessage.INTERFACE_VALIDATION_DISABLED, interface=intf.name)
                 continue
             interfaces.append(InterfaceState(name=intf.name, status="adminDown" if intf.shutdown else "up"))
 
diff --git a/python-avd/pyavd/_anta/input_factories/security.py b/python-avd/pyavd/_anta/input_factories/security.py
index acaac8039f0..7b76120050f 100644
--- a/python-avd/pyavd/_anta/input_factories/security.py
+++ b/python-avd/pyavd/_anta/input_factories/security.py
@@ -48,7 +48,7 @@ def create(self) -> list[VerifySpecificIPSecConn.Input] | None:
         for path_group in self.structured_config.router_path_selection.path_groups:
             # Check if the path group has static peers
             if not path_group.static_peers:
-                self.logger.debug(LogMessage.STUN_NO_STATIC_PEERS, caller=path_group.name)
+                self.logger_adapter.debug(LogMessage.PATH_GROUP_NO_STATIC_PEERS, path_group=path_group.name)
                 continue
 
             # Add static peers to the list of IP security connections
diff --git a/python-avd/pyavd/_anta/input_factories/stun.py b/python-avd/pyavd/_anta/input_factories/stun.py
index 2685122c82b..88365e81836 100644
--- a/python-avd/pyavd/_anta/input_factories/stun.py
+++ b/python-avd/pyavd/_anta/input_factories/stun.py
@@ -34,7 +34,7 @@ def create(self) -> list[VerifyStunClientTranslation.Input] | None:
             # Check if the path group has local interfaces with STUN server profiles
             stun_interfaces = [local_interface.name for local_interface in path_group.local_interfaces if local_interface.stun.server_profiles]
             if not stun_interfaces:
-                self.logger.debug(LogMessage.STUN_NO_CLIENT_INTERFACE, caller=path_group.name)
+                self.logger_adapter.debug(LogMessage.PATH_GROUP_NO_STUN_INTERFACE, path_group=path_group.name)
                 continue
 
             for interface in stun_interfaces:
@@ -43,10 +43,10 @@ def create(self) -> list[VerifyStunClientTranslation.Input] | None:
                     self.structured_config.ethernet_interfaces[interface].ip_address if interface in self.structured_config.ethernet_interfaces else None
                 )
                 if ip_address is None:
-                    self.logger.debug(LogMessage.INTERFACE_NO_IP, caller=interface)
+                    self.logger_adapter.debug(LogMessage.INTERFACE_NO_IP, interface=interface)
                     continue
                 if ip_address == "dhcp":
-                    self.logger.debug(LogMessage.INTERFACE_USING_DHCP, caller=interface)
+                    self.logger_adapter.debug(LogMessage.INTERFACE_USING_DHCP, interface=interface)
                     continue
 
                 source_address = ip_interface(ip_address).ip
diff --git a/python-avd/pyavd/_anta/logs.py b/python-avd/pyavd/_anta/logs.py
index de47f8ddbc0..c8908ba7f54 100644
--- a/python-avd/pyavd/_anta/logs.py
+++ b/python-avd/pyavd/_anta/logs.py
@@ -13,7 +13,6 @@
 
 if TYPE_CHECKING:
     from collections.abc import Generator
-    from logging import Logger
 
 
 class TestLoggerAdapter(LoggerAdapter):
@@ -26,13 +25,12 @@ class TestLoggerAdapter(LoggerAdapter):
     extra = {
         "device": "<device_name>",
         "test": "<test_name>",
-        "context": "<test_context>",
+        "context": "<test_context>",  # Optional
     }
     ```
-    The `create` method can be used as an alternative constructor to create a new TestLoggerAdapter instance using the proper `extra` dict shown above.
 
     When logging a message, the logger will format the `LogMessage` Enum message using the kwargs passed to the logger,
-    and prepend the message with the device and test names, and optionally the context.
+    and prepend the message with the device and test names, and optionally the context: `<device> test context message`.
     """
 
     def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
@@ -43,12 +41,11 @@ def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
         # Extract the device, test, and context from extra
         device = self.extra["device"]
         test = self.extra["test"]
-        context = self.extra.get("context", "")
+        context = self.extra.get("context")
 
-        # Format: <device> [test] (context): message
-        prefix = f"<{device}> [{test}]"
+        prefix = f"<{device}> {test}"
         if context:
-            prefix += f" ({context})"
+            prefix += f" {context}"
 
         # Format the LogMessage using the provided kwargs and extract the fields name from the message string
         fields = [field_name for _, field_name, _, _ in string.Formatter().parse(msg.value) if field_name is not None]
@@ -58,7 +55,7 @@ def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
         for field in fields:
             kwargs.pop(field, None)
 
-        return f"{prefix}: {msg}", kwargs
+        return f"{prefix} {msg}", kwargs
 
     @contextmanager
     def context(self, context: str) -> Generator[TestLoggerAdapter, None, None]:
@@ -70,12 +67,6 @@ def context(self, context: str) -> Generator[TestLoggerAdapter, None, None]:
         finally:
             self.extra = original_extra
 
-    @staticmethod
-    def create(device: str, test: str, logger: Logger, context: str | None = None) -> TestLoggerAdapter:
-        """Construct a new TestLoggerAdapter instance."""
-        extra = {"device": device, "test": test, "context": context or ""}
-        return TestLoggerAdapter(logger, extra)
-
 
 class LogMessage(Enum):
     """
@@ -85,36 +76,21 @@ class LogMessage(Enum):
     """
 
     # Peer-related messages
-    PEER_UNAVAILABLE = "{caller} skipped - peer {peer} not in fabric or not deployed"
-    PEER_OUTSIDE_BOUNDARY = "{caller} skipped - peer {peer} not in {boundary} boundary"
-    PEER_INTERFACE_NO_IP = "{caller} skipped - peer {peer} interface {peer_interface} has no IP"
+    PEER_UNAVAILABLE = "{identity} skipped - Peer {peer} not in fabric or not deployed"
+    PEER_INTERFACE_NO_IP = "{interface} skipped - Peer {peer} interface {peer_interface} has no IP"
 
     # Interface state messages
-    INTERFACE_SHUTDOWN = "{caller} skipped - interface is shutdown"
-    INTERFACE_USING_DHCP = "{caller} skipped - DHCP interface"
-    INTERFACE_IS_SUBINTERFACE = "{caller} skipped - subinterface"
-    INTERFACE_NOT_INBAND_MGMT = "{caller} skipped - not an inband management SVI"
-    INTERFACE_VALIDATION_DISABLED = "{caller} skipped - validate_state or validate_lldp disabled"
-    INTERFACE_NO_IP = "{caller} skipped - no IP address configured"
-
-    # IP-related messages
-    IPV6_UNSUPPORTED = "{caller} skipped - IPv6 not supported"
-    LOOPBACK0_NO_IP = "skipped - no Loopback0 IP"
-    VTEP_NO_IP = "skipped - no VTEP IP"
-
-    # Device role messages
-    DEVICE_NOT_VTEP = "skipped - device is not a VTEP or is a WAN router"
-    DEVICE_NOT_WAN_ROUTER = "skipped - device is not a WAN router"
-    DEVICE_NO_INBAND_MGMT = "skipped - no inband management SVI found"
-
-    # BGP-specific messages
-    BGP_AF_NOT_ACTIVATED = "{caller} - address families {capability} skipped"
-
-    # STUN-specific messages
-    STUN_NO_CLIENT_INTERFACE = "path group {caller} skipped - no STUN client interfaces found"
-    STUN_NO_STATIC_PEERS = "path group {caller} skipped - no static peers configured"
+    INTERFACE_SHUTDOWN = "{interface} skipped - Interface is shutdown"
+    INTERFACE_USING_DHCP = "{interface} skipped - DHCP interface"
+    INTERFACE_IS_SUBINTERFACE = "{interface} skipped - Subinterface"
+    INTERFACE_VALIDATION_DISABLED = "{interface} skipped - validate_state or validate_lldp disabled"
+    INTERFACE_NO_IP = "{interface} skipped - No IP address configured"
+
+    # WAN-specific messages
+    PATH_GROUP_NO_STUN_INTERFACE = "path group {path_group} skipped - No STUN client interfaces found"
+    PATH_GROUP_NO_STATIC_PEERS = "path group {path_group} skipped - No static peers configured"
 
     # Input generation messages
-    INPUT_NONE_FOUND = "skipped - no inputs available"
-    INPUT_NO_DATA_MODEL = "skipped - data model {caller} not found"
-    INPUT_MISSING_FIELDS = "{caller} skipped - missing required fields: {fields}"
+    INPUT_NONE_FOUND = "skipped - No inputs available"
+    INPUT_NO_DATA_MODELS = "skipped - Data models {data_models} not found"
+    INPUT_MISSING_FIELDS = "{identity} skipped - Missing required fields: {fields}"
diff --git a/python-avd/pyavd/_anta/models.py b/python-avd/pyavd/_anta/models.py
index bef8b43fa4f..afad7e6e04c 100644
--- a/python-avd/pyavd/_anta/models.py
+++ b/python-avd/pyavd/_anta/models.py
@@ -34,7 +34,7 @@ class DeviceTestContext:
 
     hostname: str
     structured_config: EosCliConfigGen
-    structured_configs: dict[str, MinimalStructuredConfig]
+    minimal_structured_configs: dict[str, MinimalStructuredConfig]
     input_factory_settings: InputFactorySettings
 
     @cached_property
@@ -56,7 +56,7 @@ def bgp_neighbors(self) -> list[BgpNeighbor]:
 
         # Skip VRF processing if disabled
         if not self.input_factory_settings.allow_bgp_vrfs:
-            LOGGER.debug("<%s>: skipped BGP VRF peers - VRF processing disabled", self.hostname)
+            LOGGER.debug("<%s> Skipped BGP VRF peers - VRF processing disabled", self.hostname)
             return neighbors
 
         # Add VRF neighbors to the list
@@ -84,7 +84,7 @@ def _process_bgp_neighbor(
 
         # Skip neighbors that are shutdown
         if neighbor.shutdown is True:
-            LOGGER.debug("<%s>: skipped BGP peer %s - shutdown", self.hostname, identifier)
+            LOGGER.debug("<%s> Skipped BGP peer %s - Shutdown", self.hostname, identifier)
             return None
 
         # Skip neighbors in shutdown peer groups
@@ -93,22 +93,22 @@ def _process_bgp_neighbor(
             and neighbor.peer_group in self.structured_config.router_bgp.peer_groups
             and self.structured_config.router_bgp.peer_groups[neighbor.peer_group].shutdown is True
         ):
-            LOGGER.debug("<%s>: skipped BGP peer %s - peer group %s shutdown", self.hostname, identifier, neighbor.peer_group)
+            LOGGER.debug("<%s> Skipped BGP peer %s - Peer group %s shutdown", self.hostname, identifier, neighbor.peer_group)
             return None
 
         # When peer field is set, check if the peer device is in the fabric and deployed
         if (
             isinstance(neighbor, EosCliConfigGen.RouterBgp.NeighborsItem)
             and neighbor.peer
-            and (neighbor.peer not in self.structured_configs or not self.structured_configs[neighbor.peer].is_deployed)
+            and (neighbor.peer not in self.minimal_structured_configs or not self.minimal_structured_configs[neighbor.peer].is_deployed)
         ):
-            LOGGER.debug("<%s>: skipped BGP peer %s - peer not in fabric or not deployed", self.hostname, identifier)
+            LOGGER.debug("<%s> Skipped BGP peer %s - Peer not in fabric or not deployed", self.hostname, identifier)
             return None
 
         # TODO: IPv6 neighbors are not supported in ANTA yet
         ip_address = ip_interface(neighbor.ip_address).ip
         if isinstance(ip_address, IPv6Address):
-            LOGGER.debug("<%s>: skipped BGP peer %s - IPv6 not supported", self.hostname, identifier)
+            LOGGER.debug("<%s> Skipped BGP peer %s - IPv6 not supported", self.hostname, identifier)
             return None
 
         update_source = neighbor.update_source or (
diff --git a/python-avd/pyavd/_anta/utils.py b/python-avd/pyavd/_anta/utils.py
index 26289430f21..7f2a5533983 100644
--- a/python-avd/pyavd/_anta/utils.py
+++ b/python-avd/pyavd/_anta/utils.py
@@ -25,6 +25,6 @@ def dump_anta_catalog(hostname: str, catalog: AntaCatalog, catalog_dir: str) ->
     catalog_path = Path(catalog_dir) / f"{hostname}.json"
     catalog_dump = catalog.dump()
 
-    LOGGER.debug("<%s> dumping ANTA catalog at %s", hostname, catalog_path)
+    LOGGER.debug("<%s> Dumping ANTA catalog at %s", hostname, catalog_path)
     with catalog_path.open(mode="w", encoding="UTF-8") as stream:
         stream.write(catalog_dump.to_json())
diff --git a/python-avd/pyavd/get_device_test_catalog.py b/python-avd/pyavd/get_device_test_catalog.py
index c18f1a12358..2ed2660a8bb 100644
--- a/python-avd/pyavd/get_device_test_catalog.py
+++ b/python-avd/pyavd/get_device_test_catalog.py
@@ -63,10 +63,10 @@ def get_device_test_catalog(
     settings = settings or AvdCatalogGenerationSettings()
 
     start_time = perf_counter()
-    LOGGER.info("<%s>: generating catalog with settings: %s", hostname, settings.model_dump(mode="json"))
+    LOGGER.debug("<%s> Generating ANTA catalog with settings: %s", hostname, settings.model_dump(mode="json"))
 
     if settings.ignore_is_deployed is False and not structured_config.get("is_deployed", False):
-        LOGGER.debug("<%s>: device is not deployed, returning an empty catalog", hostname)
+        LOGGER.info("<%s> Device is not deployed, returning an empty catalog", hostname)
         return AntaCatalog()
 
     # Check for invalid test names across all filters
@@ -83,7 +83,7 @@ def get_device_test_catalog(
     # Remove any tests from run_tests that are in skip_tests
     if settings.run_tests and settings.skip_tests:
         settings.run_tests = [test for test in settings.run_tests if test not in settings.skip_tests]
-        LOGGER.debug("<%s>: cleaned up run_tests after removing skipped tests: %s", hostname, settings.run_tests)
+        LOGGER.debug("<%s> Cleaned up run_tests after removing skipped tests: %s", hostname, settings.run_tests)
 
     # Filter test specs based on skip_tests and run_tests
     filtered_test_specs = []
@@ -107,6 +107,6 @@ def get_device_test_catalog(
         dump_anta_catalog(hostname, catalog, settings.output_dir)
 
     stop_time = perf_counter()
-    LOGGER.debug("<%s>: generated catalog in %.4f seconds", hostname, stop_time - start_time)
+    LOGGER.debug("<%s> Generated ANTA catalog in %.4f seconds", hostname, stop_time - start_time)
 
     return catalog


Reviews:
user: login: joelbreton2
body: tested with Internal-CI and results were as expected 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:13:02Z
user: login: gmuloc
body: Should the PR be marked as a breaking change (but fix for preview so its ok)? To indicate to people that they might need to adjust thing
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:05:43Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:23:07Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:23:38Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:24:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:28:01Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:01:23Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:04:31Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:05:06Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T17:36:06Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:04:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:05:05Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:06:32Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:07:19Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:08Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:15Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:20Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:32Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:50Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:08:56Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:09:02Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:09:08Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:10:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T19:11:00Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:38:42Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:39:54Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:41:51Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:47:28Z
user: login: gmuloc
body: Ok with the current status - let's see the feedback once it is in devel
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T13:14:48Z

Review comments:
diff_hunk: @@ -213,38 +215,29 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
 
 def run_anta(devices: list[str]) -> ResultManager:
     """Run ANTA."""
-    joined_devices = ", ".join(devices)
-    process_name = current_process().name
+    # Generate a unique ID for this child process run
+    unique_id = f"anta-run-{str(uuid4())[:8]}"
 
-    # Setup process logging and get warning tracker
-    has_warnings_ref, anta_log_filename = setup_process_logging()
+    # Setup child process logging
+    setup_child_process_logging(LOG_QUEUE, display.verbosity, unique_id)
 
     # Build the objects required to run ANTA
     result_manager, inventory, catalog = build_anta_runner_objects(devices)
     tags = set(get(PLUGIN_ARGS, "runner.tags", default=[])) or None
+    dry_run = get(PLUGIN_ARGS, "runner.dry_run")
+    run_mode = "dry-run" if dry_run else "run"
 
     # Run ANTA
-    LOGGER.info("running ANTA in process %s for devices: %s", process_name, joined_devices)
-    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=get(PLUGIN_ARGS, "runner.dry_run")))
-
-    # Check if warnings/errors occurred in ANTA and notify via main logger
-    # ANTA errors are typically handled properly within ANTA by marking impacted
-    # tests as errors or failures, so we don't need to fail the task here
-    if has_warnings_ref[0]:
-        base_message = f"ANTA warnings/errors detected that could impact test results for devices {joined_devices}. "
-        if anta_log_filename:
-            base_message += f"See '{anta_log_filename}' for details."
-            LOGGER.warning(base_message)
-        else:
-            base_message += "No log directory was provided to save ANTA logs."
-            LOGGER.warning(base_message)
-
-    LOGGER.info("ANTA process %s completed for devices: %s", process_name, joined_devices)
+    joined_devices = ", ".join(devices)
+    LOGGER.info("Starting ANTA %s for devices: %s", run_mode, joined_devices)
+    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=dry_run))
+
+    LOGGER.info("ANTA %s completed for devices: %s", run_mode, joined_devices)
     return result_manager
 
 
-def build_reports(batch_results: Iterator[ResultManager], report_settings: dict) -> ResultManager:
-    """Build the ANTA reports from the batch results and return the aggregated results."""
+def build_reports(batch_results: Iterator[ResultManager], report_settings: dict[str, Any]) -> dict[str, Any]:
+    """Build the ANTA reports from the batch results and return a summary dictionary containing ANTA test statistics."""
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: gmuloc
body: can we add docstrings with Args / Return for these?
created_at: 2025-05-15T12:56:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -265,57 +258,93 @@ def build_reports(batch_results: Iterator[ResultManager], report_settings: dict)
 
     # TODO: Consider using multiprocessing to generate reports in parallel
     if csv_output_path:
-        LOGGER.info("generating CSV report at %s", csv_output_path)
+        LOGGER.info("Generating CSV report at %s", csv_output_path)
         path = Path(csv_output_path)
         report_csv = ReportCsv()
         report_csv.generate(result_manager, path)
 
     if md_output_path:
-        LOGGER.info("generating Markdown report at %s", md_output_path)
+        LOGGER.info("Generating Markdown report at %s", md_output_path)
         path = Path(md_output_path)
         md_report = MDReportGenerator()
         md_report.generate(result_manager, path)
 
     if json_output_path:
-        LOGGER.info("generating JSON report at %s", json_output_path)
+        LOGGER.info("Generating JSON report at %s", json_output_path)
         path = Path(json_output_path)
         with path.open("w", encoding="UTF-8") as file:
             file.write(result_manager.json)
 
-    return result_manager
-
-
-def update_ansible_result(result: dict[str, Any], result_manager: ResultManager, *, strict_mode: bool = False) -> dict[str, Any]:
-    """
-    Update the Ansible result dictionary from the aggregated ANTA results.
-
-    If a test failed or errored, the Ansible task is set to 'failed' if `strict_mode`
-    is True or 'changed' if False.
-    """
-    anta_summary = {
+    # Build a summary with ANTA test stats
+    test_summary = {
         "total_tests": result_manager.get_total_results(),
-        "passed": result_manager.get_total_results({"success"}),
-        "failed": result_manager.get_total_results({"failure"}),
-        "error": result_manager.get_total_results({"error"}),
-        "skipped": result_manager.get_total_results({"skipped"}),
-        "unset": result_manager.get_total_results({"unset"}),
-        "devices_with_failures": [],
-        "devices_with_errors": [],
+        "tests_passed": result_manager.get_total_results({"success"}),
+        "tests_failed": result_manager.get_total_results({"failure"}),
+        "tests_error": result_manager.get_total_results({"error"}),
+        "tests_skipped": result_manager.get_total_results({"skipped"}),
+        "tests_unset": result_manager.get_total_results({"unset"}),
+        "devices_with_test_failures": [],
+        "devices_with_test_errors": [],
     }
     for device, stat in result_manager.device_stats.items():
         if stat.tests_failure_count:
-            anta_summary["devices_with_failures"].append(device)
+            test_summary["devices_with_test_failures"].append(device)
         if stat.tests_error_count:
-            anta_summary["devices_with_errors"].append(device)
+            test_summary["devices_with_test_errors"].append(device)
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: gmuloc
body: we could probably build this into ANTA result_manager
created_at: 2025-05-15T12:58:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+import logging
+
+from ansible.utils.display import Display
+
+
+class AntaWorkflowFilter(logging.Filter):
+    """
+    ANTA workflow logging filter.
+
+    Injects a unique ID into log records for context tracking.
+    """
+
+    def __init__(self, unique_id: str) -> None:
+        """
+        Initialize the filter.
+
+        Args:
+          unique_id: Identifier for the current context (e.g., 'anta-workflow'
+                     or 'anta-run-xxxxxxxx'). Added to all records.
+        """
+        super().__init__()
+        self.unique_id = unique_id
+
+    def filter(self, record: logging.LogRecord) -> bool:
+        """
+        Add `unique_id` to the record.
+
+        Args:
+          record: The log record to be processed.
+
+        Returns:
+          bool: Always True.
+        """
+        record.unique_id = self.unique_id
+        return True
+
+
+class AntaWorkflowHandler(logging.Handler):
+    """
+    ANTA workflow logging handler.
+
+    Route log records to Ansible Display based on level and track errors in a
+    boolean list.
+    """
+
+    def __init__(self, has_errors_ref: list[bool], display: Display | None = None) -> None:
path: ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
user: login: gmuloc
body: display is a singleton so you don't need to give it to the __init__ you can just get it (yeah .. the other code may not be updated yet but lets not make the same mistakes again)
created_at: 2025-05-15T13:09:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+import logging
+
+from ansible.utils.display import Display
+
+
+class AntaWorkflowFilter(logging.Filter):
+    """
+    ANTA workflow logging filter.
+
+    Injects a unique ID into log records for context tracking.
+    """
+
+    def __init__(self, unique_id: str) -> None:
+        """
+        Initialize the filter.
+
+        Args:
+          unique_id: Identifier for the current context (e.g., 'anta-workflow'
+                     or 'anta-run-xxxxxxxx'). Added to all records.
+        """
+        super().__init__()
+        self.unique_id = unique_id
+
+    def filter(self, record: logging.LogRecord) -> bool:
+        """
+        Add `unique_id` to the record.
+
+        Args:
+          record: The log record to be processed.
+
+        Returns:
+          bool: Always True.
+        """
+        record.unique_id = self.unique_id
+        return True
+
+
+class AntaWorkflowHandler(logging.Handler):
+    """
+    ANTA workflow logging handler.
+
+    Route log records to Ansible Display based on level and track errors in a
+    boolean list.
+    """
+
+    def __init__(self, has_errors_ref: list[bool], display: Display | None = None) -> None:
+        """
+        Initialize the handler.
+
+        Args:
+          has_errors_ref: Mutable boolean list to track errors.
+                          It's expected to be a list with one boolean element.
+          display: Optional Ansible Display instance. Retrieves singleton if None.
+        """
+        super().__init__()
+        self.display = display or Display()
+        self.has_errors_ref = has_errors_ref
+
+    def emit(self, record: logging.LogRecord) -> None:
+        """
+        Process a log record.
+
+        Formats the message (including unique_id) and sends it to the appropriate
+        Ansible Display method. Also update the error boolean list when handling error
+        logs and above.
+
+        Args:
+          record: The log record to be processed.
+        """
+        # Get the unique_id injected by the AntaLoggingFilter
+        unique_id = getattr(record, "unique_id", "unknown")
+
+        # Format the message including unique_id
+        base_message = self.format(record)
+        final_message = f"[{unique_id}] {base_message}"
+
+        if record.levelno >= logging.ERROR:
+            self.display.error(final_message, wrap_text=False)
+            self.has_errors_ref[0] = True
+        elif record.levelno == logging.WARNING:
+            self.display.warning(final_message)
path: ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
user: login: gmuloc
body: so this is correct but lets take another round at reviewing the logs coming from ANTA to decide what level we want them to be in AVD I think
created_at: 2025-05-15T13:11:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,26 +179,11 @@ anta_runner_batch_size: 5
 
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
-
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: I think the readme should state somewhere that the task will fail if there is any error
created_at: 2025-05-15T13:12:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: ```suggestion
The `anta_runner` role uses Ansible standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
```
created_at: 2025-05-15T13:16:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: httpx as WARNING?
created_at: 2025-05-15T13:17:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: this does not make sense
created_at: 2025-05-15T13:17:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: ```suggestion
    Running `ansible-playbook` with `-vvv` is particularly helpful to troubleshoot why some specific AVD-generated tests are missing from a device final catalog.  PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
```
created_at: 2025-05-15T13:18:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
+
+### Understanding Log Output
+
+`anta_runner` processes devices in parallel batches for improved performance. To help correlate log messages to specific operations or device batches, each log entry is prefixed with a unique identifier:
+
+- `anta-workflow`: Indicates logs from the main Ansible action plugin process.
+- `anta-run-xxxxxxxx`: Indicates logs from a specific ANTA execution run handling a batch of devices.
+
+These prefixes are essential when debugging issues related to a particular device or test batch.
+
+### Managing Log Files
+
+Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially with high verbosity, consider configuring `log_path` in your `ansible.cfg` file. This redirects the primary output stream to a specified log file, which is very useful for troubleshooting. For more details, refer to the [Ansible Logging Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: ```suggestion
Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially when using high verbosity, consider configuring `log_path` in your `ansible.cfg` file to redirect the primary output stream to the specified log file. For more details, refer to the [Ansible Logging Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
```
created_at: 2025-05-15T13:20:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
+
+### Understanding Log Output
+
+`anta_runner` processes devices in parallel batches for improved performance. To help correlate log messages to specific operations or device batches, each log entry is prefixed with a unique identifier:
+
+- `anta-workflow`: Indicates logs from the main Ansible action plugin process.
+- `anta-run-xxxxxxxx`: Indicates logs from a specific ANTA execution run handling a batch of devices.
+
+These prefixes are essential when debugging issues related to a particular device or test batch.
+
+### Managing Log Files
+
+Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially with high verbosity, consider configuring `log_path` in your `ansible.cfg` file. This redirects the primary output stream to a specified log file, which is very useful for troubleshooting. For more details, refer to the [Ansible Logging Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
+
+### Task Failure Conditions
+
+The `anta_runner` role will mark the Ansible task as failed if any of the following conditions occur:
+
+- **No tests executed:** If no tests were run against any device (and it's not a `dry_run`)
+- **Execution errors:** If errors arise during the role's execution, originating from Ansible, PyAVD, or ANTA
+- **Test failures or errors:** If any ANTA test reports a failure or error status.
+
+Understanding these conditions can help you quickly identify the cause of a failed playbook run.
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: This needs to be higher up in the documentation and not in the troubleshooting section
created_at: 2025-05-15T13:23:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,9 +22,6 @@
     from pyavd.api._anta import InputFactorySettings, MinimalStructuredConfig, TestSpec
 
 
-LOGGER = getLogger(__name__)
path: python-avd/pyavd/_anta/factories.py
user: login: gmuloc
body: Maye worth having some pyavd logs at debug level stating we have creeated catalog for a given device?
created_at: 2025-05-15T13:38:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,8 +59,10 @@ def create_catalog(
     return AntaCatalog(tests=tests)
 
 
-def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> list[AntaTestDefinition] | None:
+def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext) -> list[AntaTestDefinition] | None:
     """Create the AntaTestDefinition's from this TestSpec instance."""
+    logger = TestLoggerAdapter(logger=getLogger(__name__), extra={"device": device_context.hostname, "test": test_spec.test_class.name})
path: python-avd/pyavd/_anta/factories.py
user: login: gmuloc
body: we could rename this logger `logger_adapter`
and maybe add some method on it like `log_input_no_data_model`, `log_input_rendering`... this would centralized the complexisty in the adapter and remove the need to get the LogMessage.INPUT_NO_DATA_MODEL in here.

what do you think?
created_at: 2025-05-15T13:43:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,7 +87,7 @@ def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestConte
     # Create the AntaTest.Input instance(s) from the input factory if available
     if test_spec.input_factory is not None:
         logger.debug(LogMessage.INPUT_RENDERING, caller="input factory")
-        factory = test_spec.input_factory(device_context, logger)  # pylint: disable=not-callable
+        factory = test_spec.input_factory(device_context, test_spec.test_class.name)  # pylint: disable=not-callable
path: python-avd/pyavd/_anta/factories.py
user: login: gmuloc
body: I have not look at the code but could we not have all test with an input_factory and the one with input_dictionary are just using a common "dictionary_input_factory"  that is doing line 73 to 85? This way it would mean only one "way to interact with the tests rather than two?
created_at: 2025-05-15T13:47:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,7 +87,7 @@ def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestConte
     # Create the AntaTest.Input instance(s) from the input factory if available
     if test_spec.input_factory is not None:
         logger.debug(LogMessage.INPUT_RENDERING, caller="input factory")
-        factory = test_spec.input_factory(device_context, logger)  # pylint: disable=not-callable
+        factory = test_spec.input_factory(device_context, test_spec.test_class.name)  # pylint: disable=not-callable
path: python-avd/pyavd/_anta/factories.py
user: login: gmuloc
body: also the pylint ignore seems surprising will take a look
created_at: 2025-05-15T13:48:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +34,14 @@ class AntaTestInputFactory(ABC):
         Custom logger used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
         self.structured_configs = device_context.structured_configs
path: python-avd/pyavd/_anta/input_factories/_base_classes.py
user: login: gmuloc
body: first time reviwing but I think I need to understand what the minimal structured config is and maybe the variable name should mention minimal for ease of understaniding later in the flow. Also can these min structured config be a single frozen object that is shared between all factories?
created_at: 2025-05-15T13:55:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +34,14 @@ class AntaTestInputFactory(ABC):
         Custom logger used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
         self.structured_configs = device_context.structured_configs
-        self.logger = logger
+
+        # Create the logger adapter for the test input factory
+        self.logger = TestLoggerAdapter(logger=getLogger(self.__module__), extra={"device": self.device.hostname, "test": test_name})
path: python-avd/pyavd/_anta/input_factories/_base_classes.py
user: login: gmuloc
body: below we should say what `caller` is supposed to be as an argument in docstrings
created_at: 2025-05-15T13:57:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,14 +39,13 @@ def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
         kwargs["extra"] = self.extra
 
         # Extract the device, test, and context from extra
-        device = self.extra["device"]
-        test = self.extra["test"]
-        context = self.extra.get("context", "")
+        device = self.extra.get("device", "-")
+        test = self.extra.get("test", "-")
path: python-avd/pyavd/_anta/logs.py
user: login: gmuloc
body: do we need the `-` ? won't this render logs with `<-> - ` ?
created_at: 2025-05-15T14:03:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -265,57 +258,93 @@ def build_reports(batch_results: Iterator[ResultManager], report_settings: dict)
 
     # TODO: Consider using multiprocessing to generate reports in parallel
     if csv_output_path:
-        LOGGER.info("generating CSV report at %s", csv_output_path)
+        LOGGER.info("Generating CSV report at %s", csv_output_path)
         path = Path(csv_output_path)
         report_csv = ReportCsv()
         report_csv.generate(result_manager, path)
 
     if md_output_path:
-        LOGGER.info("generating Markdown report at %s", md_output_path)
+        LOGGER.info("Generating Markdown report at %s", md_output_path)
         path = Path(md_output_path)
         md_report = MDReportGenerator()
         md_report.generate(result_manager, path)
 
     if json_output_path:
-        LOGGER.info("generating JSON report at %s", json_output_path)
+        LOGGER.info("Generating JSON report at %s", json_output_path)
         path = Path(json_output_path)
         with path.open("w", encoding="UTF-8") as file:
             file.write(result_manager.json)
 
-    return result_manager
-
-
-def update_ansible_result(result: dict[str, Any], result_manager: ResultManager, *, strict_mode: bool = False) -> dict[str, Any]:
-    """
-    Update the Ansible result dictionary from the aggregated ANTA results.
-
-    If a test failed or errored, the Ansible task is set to 'failed' if `strict_mode`
-    is True or 'changed' if False.
-    """
-    anta_summary = {
+    # Build a summary with ANTA test stats
+    test_summary = {
         "total_tests": result_manager.get_total_results(),
-        "passed": result_manager.get_total_results({"success"}),
-        "failed": result_manager.get_total_results({"failure"}),
-        "error": result_manager.get_total_results({"error"}),
-        "skipped": result_manager.get_total_results({"skipped"}),
-        "unset": result_manager.get_total_results({"unset"}),
-        "devices_with_failures": [],
-        "devices_with_errors": [],
+        "tests_passed": result_manager.get_total_results({"success"}),
+        "tests_failed": result_manager.get_total_results({"failure"}),
+        "tests_error": result_manager.get_total_results({"error"}),
+        "tests_skipped": result_manager.get_total_results({"skipped"}),
+        "tests_unset": result_manager.get_total_results({"unset"}),
+        "devices_with_test_failures": [],
+        "devices_with_test_errors": [],
     }
     for device, stat in result_manager.device_stats.items():
         if stat.tests_failure_count:
-            anta_summary["devices_with_failures"].append(device)
+            test_summary["devices_with_test_failures"].append(device)
         if stat.tests_error_count:
-            anta_summary["devices_with_errors"].append(device)
+            test_summary["devices_with_test_errors"].append(device)
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: gmuloc
body: discussed on the call - we will see if it is used in ANTA before adding it.
created_at: 2025-05-15T14:23:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,26 +179,11 @@ anta_runner_batch_size: 5
 
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
-
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: it does at the end but it should not be at the end :) 
created_at: 2025-05-15T14:23:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,26 +179,11 @@ anta_runner_batch_size: 5
 
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
-
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Also add a tip about ignore_errors if they want.
created_at: 2025-05-15T14:24:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: As discussed, will remove the `-v` descriptions.
created_at: 2025-05-15T14:28:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +34,14 @@ class AntaTestInputFactory(ABC):
         Custom logger used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
         self.structured_configs = device_context.structured_configs
path: python-avd/pyavd/_anta/input_factories/_base_classes.py
user: login: carl-baillargeon
body: Already frozen and global in the plugin.
created_at: 2025-05-15T15:01:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +34,14 @@ class AntaTestInputFactory(ABC):
         Custom logger used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
         self.structured_configs = device_context.structured_configs
-        self.logger = logger
+
+        # Create the logger adapter for the test input factory
+        self.logger = TestLoggerAdapter(logger=getLogger(self.__module__), extra={"device": self.device.hostname, "test": test_name})
path: python-avd/pyavd/_anta/input_factories/_base_classes.py
user: login: carl-baillargeon
body: Will rename to something more specific releated to the tests.
created_at: 2025-05-15T15:04:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,14 +39,13 @@ def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
         kwargs["extra"] = self.extra
 
         # Extract the device, test, and context from extra
-        device = self.extra["device"]
-        test = self.extra["test"]
-        context = self.extra.get("context", "")
+        device = self.extra.get("device", "-")
+        test = self.extra.get("test", "-")
path: python-avd/pyavd/_anta/logs.py
user: login: carl-baillargeon
body: Will remove, unrelated.
created_at: 2025-05-15T15:05:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -213,38 +215,29 @@ def run(self, tmp: Any = None, task_vars: dict | None = None) -> dict:
 
 def run_anta(devices: list[str]) -> ResultManager:
     """Run ANTA."""
-    joined_devices = ", ".join(devices)
-    process_name = current_process().name
+    # Generate a unique ID for this child process run
+    unique_id = f"anta-run-{str(uuid4())[:8]}"
 
-    # Setup process logging and get warning tracker
-    has_warnings_ref, anta_log_filename = setup_process_logging()
+    # Setup child process logging
+    setup_child_process_logging(LOG_QUEUE, display.verbosity, unique_id)
 
     # Build the objects required to run ANTA
     result_manager, inventory, catalog = build_anta_runner_objects(devices)
     tags = set(get(PLUGIN_ARGS, "runner.tags", default=[])) or None
+    dry_run = get(PLUGIN_ARGS, "runner.dry_run")
+    run_mode = "dry-run" if dry_run else "run"
 
     # Run ANTA
-    LOGGER.info("running ANTA in process %s for devices: %s", process_name, joined_devices)
-    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=get(PLUGIN_ARGS, "runner.dry_run")))
-
-    # Check if warnings/errors occurred in ANTA and notify via main logger
-    # ANTA errors are typically handled properly within ANTA by marking impacted
-    # tests as errors or failures, so we don't need to fail the task here
-    if has_warnings_ref[0]:
-        base_message = f"ANTA warnings/errors detected that could impact test results for devices {joined_devices}. "
-        if anta_log_filename:
-            base_message += f"See '{anta_log_filename}' for details."
-            LOGGER.warning(base_message)
-        else:
-            base_message += "No log directory was provided to save ANTA logs."
-            LOGGER.warning(base_message)
-
-    LOGGER.info("ANTA process %s completed for devices: %s", process_name, joined_devices)
+    joined_devices = ", ".join(devices)
+    LOGGER.info("Starting ANTA %s for devices: %s", run_mode, joined_devices)
+    run(anta_runner(result_manager, inventory, catalog, tags=tags, dry_run=dry_run))
+
+    LOGGER.info("ANTA %s completed for devices: %s", run_mode, joined_devices)
     return result_manager
 
 
-def build_reports(batch_results: Iterator[ResultManager], report_settings: dict) -> ResultManager:
-    """Build the ANTA reports from the batch results and return the aggregated results."""
+def build_reports(batch_results: Iterator[ResultManager], report_settings: dict[str, Any]) -> dict[str, Any]:
+    """Build the ANTA reports from the batch results and return a summary dictionary containing ANTA test statistics."""
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: carl-baillargeon
body: As discussed live, I will create another PR with improved docstrings everywhere before we go GA so you can review more easily.
created_at: 2025-05-15T17:36:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +34,14 @@ class AntaTestInputFactory(ABC):
         Custom logger used for the input factory.
     """
 
-    def __init__(self, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> None:
+    def __init__(self, device_context: DeviceTestContext, test_name: str) -> None:
         """Initialize the `AntaTestInputFactory`."""
         self.device = device_context
         self.structured_config = device_context.structured_config
         self.structured_configs = device_context.structured_configs
-        self.logger = logger
+
+        # Create the logger adapter for the test input factory
+        self.logger = TestLoggerAdapter(logger=getLogger(self.__module__), extra={"device": self.device.hostname, "test": test_name})
path: python-avd/pyavd/_anta/input_factories/_base_classes.py
user: login: carl-baillargeon
body: Fixed in latest commit.
created_at: 2025-05-15T19:04:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,14 +39,13 @@ def process(self, msg: LogMessage, kwargs: dict) -> tuple[str, dict]:
         kwargs["extra"] = self.extra
 
         # Extract the device, test, and context from extra
-        device = self.extra["device"]
-        test = self.extra["test"]
-        context = self.extra.get("context", "")
+        device = self.extra.get("device", "-")
+        test = self.extra.get("test", "-")
path: python-avd/pyavd/_anta/logs.py
user: login: carl-baillargeon
body: Removed in latest commit.
created_at: 2025-05-15T19:05:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,7 +87,7 @@ def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestConte
     # Create the AntaTest.Input instance(s) from the input factory if available
     if test_spec.input_factory is not None:
         logger.debug(LogMessage.INPUT_RENDERING, caller="input factory")
-        factory = test_spec.input_factory(device_context, logger)  # pylint: disable=not-callable
+        factory = test_spec.input_factory(device_context, test_spec.test_class.name)  # pylint: disable=not-callable
path: python-avd/pyavd/_anta/factories.py
user: login: carl-baillargeon
body: As discussed live, let's remove input_dict in a separate PR: https://github.com/aristanetworks/avd-internal/issues/261
created_at: 2025-05-15T19:06:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,8 +59,10 @@ def create_catalog(
     return AntaCatalog(tests=tests)
 
 
-def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext, logger: TestLoggerAdapter) -> list[AntaTestDefinition] | None:
+def create_test_definitions(test_spec: TestSpec, device_context: DeviceTestContext) -> list[AntaTestDefinition] | None:
     """Create the AntaTestDefinition's from this TestSpec instance."""
+    logger = TestLoggerAdapter(logger=getLogger(__name__), extra={"device": device_context.hostname, "test": test_spec.test_class.name})
path: python-avd/pyavd/_anta/factories.py
user: login: carl-baillargeon
body: Agreed. Renamed to `logger_adapter` and `self.logger_adapter` in factories.
created_at: 2025-05-15T19:07:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,9 +22,6 @@
     from pyavd.api._anta import InputFactorySettings, MinimalStructuredConfig, TestSpec
 
 
-LOGGER = getLogger(__name__)
path: python-avd/pyavd/_anta/factories.py
user: login: carl-baillargeon
body: Catalog generation logs are in the public function `pyavd.get_device_test_catalog`.
created_at: 2025-05-15T19:08:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
+
+### Understanding Log Output
+
+`anta_runner` processes devices in parallel batches for improved performance. To help correlate log messages to specific operations or device batches, each log entry is prefixed with a unique identifier:
+
+- `anta-workflow`: Indicates logs from the main Ansible action plugin process.
+- `anta-run-xxxxxxxx`: Indicates logs from a specific ANTA execution run handling a batch of devices.
+
+These prefixes are essential when debugging issues related to a particular device or test batch.
+
+### Managing Log Files
+
+Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially with high verbosity, consider configuring `log_path` in your `ansible.cfg` file. This redirects the primary output stream to a specified log file, which is very useful for troubleshooting. For more details, refer to the [Ansible Logging Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
+
+### Task Failure Conditions
+
+The `anta_runner` role will mark the Ansible task as failed if any of the following conditions occur:
+
+- **No tests executed:** If no tests were run against any device (and it's not a `dry_run`)
+- **Execution errors:** If errors arise during the role's execution, originating from Ansible, PyAVD, or ANTA
+- **Test failures or errors:** If any ANTA test reports a failure or error status.
+
+Understanding these conditions can help you quickly identify the cause of a failed playbook run.
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Moved in the basic usage at the beginning.
created_at: 2025-05-15T19:08:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
+
+### Understanding Log Output
+
+`anta_runner` processes devices in parallel batches for improved performance. To help correlate log messages to specific operations or device batches, each log entry is prefixed with a unique identifier:
+
+- `anta-workflow`: Indicates logs from the main Ansible action plugin process.
+- `anta-run-xxxxxxxx`: Indicates logs from a specific ANTA execution run handling a batch of devices.
+
+These prefixes are essential when debugging issues related to a particular device or test batch.
+
+### Managing Log Files
+
+Console logs adhere to standard Ansible logging configurations. For persistent storage and easier analysis, especially with high verbosity, consider configuring `log_path` in your `ansible.cfg` file. This redirects the primary output stream to a specified log file, which is very useful for troubleshooting. For more details, refer to the [Ansible Logging Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html).
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-15T19:08:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
+- `-vvvvv`: `DEBUG` level for all sources
+
+!!! tip
+    When troubleshooting why specific AVD-generated tests might be missing from a device's final catalog, running `ansible-playbook` with `-vvv` is particularly helpful. PyAVD will output detailed information about skipped tests and the reasons for their exclusion.
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-15T19:08:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
+- `-vv`: `INFO` level for all sources, including `httpx`
+- `-vvv`: `DEBUG` level for all sources except ANTA **and** its low-level libraries
+- `-vvvv`: `DEBUG` level for all sources except ANTA low-level libraries
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Removed the all `-v` details.
created_at: 2025-05-15T19:08:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
+
+- **Default (no `-v` flag):** `WARNING` level and above for all sources
+- `-v`: `INFO` level for all sources, except `httpx` logs (which remain at `WARNING` due to high chattiness)
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Removed.
created_at: 2025-05-15T19:08:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -386,6 +366,47 @@ Below is a list of ANTA tests generated by AVD. As of this writing, ANTA support
 - VerifyTransceiversTemperature
 - VerifyZeroTouch
 
+## Logging and Troubleshooting
+
+This section details how to control log verbosity, understand log output, and diagnose common issues for the `anta_runner` role.
+
+### Controlling Log Verbosity
+
+The `anta_runner` role utilizes Ansible's standard verbosity flags (`-v`, `-vv`, etc.) to control log output from its various components, including PyAVD, ANTA, underlying libraries, and the `anta_workflow` action plugin. Adjust the verbosity level using the `ansible-playbook` command:
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-15T19:09:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,26 +179,11 @@ anta_runner_batch_size: 5
 
 # Run ANTA in dry-run mode. This will generate the tests but not execute them.
 anta_runner_dry_run: false
-
-# When `true`, the plugin returns `failed: true` if any ANTA tests fail or error.
-# Otherwise, the plugin returns `changed: true` in such cases.
-anta_strict_mode: false
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: carl-baillargeon
body: Done.
created_at: 2025-05-15T19:09:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+import logging
+
+from ansible.utils.display import Display
+
+
+class AntaWorkflowFilter(logging.Filter):
+    """
+    ANTA workflow logging filter.
+
+    Injects a unique ID into log records for context tracking.
+    """
+
+    def __init__(self, unique_id: str) -> None:
+        """
+        Initialize the filter.
+
+        Args:
+          unique_id: Identifier for the current context (e.g., 'anta-workflow'
+                     or 'anta-run-xxxxxxxx'). Added to all records.
+        """
+        super().__init__()
+        self.unique_id = unique_id
+
+    def filter(self, record: logging.LogRecord) -> bool:
+        """
+        Add `unique_id` to the record.
+
+        Args:
+          record: The log record to be processed.
+
+        Returns:
+          bool: Always True.
+        """
+        record.unique_id = self.unique_id
+        return True
+
+
+class AntaWorkflowHandler(logging.Handler):
+    """
+    ANTA workflow logging handler.
+
+    Route log records to Ansible Display based on level and track errors in a
+    boolean list.
+    """
+
+    def __init__(self, has_errors_ref: list[bool], display: Display | None = None) -> None:
+        """
+        Initialize the handler.
+
+        Args:
+          has_errors_ref: Mutable boolean list to track errors.
+                          It's expected to be a list with one boolean element.
+          display: Optional Ansible Display instance. Retrieves singleton if None.
+        """
+        super().__init__()
+        self.display = display or Display()
+        self.has_errors_ref = has_errors_ref
+
+    def emit(self, record: logging.LogRecord) -> None:
+        """
+        Process a log record.
+
+        Formats the message (including unique_id) and sends it to the appropriate
+        Ansible Display method. Also update the error boolean list when handling error
+        logs and above.
+
+        Args:
+          record: The log record to be processed.
+        """
+        # Get the unique_id injected by the AntaLoggingFilter
+        unique_id = getattr(record, "unique_id", "unknown")
+
+        # Format the message including unique_id
+        base_message = self.format(record)
+        final_message = f"[{unique_id}] {base_message}"
+
+        if record.levelno >= logging.ERROR:
+            self.display.error(final_message, wrap_text=False)
+            self.has_errors_ref[0] = True
+        elif record.levelno == logging.WARNING:
+            self.display.warning(final_message)
path: ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
user: login: carl-baillargeon
body: Discussed live about the strategy. Done in latest commit, you can check the comments in `setup_root_logger`.
created_at: 2025-05-15T19:10:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+import logging
+
+from ansible.utils.display import Display
+
+
+class AntaWorkflowFilter(logging.Filter):
+    """
+    ANTA workflow logging filter.
+
+    Injects a unique ID into log records for context tracking.
+    """
+
+    def __init__(self, unique_id: str) -> None:
+        """
+        Initialize the filter.
+
+        Args:
+          unique_id: Identifier for the current context (e.g., 'anta-workflow'
+                     or 'anta-run-xxxxxxxx'). Added to all records.
+        """
+        super().__init__()
+        self.unique_id = unique_id
+
+    def filter(self, record: logging.LogRecord) -> bool:
+        """
+        Add `unique_id` to the record.
+
+        Args:
+          record: The log record to be processed.
+
+        Returns:
+          bool: Always True.
+        """
+        record.unique_id = self.unique_id
+        return True
+
+
+class AntaWorkflowHandler(logging.Handler):
+    """
+    ANTA workflow logging handler.
+
+    Route log records to Ansible Display based on level and track errors in a
+    boolean list.
+    """
+
+    def __init__(self, has_errors_ref: list[bool], display: Display | None = None) -> None:
path: ansible_collections/arista/avd/plugins/plugin_utils/utils/anta_workflow_logging.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-05-15T19:11:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -594,27 +594,25 @@ def setup_root_logger(unique_id: str, log_queue: Queue, verbosity: int) -> None:
     """
     root_logger = logging.getLogger()
 
-    # All loggers (pyavd, anta, ansible_collections.arista.avd) including low-level libraries (asyncio, httpcore, httpx) will be at DEBUG
+    # ANTA low-level libraries (asyncio, httpcore, httpx) are always at WARNING level except at full verbosity `-vvvvv`
+    for logger_name in ("asyncio", "httpcore", "httpx"):
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: gmuloc
body: do a var
created_at: 2025-05-21T12:38:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,15 @@ This example will generate a per-device test catalog based on the AVD structured
 
 The artifacts generated by the role, including results from ANTA, will be stored in the `anta` folder in the inventory directory.
 
+The `Run ANTA on EOS devices` task will fail if any of the following conditions occur:
+
+- **No tests executed:** If no tests were run against any device (and it's not a `dry_run`)
+- **Execution errors:** If errors arise during the role's execution, originating from Ansible, PyAVD, or ANTA
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: ```suggestion
- **Execution errors:** If errors arise during the role execution, originating from Ansible, PyAVD, or ANTA
```
created_at: 2025-05-21T12:39:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,15 @@ This example will generate a per-device test catalog based on the AVD structured
 
 The artifacts generated by the role, including results from ANTA, will be stored in the `anta` folder in the inventory directory.
 
+The `Run ANTA on EOS devices` task will fail if any of the following conditions occur:
+
+- **No tests executed:** If no tests were run against any device (and it's not a `dry_run`)
+- **Execution errors:** If errors arise during the role's execution, originating from Ansible, PyAVD, or ANTA
+- **Test failures or errors:** If any ANTA test reports a failure or error status.
+
+!!! tip
+    Standard Ansible error handling (e.g., `ignore_errors`) can be used if desired. For more details, refer to the [Ansible documentation](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html).
path: ansible_collections/arista/avd/roles/anta_runner/README.md
user: login: gmuloc
body: ```suggestion
    To continue running the playbook when `anta_runner` fails, use Ansible `ignore_errors`. For more details, refer to the [Ansible documentation](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_error_handling.html).
```
created_at: 2025-05-21T12:41:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -594,27 +594,25 @@ def setup_root_logger(unique_id: str, log_queue: Queue, verbosity: int) -> None:
     """
     root_logger = logging.getLogger()
 
-    # All loggers (pyavd, anta, ansible_collections.arista.avd) including low-level libraries (asyncio, httpcore, httpx) will be at DEBUG
+    # ANTA low-level libraries (asyncio, httpcore, httpx) are always at WARNING level except at full verbosity `-vvvvv`
+    for logger_name in ("asyncio", "httpcore", "httpx"):
path: ansible_collections/arista/avd/plugins/action/anta_workflow.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-05-21T12:47:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-29T12:59:53Z
body: Review docs on [Read the Docs](https://ansible-avd--5341.org.readthedocs.build/en/5341/)

To test this pull request:

```sh
# Create virtual environment for this testing below the current directory
python -m venv test-avd-pr-5341
# Activate the virtual environment
source test-avd-pr-5341/bin/activate
# Install all requirements including PyAVD
pip install "pyavd[ansible] @ git+https://github.com/carl-baillargeon/avd.git@anta/update_logging#subdirectory=python-avd" --force
# Point Ansible collections path to the Python virtual environment
export ANSIBLE_COLLECTIONS_PATH=$VIRTUAL_ENV/ansible_collections
# Install Ansible collection
ansible-galaxy collection install git+https://github.com/carl-baillargeon/avd.git#/ansible_collections/arista/avd/,anta/update_logging --force
# Optional: Install AVD examples
cd test-avd-pr-5341
ansible-playbook arista.avd.install_examples
```

author_association: NONE
user: login: gmuloc
created_at: 2025-04-30T12:45:20Z
body: I feel like this

```
    "anta_process_log_stats": {
        "anta-run-4defbe5b": {
            "error_count": 0,
            "warning_count": 6
        },
        "anta-run-91335b50": {
            "error_count": 0,
            "warning_count": 6
        },
        "anta-run-ee100190": {
            "error_count": 0,
            "warning_count": 6
       
```

is today the only way to know which anta run belonged to one run of the `anta_runner` but we should find a way to make this clearer

author_association: CONTRIBUTOR
user: login: carl-baillargeon
created_at: 2025-04-30T19:26:25Z
body: > I feel like this
> 
> ```
>     "anta_process_log_stats": {
>         "anta-run-4defbe5b": {
>             "error_count": 0,
>             "warning_count": 6
>         },
>         "anta-run-91335b50": {
>             "error_count": 0,
>             "warning_count": 6
>         },
>         "anta-run-ee100190": {
>             "error_count": 0,
>             "warning_count": 6
>        
> ```
> 
> is today the only way to know which anta run belonged to one run of the `anta_runner` but we should find a way to make this clearer

@gmuloc Added devices per run + timestamp to log files.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-21T06:00:22Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-21T12:02:17Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-21T13:19:07Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_avd&pullRequest=5341) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_avd&pullRequest=5341&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_avd&pullRequest=5341&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_avd&pullRequest=5341&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [87.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_avd&pullRequest=5341&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_avd&pullRequest=5341&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_avd&pullRequest=5341)


author_association: NONE