xref: /aosp_15_r20/external/autotest/client/cros/bluetooth/logger_helper.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright 2021 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li"""Helper class to manage bluetooth logs"""
6*9c5db199SXin Li
7*9c5db199SXin Lifrom datetime import datetime
8*9c5db199SXin Liimport logging
9*9c5db199SXin Liimport os
10*9c5db199SXin Liimport re
11*9c5db199SXin Liimport subprocess
12*9c5db199SXin Liimport time
13*9c5db199SXin Li
14*9c5db199SXin Li
15*9c5db199SXin LiSYSLOG_PATH = '/var/log/messages'
16*9c5db199SXin Li
17*9c5db199SXin Li
18*9c5db199SXin Liclass LogManager(object):
19*9c5db199SXin Li    """The LogManager class helps to collect logs without a listening thread"""
20*9c5db199SXin Li
21*9c5db199SXin Li    DEFAULT_ENCODING = 'utf-8'
22*9c5db199SXin Li
23*9c5db199SXin Li    class LoggingException(Exception):
24*9c5db199SXin Li        """A stub exception class for LogManager class."""
25*9c5db199SXin Li        pass
26*9c5db199SXin Li
27*9c5db199SXin Li    def __init__(self, log_path=SYSLOG_PATH, raise_missing=False):
28*9c5db199SXin Li        """Initialize log manager object
29*9c5db199SXin Li
30*9c5db199SXin Li        @param log_path: string path to log file to manage
31*9c5db199SXin Li        @param raise_missing: raise an exception if the log file is missing
32*9c5db199SXin Li
33*9c5db199SXin Li        @raises: LogManager.LoggingException on non-existent log file
34*9c5db199SXin Li        """
35*9c5db199SXin Li        if not os.path.isfile(log_path):
36*9c5db199SXin Li            msg = 'Requested log file {} does not exist'.format(log_path)
37*9c5db199SXin Li            if raise_missing:
38*9c5db199SXin Li                raise LogManager.LoggingException(msg)
39*9c5db199SXin Li            else:
40*9c5db199SXin Li                self._LogErrorToSyslog(msg)
41*9c5db199SXin Li
42*9c5db199SXin Li        self.log_path = log_path
43*9c5db199SXin Li
44*9c5db199SXin Li        self.ResetLogMarker()
45*9c5db199SXin Li        self._bin_log_contents = []
46*9c5db199SXin Li
47*9c5db199SXin Li    def _LogErrorToSyslog(self, message):
48*9c5db199SXin Li        """Create a new syslog file and add a message to syslog."""
49*9c5db199SXin Li        subprocess.call(['reload', 'syslog'])
50*9c5db199SXin Li        subprocess.call(['logger', message])
51*9c5db199SXin Li
52*9c5db199SXin Li    def _GetSize(self):
53*9c5db199SXin Li        """Get the size of the log"""
54*9c5db199SXin Li        try:
55*9c5db199SXin Li            return os.path.getsize(self.log_path)
56*9c5db199SXin Li        except Exception as e:
57*9c5db199SXin Li            logging.error('Failed to get log size: {}'.format(e))
58*9c5db199SXin Li            return 0
59*9c5db199SXin Li
60*9c5db199SXin Li    def ResetLogMarker(self, now_size=None):
61*9c5db199SXin Li        """Reset the start-of-log marker for later comparison"""
62*9c5db199SXin Li        if now_size is None:
63*9c5db199SXin Li            now_size = self._GetSize()
64*9c5db199SXin Li        self.initial_log_size = now_size
65*9c5db199SXin Li
66*9c5db199SXin Li    def StartRecording(self):
67*9c5db199SXin Li        """Mark initial log size for later comparison"""
68*9c5db199SXin Li
69*9c5db199SXin Li        self._bin_log_contents = []
70*9c5db199SXin Li
71*9c5db199SXin Li    def StopRecording(self):
72*9c5db199SXin Li        """Gather the logs since StartRecording was called
73*9c5db199SXin Li
74*9c5db199SXin Li        @raises: LogManager.LoggingException if:
75*9c5db199SXin Li                - Log file disappeared since StartRecording was called
76*9c5db199SXin Li                - Log file is smaller than when logging began
77*9c5db199SXin Li                - StartRecording was never called
78*9c5db199SXin Li        """
79*9c5db199SXin Li        initial_size = self.initial_log_size
80*9c5db199SXin Li        now_size = self._GetSize()
81*9c5db199SXin Li
82*9c5db199SXin Li        if not os.path.isfile(self.log_path):
83*9c5db199SXin Li            msg = 'File {} disappeared unexpectedly'.format(self.log_path)
84*9c5db199SXin Li            raise LogManager.LoggingException(msg)
85*9c5db199SXin Li
86*9c5db199SXin Li        if now_size < initial_size:
87*9c5db199SXin Li            msg = 'Log became smaller unexpectedly'
88*9c5db199SXin Li            raise LogManager.LoggingException(msg)
89*9c5db199SXin Li
90*9c5db199SXin Li        with open(self.log_path, 'rb') as mf:
91*9c5db199SXin Li            # Skip to the point where we started recording
92*9c5db199SXin Li            mf.seek(self.initial_log_size)
93*9c5db199SXin Li
94*9c5db199SXin Li            readsize = now_size - self.initial_log_size
95*9c5db199SXin Li            self._bin_log_contents = mf.read(readsize).split(b'\n')
96*9c5db199SXin Li
97*9c5db199SXin Li        # Re-set start of log marker
98*9c5db199SXin Li        self.ResetLogMarker(now_size)
99*9c5db199SXin Li
100*9c5db199SXin Li    def LogContains(self, search_str):
101*9c5db199SXin Li        """Performs simple string checking on each line from the collected log
102*9c5db199SXin Li
103*9c5db199SXin Li        @param search_str: string to be located within log contents. This arg
104*9c5db199SXin Li                is expected to not span between lines in the logs
105*9c5db199SXin Li
106*9c5db199SXin Li        @returns: True if search_str was located in the collected log contents,
107*9c5db199SXin Li                False otherwise
108*9c5db199SXin Li        """
109*9c5db199SXin Li        pattern = re.compile(search_str.encode(self.DEFAULT_ENCODING))
110*9c5db199SXin Li        for line in self._bin_log_contents:
111*9c5db199SXin Li            if pattern.search(line):
112*9c5db199SXin Li                return True
113*9c5db199SXin Li
114*9c5db199SXin Li        return False
115*9c5db199SXin Li
116*9c5db199SXin Li    def FilterOut(self, rm_reg_exp):
117*9c5db199SXin Li        """Remove lines with specified pattern from the log file
118*9c5db199SXin Li
119*9c5db199SXin Li        @param rm_reg_exp: regular expression of the lines to be removed
120*9c5db199SXin Li        """
121*9c5db199SXin Li        # If log_path doesn't exist, there's nothing to do
122*9c5db199SXin Li        if not os.path.isfile(self.log_path):
123*9c5db199SXin Li            return
124*9c5db199SXin Li
125*9c5db199SXin Li        rm_line_cnt = 0
126*9c5db199SXin Li        initial_size = self._GetSize()
127*9c5db199SXin Li        rm_pattern = re.compile(rm_reg_exp.encode(self.DEFAULT_ENCODING))
128*9c5db199SXin Li
129*9c5db199SXin Li        with open(self.log_path, 'rb+') as mf:
130*9c5db199SXin Li            lines = mf.readlines()
131*9c5db199SXin Li            mf.seek(0)
132*9c5db199SXin Li            for line in lines:
133*9c5db199SXin Li                if rm_pattern.search(line):
134*9c5db199SXin Li                    rm_line_cnt += 1
135*9c5db199SXin Li                else:
136*9c5db199SXin Li                    mf.write(line)
137*9c5db199SXin Li            mf.truncate()
138*9c5db199SXin Li
139*9c5db199SXin Li        # Some tracebacks point out here causing /var/log/messages missing but
140*9c5db199SXin Li        # we don't have many clues. Adding a check and logs here.
141*9c5db199SXin Li        if not os.path.isfile(self.log_path):
142*9c5db199SXin Li            msg = '{} does not exist after FilterOut'.format(self.log_path)
143*9c5db199SXin Li            logging.warning(msg)
144*9c5db199SXin Li            self._LogErrorToSyslog(msg)
145*9c5db199SXin Li
146*9c5db199SXin Li        new_size = self._GetSize()
147*9c5db199SXin Li        rm_byte = initial_size - new_size
148*9c5db199SXin Li        logging.info('Removed number of line: %d, Reduced log size: %d byte',
149*9c5db199SXin Li                     rm_line_cnt, rm_byte)
150*9c5db199SXin Li
151*9c5db199SXin Li        # Note the new size of the log
152*9c5db199SXin Li        self.ResetLogMarker(new_size)
153*9c5db199SXin Li
154*9c5db199SXin Li
155*9c5db199SXin Liclass InterleaveLogger(LogManager):
156*9c5db199SXin Li    """LogManager class that focus on interleave scan"""
157*9c5db199SXin Li
158*9c5db199SXin Li    # Example bluetooth kernel log:
159*9c5db199SXin Li    # "2020-11-23T07:52:31.395941Z DEBUG kernel: [ 6469.811135] Bluetooth: "
160*9c5db199SXin Li    # "cancel_interleave_scan() hci0: cancelling interleave scan"
161*9c5db199SXin Li    KERNEL_LOG_PATTERN = ('([^ ]+) DEBUG kernel: \[.*\] Bluetooth: '
162*9c5db199SXin Li                          '{FUNCTION}\(\) hci0: {LOG_STR}')
163*9c5db199SXin Li    STATE_PATTERN = KERNEL_LOG_PATTERN.format(
164*9c5db199SXin Li            FUNCTION='hci_req_add_le_interleaved_scan',
165*9c5db199SXin Li            LOG_STR='next state: (.+)')
166*9c5db199SXin Li    CANCEL_PATTERN = KERNEL_LOG_PATTERN.format(
167*9c5db199SXin Li            FUNCTION='cancel_interleave_scan',
168*9c5db199SXin Li            LOG_STR='cancelling interleave scan')
169*9c5db199SXin Li    SYSTIME_LENGTH = len('2020-12-18T00:11:22.345678')
170*9c5db199SXin Li
171*9c5db199SXin Li    def __init__(self):
172*9c5db199SXin Li        """ Initialize object
173*9c5db199SXin Li        """
174*9c5db199SXin Li        self.reset()
175*9c5db199SXin Li        self.state_pattern = re.compile(
176*9c5db199SXin Li                self.STATE_PATTERN.encode(self.DEFAULT_ENCODING))
177*9c5db199SXin Li        self.cancel_pattern = re.compile(
178*9c5db199SXin Li                self.CANCEL_PATTERN.encode(self.DEFAULT_ENCODING))
179*9c5db199SXin Li        super(InterleaveLogger, self).__init__()
180*9c5db199SXin Li
181*9c5db199SXin Li    def reset(self):
182*9c5db199SXin Li        """ Clear data between each log collection attempt
183*9c5db199SXin Li        """
184*9c5db199SXin Li        self.records = []
185*9c5db199SXin Li        self.cancel_events = []
186*9c5db199SXin Li
187*9c5db199SXin Li    def StartRecording(self):
188*9c5db199SXin Li        """ Reset the previous data and start recording.
189*9c5db199SXin Li        """
190*9c5db199SXin Li        self.reset()
191*9c5db199SXin Li        super(InterleaveLogger, self).ResetLogMarker()
192*9c5db199SXin Li        super(InterleaveLogger, self).StartRecording()
193*9c5db199SXin Li
194*9c5db199SXin Li    def StopRecording(self):
195*9c5db199SXin Li        """ Stop recording and parse logs
196*9c5db199SXin Li            The following data will be set after this call
197*9c5db199SXin Li
198*9c5db199SXin Li            - self.records: a dictionary where each item is a record of
199*9c5db199SXin Li                            interleave |state| and the |time| the state starts.
200*9c5db199SXin Li                            |state| could be {'no filter', 'allowlist'}
201*9c5db199SXin Li                            |time| is system time in sec
202*9c5db199SXin Li
203*9c5db199SXin Li            - self.cancel_events: a list of |time| when a interleave cancel
204*9c5db199SXin Li                                  event log was found
205*9c5db199SXin Li                                  |time| is system time in sec
206*9c5db199SXin Li
207*9c5db199SXin Li            @returns: True if StopRecording success, False otherwise
208*9c5db199SXin Li
209*9c5db199SXin Li        """
210*9c5db199SXin Li        try:
211*9c5db199SXin Li            super(InterleaveLogger, self).StopRecording()
212*9c5db199SXin Li        except Exception as e:
213*9c5db199SXin Li            logging.error(e)
214*9c5db199SXin Li            return False
215*9c5db199SXin Li
216*9c5db199SXin Li        success = True
217*9c5db199SXin Li
218*9c5db199SXin Li        def sys_time_to_timestamp(time_str):
219*9c5db199SXin Li            """ Return timestamp of time_str """
220*9c5db199SXin Li
221*9c5db199SXin Li            # This is to remove the suffix of time string, in some cases the
222*9c5db199SXin Li            # time string ends with an extra 'Z', in other cases, the string
223*9c5db199SXin Li            # ends with time zone (ex. '+08:00')
224*9c5db199SXin Li            time_str = time_str[:self.SYSTIME_LENGTH]
225*9c5db199SXin Li
226*9c5db199SXin Li            try:
227*9c5db199SXin Li                dt = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%f")
228*9c5db199SXin Li            except Exception as e:
229*9c5db199SXin Li                logging.error(e)
230*9c5db199SXin Li                success = False
231*9c5db199SXin Li                return 0
232*9c5db199SXin Li
233*9c5db199SXin Li            return time.mktime(dt.timetuple()) + dt.microsecond * (10**-6)
234*9c5db199SXin Li
235*9c5db199SXin Li        for line in self._bin_log_contents:
236*9c5db199SXin Li            line = line.strip().replace(b'\\r\\n', b'')
237*9c5db199SXin Li            state_pattern = self.state_pattern.search(line)
238*9c5db199SXin Li            cancel_pattern = self.cancel_pattern.search(line)
239*9c5db199SXin Li
240*9c5db199SXin Li            if cancel_pattern:
241*9c5db199SXin Li                time_str = cancel_pattern.groups()[0].decode(
242*9c5db199SXin Li                        self.DEFAULT_ENCODING)
243*9c5db199SXin Li                time_sec = sys_time_to_timestamp(time_str)
244*9c5db199SXin Li                self.cancel_events.append(time_sec)
245*9c5db199SXin Li
246*9c5db199SXin Li            if state_pattern:
247*9c5db199SXin Li                time_str, state = [
248*9c5db199SXin Li                        x.decode(self.DEFAULT_ENCODING)
249*9c5db199SXin Li                        for x in state_pattern.groups()
250*9c5db199SXin Li                ]
251*9c5db199SXin Li                time_sec = sys_time_to_timestamp(time_str)
252*9c5db199SXin Li                self.records.append({'time': time_sec, 'state': state})
253*9c5db199SXin Li
254*9c5db199SXin Li        return success
255