DPDK patches and discussions
 help / color / mirror / Atom feed
From: Luca Vizzarro <luca.vizzarro@arm.com>
To: dev@dpdk.org
Cc: Luca Vizzarro <luca.vizzarro@arm.com>,
	Paul Szczepanek <paul.szczepanek@arm.com>,
	Patrick Robb <probb@iol.unh.edu>
Subject: [PATCH 5/6] dts: make log files into artifacts
Date: Fri, 25 Jul 2025 16:11:40 +0100	[thread overview]
Message-ID: <20250725151503.87374-6-luca.vizzarro@arm.com> (raw)
In-Reply-To: <20250725151503.87374-1-luca.vizzarro@arm.com>

Make log files behave like artifacts as dictated by the Artifact class.
Implicitly, this will automatically place all the logs in a structured
manner.

Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
 dts/framework/logger.py   | 113 +++++++++++++++++++++-----------------
 dts/framework/runner.py   |   5 +-
 dts/framework/test_run.py |  17 ++----
 3 files changed, 69 insertions(+), 66 deletions(-)

diff --git a/dts/framework/logger.py b/dts/framework/logger.py
index f43b442bc9..230513c01e 100644
--- a/dts/framework/logger.py
+++ b/dts/framework/logger.py
@@ -2,43 +2,54 @@
 # Copyright(c) 2010-2014 Intel Corporation
 # Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
 # Copyright(c) 2022-2023 University of New Hampshire
+# Copyright(c) 2025 Arm Limited
 
 """DTS logger module.
 
 The module provides several additional features:
 
     * The storage of DTS execution stages,
-    * Logging to console, a human-readable log file and a machine-readable log file,
-    * Optional log files for specific stages.
+    * Logging to console, a human-readable log artifact and a machine-readable log artifact,
+    * Optional log artifacts for specific stages.
 """
 
 import logging
-from logging import FileHandler, StreamHandler
-from pathlib import Path
-from typing import ClassVar
+from logging import StreamHandler
+from typing import TYPE_CHECKING, ClassVar, NamedTuple
+
+if TYPE_CHECKING:
+    from framework.testbed_model.artifact import Artifact
 
 date_fmt = "%Y/%m/%d %H:%M:%S"
 stream_fmt = "%(asctime)s - %(stage)s - %(name)s - %(levelname)s - %(message)s"
 dts_root_logger_name = "dts"
 
 
+class ArtifactHandler(NamedTuple):
+    """A logger handler with an associated artifact."""
+
+    artifact: "Artifact"
+    handler: StreamHandler
+
+
 class DTSLogger(logging.Logger):
     """The DTS logger class.
 
     The class extends the :class:`~logging.Logger` class to add the DTS execution stage information
     to log records. The stage is common to all loggers, so it's stored in a class variable.
 
-    Any time we switch to a new stage, we have the ability to log to an additional log file along
-    with a supplementary log file with machine-readable format. These two log files are used until
-    a new stage switch occurs. This is useful mainly for logging per test suite.
+    Any time we switch to a new stage, we have the ability to log to an additional log artifact
+    along with a supplementary log artifact with machine-readable format. These two log artifacts
+    are used until a new stage switch occurs. This is useful mainly for logging per test suite.
     """
 
     _stage: ClassVar[str] = "pre_run"
-    _extra_file_handlers: list[FileHandler] = []
+    _root_artifact_handlers: list[ArtifactHandler] = []
+    _extra_artifact_handlers: list[ArtifactHandler] = []
 
     def __init__(self, *args, **kwargs):
-        """Extend the constructor with extra file handlers."""
-        self._extra_file_handlers = []
+        """Extend the constructor with extra artifact handlers."""
+        self._extra_artifact_handlers = []
         super().__init__(*args, **kwargs)
 
     def makeRecord(self, *args, **kwargs) -> logging.LogRecord:
@@ -56,7 +67,7 @@ def makeRecord(self, *args, **kwargs) -> logging.LogRecord:
         record.stage = DTSLogger._stage
         return record
 
-    def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
+    def add_dts_root_logger_handlers(self, verbose: bool) -> None:
         """Add logger handlers to the DTS root logger.
 
         This method should be called only on the DTS root logger.
@@ -65,18 +76,16 @@ def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
         Three handlers are added:
 
             * A console handler,
-            * A file handler,
-            * A supplementary file handler with machine-readable logs
+            * An artifact handler,
+            * A supplementary artifact handler with machine-readable logs
               containing more debug information.
 
-        All log messages will be logged to files. The log level of the console handler
+        All log messages will be logged to artifacts. The log level of the console handler
         is configurable with `verbose`.
 
         Args:
             verbose: If :data:`True`, log all messages to the console.
                 If :data:`False`, log to console with the :data:`logging.INFO` level.
-            output_dir: The directory where the log files will be located.
-                The names of the log files correspond to the name of the logger instance.
         """
         self.setLevel(1)
 
