diff mbox series

[v3] host_dmesg_logger: dmesg diff on BuildStarted and BuildCompleted

Message ID 20250705175707.394405-1-gavrosc@yahoo.com
State New
Headers show
Series [v3] host_dmesg_logger: dmesg diff on BuildStarted and BuildCompleted | expand

Commit Message

Christos Gavros July 5, 2025, 5:57 p.m. UTC
This class is used to capture the host dmesg output
when BuildStarted and BuildCompleted. Filters are applied
to these messages.Then computes a diff and prints the result.
Fixes [YOCTO #15557]

CC: Yoann Congal <yoann.congal@smile.fr>
CC: Randy MacLeod <randy.macleod@windriver.com>
CC: Alexandre Belloni <alexandre.belloni@bootlin.com>
Signed-off-by: Christos Gavros <gavrosc@yahoo.com>
---
v2->v3
* remove message 'No differences in dmesg output'
* show message's log level
* capture messages from notice level and above
* include messages which contain keywords from list keywords_include_messages
* exclude messages which contain keywords from list keywords_exclude_messages
* the description is extended
---
 meta/classes-global/host_dmesg_logger.bbclass | 148 ++++++++++++++++++
 1 file changed, 148 insertions(+)
 create mode 100644 meta/classes-global/host_dmesg_logger.bbclass
diff mbox series

Patch

diff --git a/meta/classes-global/host_dmesg_logger.bbclass b/meta/classes-global/host_dmesg_logger.bbclass
new file mode 100644
index 0000000000..a7c8e04c63
--- /dev/null
+++ b/meta/classes-global/host_dmesg_logger.bbclass
@@ -0,0 +1,148 @@ 
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+
+# This class captures the host's dmesg output at the start and completion of a build.
+# It captures messages with log level "notice (5)" and above. Messages that contain
+# keywords from "keywords_include_messages" list are captured regardless of the log level.
+# It excludes messages that contain keywords from "keywords_exclude_messages" list regardless
+# of the log level.
+# It stores these outputs in a variable and upon build completion, computes a diff between
+# the two logs to detect any changes that occurred during the build process. It prints the diff
+# using bb.warn().
+# This class is intended to expose failures (e.g. segmentation faults) that 
+# are hidden or ignored by the host build system. However, it may also capture
+# irrelevant or unimportant messages.
+# The user needs to have privileges to run dmesg otherwise the operation is not allowed
+# and a warning is generated.
+# To enable dmesg logging, add the following to your local.conf:
+# INHERIT += "host_dmesg_logger"
+
+def append_log(msg, d):
+    """
+    Appends a log message to variable '_dmesg_log'
+    """
+    current_log = d.getVar('_dmesg_log') or ''
+    current_log += msg + '\n'
+    d.setVar('_dmesg_log', current_log)
+
+def capture_dmesg():
+    """
+    Returns the current output of dmesg command
+    """
+    import subprocess
+    import re
+
+    log_level_map = {
+        "emerg": 0,
+        "alert": 1,
+        "crit": 2,
+        "err": 3,
+        "warning": 4,
+        "notice": 5,
+        "info": 6,
+        "debug": 7
+    }
+
+    # keywords of messages which will be exclude in diff regardless of log level
+    keywords_include_messages = [
+        "over core_pipe_limit",
+    ]
+
+    # keywords of messages which will be exclude in diff regardless of log level
+    keywords_exclude_messages = [
+        "keywords_to_exclude",
+    ]
+
+    try:
+        result = subprocess.run(['dmesg', '-T', '-x'], capture_output=True, text=True, check=False)
+        if result.returncode != 0:
+            return f"Error running dmesg: {result.stderr.strip()}"
+        filtered_lines = []
+        for line in result.stdout.splitlines():
+            line_lower = line.lower()
+            match = re.search(r":(emerg|alert|crit|err|warning|notice|info|debug)", line_lower)
+            if match:
+                level_name = match.group(1).lower()
+            else:
+                level_name = None
+
+            # include messages which belong in the required level (notice -5)
+            if level_name is not None and level_name in log_level_map and log_level_map[level_name] <= 5:
+                log_level_ok = True
+            else:
+                log_level_ok = False
+
+            # include messages that contain keywords of "keywords_include_messages" list
+            include_message = False
+            for keyword in keywords_include_messages:
+                if keyword.lower() in line_lower:
+                    include_message = True
+                    break
+
+            # exclude messages that contain keywords of "keywords_exclude_messages" list
+            exclude_message = False
+            for keyword in keywords_exclude_messages:
+                if keyword.lower() in line_lower:
+                    exclude_message = True
+                    break
+
+            if (log_level_ok or include_message) and not exclude_message:
+                filtered_lines.append(line)
+
+        return "\n".join(filtered_lines) if filtered_lines else "No relevant dmesg messages."
+    except Exception as e:
+        return f"Exception running dmesg: {str(e)}"
+
+addhandler hostdmesglogger_eventhandler
+python hostdmesglogger_eventhandler() {
+    import difflib
+
+    start_marker = "=== BuildStarted dmesg ==="
+    end_marker = "=== BuildCompleted dmesg ==="
+    diff_marker = "=== dmesg diff ==="
+
+    # execute dmesg when BuildStarted event is fired
+    if isinstance(e, bb.event.BuildStarted):
+        dmesg_output = capture_dmesg()
+        if dmesg_output.startswith("Error running dmesg:") or dmesg_output.startswith("Exception running dmesg:"):
+            bb.warn(dmesg_output)
+        else:
+            append_log(start_marker, d)
+            append_log(dmesg_output, d)
+
+    # execute dmesg when BuildCompleted event is fired
+    if isinstance(e, bb.event.BuildCompleted):
+        dmesg_output = capture_dmesg()
+        if dmesg_output.startswith("Error running dmesg:") or dmesg_output.startswith("Exception running dmesg:"):
+            bb.warn(dmesg_output)
+        else:
+            append_log(end_marker, d)
+            append_log(dmesg_output, d)
+
+        content = d.getVar('_dmesg_log') or ''
+        if start_marker in content and end_marker in content:
+            start_dmesg = content.split(start_marker)[1].split(end_marker)[0]
+            end_dmesg = content.split(end_marker)[1]
+
+            start_lines = start_dmesg.strip().splitlines()
+            end_lines = end_dmesg.strip().splitlines()
+
+            # generating diff between BuildStarted and BuildCompleted dmesg outputs
+            diff = list(difflib.unified_diff(
+                start_lines, end_lines,
+                fromfile = 'dmesg_start',
+                tofile = 'dmesg_end',
+                lineterm = ''
+            ))
+
+            append_log(diff_marker, d)
+            if diff:
+                for line in diff:
+                    bb.warn(line)
+        else:
+            bb.warn("Could not find both dmesg sections for diff.")
+}
+hostdmesglogger_eventhandler[eventmask] = "bb.event.BuildStarted bb.event.BuildCompleted"