• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 Collabora Limited
4# Author: Guilherme Gallo <guilherme.gallo@collabora.com>
5#
6# SPDX-License-Identifier: MIT
7
8"""
9Some utilities to analyse logs, create gitlab sections and other quality of life
10improvements
11"""
12
13import logging
14import re
15import sys
16from dataclasses import dataclass, field
17from datetime import datetime, timedelta
18from typing import Optional, Union
19
20from lava.exceptions import MesaCITimeoutError
21from lava.utils.console_format import CONSOLE_LOG
22from lava.utils.gitlab_section import GitlabSection
23from lava.utils.lava_log_hints import LAVALogHints
24from lava.utils.log_section import (
25    DEFAULT_GITLAB_SECTION_TIMEOUTS,
26    FALLBACK_GITLAB_SECTION_TIMEOUT,
27    LOG_SECTIONS,
28    LogSectionType,
29)
30
31
32@dataclass
33class LogFollower:
34    current_section: Optional[GitlabSection] = None
35    timeout_durations: dict[LogSectionType, timedelta] = field(
36        default_factory=lambda: DEFAULT_GITLAB_SECTION_TIMEOUTS,
37    )
38    fallback_timeout: timedelta = FALLBACK_GITLAB_SECTION_TIMEOUT
39    _buffer: list[str] = field(default_factory=list, init=False)
40    log_hints: LAVALogHints = field(init=False)
41
42    def __post_init__(self):
43        section_is_created = bool(self.current_section)
44        section_has_started = bool(
45            self.current_section and self.current_section.has_started
46        )
47        self.log_hints = LAVALogHints(self)
48        assert (
49            section_is_created == section_has_started
50        ), "Can't follow logs beginning from uninitialized GitLab sections."
51
52    @property
53    def phase(self) -> LogSectionType:
54        return (
55            self.current_section.type
56            if self.current_section
57            else LogSectionType.UNKNOWN
58        )
59
60    def __enter__(self):
61        return self
62
63    def __exit__(self, exc_type, exc_val, exc_tb):
64        """Cleanup existing buffer if this object gets out from the context"""
65        self.clear_current_section()
66        last_lines = self.flush()
67        for line in last_lines:
68            print(line)
69
70    def watchdog(self):
71        if not self.current_section:
72            return
73
74        timeout_duration = self.timeout_durations.get(
75            self.current_section.type, self.fallback_timeout
76        )
77
78        if self.current_section.delta_time() > timeout_duration:
79            raise MesaCITimeoutError(
80                f"Gitlab Section {self.current_section} has timed out",
81                timeout_duration=timeout_duration,
82            )
83
84    def clear_current_section(self):
85        if self.current_section and not self.current_section.has_finished:
86            self._buffer.append(self.current_section.end())
87            self.current_section = None
88
89    def update_section(self, new_section: GitlabSection):
90        # Sections can have redundant regex to find them to mitigate LAVA
91        # interleaving kmsg and stderr/stdout issue.
92        if self.current_section and self.current_section.id == new_section.id:
93            return
94        self.clear_current_section()
95        self.current_section = new_section
96        self._buffer.append(new_section.start())
97
98    def manage_gl_sections(self, line):
99        if isinstance(line["msg"], list):
100            logging.debug("Ignoring messages as list. Kernel dumps.")
101            return
102
103        for log_section in LOG_SECTIONS:
104            if new_section := log_section.from_log_line_to_section(line):
105                self.update_section(new_section)
106
107    def detect_kernel_dump_line(self, line: dict[str, Union[str, list]]) -> bool:
108        # line["msg"] can be a list[str] when there is a kernel dump
109        if isinstance(line["msg"], list):
110            return line["lvl"] == "debug"
111
112        # result level has dict line["msg"]
113        if not isinstance(line["msg"], str):
114            return False
115
116        # we have a line, check if it is a kernel message
117        if re.search(r"\[[\d\s]{5}\.[\d\s]{6}\] +\S{2,}", line["msg"]):
118            print_log(f"{CONSOLE_LOG['BOLD']}{line['msg']}{CONSOLE_LOG['RESET']}")
119            return True
120
121        return False
122
123    def feed(self, new_lines: list[dict[str, str]]) -> bool:
124        """Input data to be processed by LogFollower instance
125        Returns true if the DUT (device under test) seems to be alive.
126        """
127
128        self.watchdog()
129
130        # No signal of job health in the log
131        is_job_healthy = False
132
133        for line in new_lines:
134            if self.detect_kernel_dump_line(line):
135                continue
136
137            # At least we are fed with a non-kernel dump log, it seems that the
138            # job is progressing
139            is_job_healthy = True
140            self.manage_gl_sections(line)
141            if parsed_line := parse_lava_line(line):
142                self._buffer.append(parsed_line)
143
144        self.log_hints.detect_failure(new_lines)
145
146        return is_job_healthy
147
148    def flush(self) -> list[str]:
149        buffer = self._buffer
150        self._buffer = []
151        return buffer
152
153
154def fix_lava_color_log(line):
155    """This function is a temporary solution for the color escape codes mangling
156    problem. There is some problem in message passing between the LAVA
157    dispatcher and the device under test (DUT). Here \x1b character is missing
158    before `[:digit::digit:?:digit:?m` ANSI TTY color codes, or the more
159    complicated ones with number values for text format before background and
160    foreground colors.
161    When this problem is fixed on the LAVA side, one should remove this function.
162    """
163    line["msg"] = re.sub(r"(\[(\d+;){0,2}\d{1,3}m)", "\x1b" + r"\1", line["msg"])
164
165
166def fix_lava_gitlab_section_log(line):
167    """This function is a temporary solution for the Gitlab section markers
168    mangling problem. Gitlab parses the following lines to define a collapsible
169    gitlab section in their log:
170    - \x1b[0Ksection_start:timestamp:section_id[collapsible=true/false]\r\x1b[0Ksection_header
171    - \x1b[0Ksection_end:timestamp:section_id\r\x1b[0K
172    There is some problem in message passing between the LAVA dispatcher and the
173    device under test (DUT), that digests \x1b and \r control characters
174    incorrectly. When this problem is fixed on the LAVA side, one should remove
175    this function.
176    """
177    if match := re.match(r"\[0K(section_\w+):(\d+):(\S+)\[0K([\S ]+)?", line["msg"]):
178        marker, timestamp, id_collapsible, header = match.groups()
179        # The above regex serves for both section start and end lines.
180        # When the header is None, it means we are dealing with `section_end` line
181        header = header or ""
182        line["msg"] = f"\x1b[0K{marker}:{timestamp}:{id_collapsible}\r\x1b[0K{header}"
183
184
185def parse_lava_line(line) -> Optional[str]:
186    prefix = ""
187    suffix = ""
188
189    if line["lvl"] in ["results", "feedback", "debug"]:
190        return
191    elif line["lvl"] in ["warning", "error"]:
192        prefix = CONSOLE_LOG["FG_RED"]
193        suffix = CONSOLE_LOG["RESET"]
194    elif line["lvl"] == "input":
195        prefix = "$ "
196        suffix = ""
197    elif line["lvl"] == "target":
198        fix_lava_color_log(line)
199        fix_lava_gitlab_section_log(line)
200
201    return f'{prefix}{line["msg"]}{suffix}'
202
203
204def print_log(msg):
205    # Reset color from timestamp, since `msg` can tint the terminal color
206    print(f"{CONSOLE_LOG['RESET']}{datetime.now()}: {msg}")
207
208
209def fatal_err(msg):
210    colored_msg = f"{CONSOLE_LOG['FG_RED']}"
211    f"{msg}"
212    f"{CONSOLE_LOG['RESET']}"
213    print_log(colored_msg)
214    sys.exit(1)
215
216
217def hide_sensitive_data(yaml_data, hide_tag="HIDEME"):
218    return "".join(line for line in yaml_data.splitlines(True) if hide_tag not in line)
219