@@ -86,70 +95,76 @@ def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
             sh.setLevel(logging.INFO)
         self.addHandler(sh)
 
-        self._add_file_handlers(Path(output_dir, self.name))
+        self._root_artifact_handlers = self._add_artifact_handlers(self.name)
 
-    def set_stage(self, stage: str, log_file_path: Path | None = None) -> None:
-        """Set the DTS execution stage and optionally log to files.
+    def set_stage(self, stage: str, log_file_name: str | None = None) -> None:
+        """Set the DTS execution stage and optionally log to artifact files.
 
         Set the DTS execution stage of the DTSLog class and optionally add
-        file handlers to the instance if the log file name is provided.
+        artifact handlers to the instance if the log artifact file name is provided.
 
-        The file handlers log all messages. One is a regular human-readable log file and
-        the other one is a machine-readable log file with extra debug information.
+        The artifact handlers log all messages. One is a regular human-readable log artifact and
+        the other one is a machine-readable log artifact with extra debug information.
 
         Args:
             stage: The DTS stage to set.
-            log_file_path: An optional path of the log file to use. This should be a full path
-                (either relative or absolute) without suffix (which will be appended).
+            log_file_name: An optional name of the log artifact file to use. This should be without
+                suffix (which will be appended).
         """
-        self._remove_extra_file_handlers()
+        self._remove_extra_artifact_handlers()
 
         if DTSLogger._stage != stage:
             self.info(f"Moving from stage '{DTSLogger._stage}' to stage '{stage}'.")
             DTSLogger._stage = stage
 
-        if log_file_path:
-            self._extra_file_handlers.extend(self._add_file_handlers(log_file_path))
+        if log_file_name:
+            self._extra_artifact_handlers.extend(self._add_artifact_handlers(log_file_name))
 
-    def _add_file_handlers(self, log_file_path: Path) -> list[FileHandler]:
-        """Add file handlers to the DTS root logger.
+    def _add_artifact_handlers(self, log_file_name: str) -> list[ArtifactHandler]:
+        """Add artifact handlers to the DTS root logger.
 
-        Add two type of file handlers:
+        Add two type of artifact handlers:
 
-            * A regular file handler with suffix ".log",
-            * A machine-readable file handler with suffix ".verbose.log".
+            * A regular artifact handler with suffix ".log",
+            * A machine-readable artifact handler with suffix ".verbose.log".
               This format provides extensive information for debugging and detailed analysis.
 
         Args:
-            log_file_path: The full path to the log file without suffix.
+            log_file_name: The name of the artifact log file without suffix.
 
         Returns:
-            The newly created file handlers.
-
+            The newly created artifact handlers.
         """
-        fh = FileHandler(f"{log_file_path}.log")
-        fh.setFormatter(logging.Formatter(stream_fmt, date_fmt))
-        self.addHandler(fh)
+        from framework.testbed_model.artifact import Artifact
+
+        log_artifact = Artifact("local", f"{log_file_name}.log")
+        handler = StreamHandler(log_artifact.open("w"))
+        handler.setFormatter(logging.Formatter(stream_fmt, date_fmt))
+        self.addHandler(handler)
 
