From 8512f116fc5de214fd73cacf474b3109323a5754 Mon Sep 17 00:00:00 2001 From: Douglas RAILLARD Date: Thu, 14 Mar 2019 15:32:31 +0000 Subject: [PATCH] trace: Add DmesgCollector Allows collecting dmesg output and parses it for easy filtering. --- devlib/trace/dmesg.py | 187 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 devlib/trace/dmesg.py diff --git a/devlib/trace/dmesg.py b/devlib/trace/dmesg.py new file mode 100644 index 0000000..a9f80c7 --- /dev/null +++ b/devlib/trace/dmesg.py @@ -0,0 +1,187 @@ +# Copyright 2019 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import division +import re +from itertools import takewhile +from datetime import timedelta + +from devlib.trace import TraceCollector + + +class KernelLogEntry(object): + """ + Entry of the kernel ring buffer. + + :param facility: facility the entry comes from + :type facility: str + + :param level: log level + :type level: str + + :param timestamp: Timestamp of the entry + :type timestamp: datetime.timedelta + + :param msg: Content of the entry + :type msg: str + """ + + _TIMESTAMP_MSG_REGEX = re.compile(r'\[(.*?)\] (.*)') + + def __init__(self, facility, level, timestamp, msg): + self.facility = facility + self.level = level + self.timestamp = timestamp + self.msg = msg + + @classmethod + def from_str(cls, line): + """ + Parses a "dmesg --decode" output line, formatted as following: + kern :err : [3618282.310743] nouveau 0000:01:00.0: systemd-logind[988]: nv50cal_space: -16 + + Or the more basic output: + [3618282.310743] nouveau 0000:01:00.0: systemd-logind[988]: nv50cal_space: -16 + + """ + + def parse_timestamp_msg(line): + match = cls._TIMESTAMP_MSG_REGEX.match(line.strip()) + if not match: + raise ValueError('dmesg entry format not recognized: {}'.format(line)) + timestamp, msg = match.groups() + return timestamp, msg + + # If we can parse the timestamp directly, that is a basic line + try: + timestamp, msg = parse_timestamp_msg(line) + except ValueError: + facility, level, remainder = line.split(':', 2) + timestamp, msg = parse_timestamp_msg(remainder) + facility = facility.strip() + level = level.strip() + else: + facility = None + level = None + + return cls( + facility=facility, + level=level, + timestamp=timedelta(seconds=float(timestamp.strip())), + msg=msg.strip(), + ) + + def __str__(self): + if self.facility and self.level: + prefix = '{facility}:{level}:'.format( + facility=self.facility, + level=self.level, + ) + else: + prefix = '' + + return '{prefix}[{timestamp}] {msg}'.format( + timestamp=self.timestamp.total_seconds(), + msg=self.msg, + prefix=prefix, + ) + + +class DmesgCollector(TraceCollector): + """ + Dmesg output collector. + + :param level: Minimum log level to enable. All levels that are more + critical will be collected as well. + :type level: str + + :param facility: Facility to record, see dmesg --help for the list. + :type level: str + + .. warning:: If BusyBox dmesg is used, facility and level will be ignored, + and the parsed entries will also lack that information. + """ + + # taken from "dmesg --help" + # This list needs to be ordered by priority + LOG_LEVELS = [ + "emerg", # system is unusable + "alert", # action must be taken immediately + "crit", # critical conditions + "err", # error conditions + "warn", # warning conditions + "notice", # normal but significant condition + "info", # informational + "debug", # debug-level messages + ] + + def __init__(self, target, level=LOG_LEVELS[-1], facility='kern'): + super(DmesgCollector, self).__init__(target) + + if level not in self.LOG_LEVELS: + raise ValueError('level needs to be one of: {}'.format( + ', '.join(self.LOG_LEVELS) + )) + self.level = level + + # Check if dmesg is the BusyBox one, or the one from util-linux. + # Note: BusyBox dmesg does not support -h, but will still print the + # help with an exit code of 1 + self.basic_dmesg = 'BusyBox' in self.target.execute('dmesg -h', + check_exit_code=False) + self.reset() + + @property + def entries(self): + return self._parse_entries(self.dmesg_out) + + @classmethod + def _parse_entries(cls, dmesg_out): + if not dmesg_out: + return [] + else: + return [ + KernelLogEntry.from_str(line) + for line in dmesg_out.splitlines() + ] + + def reset(self): + self.dmesg_out = None + + def start(self): + self.reset() + # Empty the dmesg ring buffer + self.target.execute('dmesg -c') + + def stop(self): + levels_list = list(takewhile( + lambda level: level != self.level, + self.LOG_LEVELS + )) + levels_list.append(self.level) + if self.basic_dmesg: + cmd = 'dmesg' + else: + cmd = 'dmesg --facility={facility} --force-prefix --decode --level={levels}'.format( + levels=','.join(levels_list), + facility=self.facility, + ) + + self.dmesg_out = self.target.execute(cmd) + + def get_trace(self, outfile): + with open(outfile, 'wt') as f: + f.write(self.dmesg_out + '\n') +