-        verbose_fh = FileHandler(f"{log_file_path}.verbose.log")
-        verbose_fh.setFormatter(
+        verbose_log_artifact = Artifact("local", f"{log_file_name}.verbose.log")
+        verbose_handler = StreamHandler(verbose_log_artifact.open("w"))
+        verbose_handler.setFormatter(
             logging.Formatter(
                 "%(asctime)s|%(stage)s|%(name)s|%(levelname)s|%(pathname)s|%(lineno)d|"
                 "%(funcName)s|%(process)d|%(thread)d|%(threadName)s|%(message)s",
                 datefmt=date_fmt,
             )
         )
-        self.addHandler(verbose_fh)
+        self.addHandler(verbose_handler)
 
-        return [fh, verbose_fh]
+        return [
+            ArtifactHandler(log_artifact, handler),
+            ArtifactHandler(verbose_log_artifact, verbose_handler),
+        ]
 
-    def _remove_extra_file_handlers(self) -> None:
-        """Remove any extra file handlers that have been added to the logger."""
-        if self._extra_file_handlers:
-            for extra_file_handler in self._extra_file_handlers:
-                self.removeHandler(extra_file_handler)
+    def _remove_extra_artifact_handlers(self) -> None:
+        """Remove any extra artifact handlers that have been added to the logger."""
+        if self._extra_artifact_handlers:
+            for extra_artifact_handler in self._extra_artifact_handlers:
+                self.removeHandler(extra_artifact_handler.handler)
 
-            self._extra_file_handlers = []
+            self._extra_artifact_handlers = []
 
 
 def get_dts_logger(name: str | None = None) -> DTSLogger:
diff --git a/dts/framework/runner.py b/dts/framework/runner.py
index 0a3d92b0c8..ae5ac014e2 100644
--- a/dts/framework/runner.py
+++ b/dts/framework/runner.py
@@ -9,7 +9,6 @@
 The module is responsible for preparing DTS and running the test run.
 """
 
-import os
 import sys
 import textwrap
 
@@ -45,9 +44,7 @@ def __init__(self):
             sys.exit(e.severity)
 
         self._logger = get_dts_logger()
-        if not os.path.exists(SETTINGS.output_dir):
-            os.makedirs(SETTINGS.output_dir)
-        self._logger.add_dts_root_logger_handlers(SETTINGS.verbose, SETTINGS.output_dir)
+        self._logger.add_dts_root_logger_handlers(SETTINGS.verbose)
 
         test_suites_result = ResultNode(label="test_suites")
         self._result = TestRunResult(test_suites=test_suites_result)
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index f70580f8fd..5609380c95 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -103,7 +103,6 @@
 from collections.abc import Iterable
 from dataclasses import dataclass
 from functools import cached_property
-from pathlib import Path
 from types import MethodType
 from typing import ClassVar, Protocol, Union
 
@@ -115,7 +114,6 @@
 from framework.settings import SETTINGS
 from framework.test_result import Result, ResultNode, TestRunResult
 from framework.test_suite import BaseConfig, TestCase, TestSuite
-from framework.testbed_model.artifact import Artifact
 from framework.testbed_model.capability import (
     Capability,
     get_supported_capabilities,
@@ -259,11 +257,11 @@ class State(Protocol):
     test_run: TestRun
     result: TestRunResult | ResultNode
 
-    def before(self):
+    def before(self) -> None:
         """Hook before the state is processed."""
-        self.logger.set_stage(self.logger_name, self.log_file_path)
+        self.logger.set_stage(self.logger_name, self.get_log_file_name())
 
-    def after(self):
+    def after(self) -> None:
         """Hook after the state is processed."""
         return
 
@@ -280,13 +278,6 @@ def get_log_file_name(self) -> str | None:
         """Name of the log file for this state."""
         return None
 
-    @property
-    def log_file_path(self) -> Path | None:
-        """Path to the log file for this state."""
-        if file_name := self.get_log_file_name():
-            return Path(SETTINGS.output_dir, file_name)
-        return None
-
     def next(self) -> Union["State", None]:
         """Next state."""
 
@@ -604,7 +595,7 @@ class TestCaseState(State):
 
     def get_log_file_name(self) -> str | None:
         """Get the log file name."""
-        return self.test_suite.name
+        return self.test_case.name
 
 
 @dataclass
-- 
2.43.0


  parent reply	other threads:[~2025-07-25 15:15 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-07-25 15:11 [PATCH 0/6] dts: add file management Luca Vizzarro
2025-07-25 15:11 ` [PATCH 1/6] dts: merge RemoteSession class Luca Vizzarro
2025-07-25 15:11 ` [PATCH 2/6] dts: add node retriever by identifier Luca Vizzarro
2025-07-25 15:11 ` [PATCH 3/6] dts: add current test suite and cases to context Luca Vizzarro
2025-07-25 15:11 ` [PATCH 4/6] dts: add artifact module Luca Vizzarro
2025-07-25 15:11 ` Luca Vizzarro [this message]
2025-07-25 15:11 ` [PATCH 6/6] dts: use artifacts in packet capture and softnic Luca Vizzarro

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250725151503.87374-6-luca.vizzarro@arm.com \
    --to=luca.vizzarro@arm.com \
    --cc=dev@dpdk.org \
    --cc=paul.szczepanek@arm.com \
    --cc=probb@iol.unh.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).