log.py 内容import inspectclass Logger:Simple logging utility with adjustable levels at runtimeLEVELS {DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40, CRIT: 50}def __init__(self, level_nameINFO):self.level self.LEVELS.get(level_name, 20)self.crit(finit log level {level_name})def set_level(self, level_name):self.level self.LEVELS.get(level_name, 20)self.crit(fset log level {level_name})def _log(self, level_name, *args, **kwargs):Internal logging method: checks level and outputs if allowedif self.LEVELS[level_name] self.level:return# 获取调用者的帧跳过当前 _log 和具体等级方法frame inspect.currentframe().f_back.f_backfuncname frame.f_code.co_namelineno frame.f_linenoif self.LEVELS.get(level_name) self.LEVELS.get(WARN):print(f{funcname} {lineno}: [{level_name}], *args, **kwargs)else:print(f{funcname} {lineno}:, *args, **kwargs)def debug(self, *args, **kwargs):self._log(DEBUG, *args, **kwargs)def info(self, *args, **kwargs):self._log(INFO, *args, **kwargs)def warn(self, *args, **kwargs):self._log(WARN, *args, **kwargs)def error(self, *args, **kwargs):self._log(ERROR, *args, **kwargs)def crit(self, *args, **kwargs):self._log(CRIT, *args, **kwargs)parse_log.py 内容:#!/usr/bin/env python3# -*- coding: utf-8 -*-Log Parser ApplicationParses log files to analyze screen wake-up time records.Supports both application logs and kernel logs with configurable log levels.import datetimeimport osimport reimport sysfrom tkinter import (Tk, Button, Label, OptionMenu, StringVar, Frame, Radiobutton, IntVar)from tkinter.filedialog import askopenfilenamefrom log import Logger# Module-level constantsCURRENT_LOG_LEVEL DEBUGlogger Logger(CURRENT_LOG_LEVEL)APP_KEYWORDS (# Waking up from Asleep,Waking up from,setPowerMode to 2,Finished setting power mode 2,SurfaceControl: setDisplayBrightness)KERNEL_KEYWORDS (,lcm_prepare: enter,lcm_prepare: exit,lcm_setbacklight_cmdq: bl level)TIME_PATTERN_1 r\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}TIME_PATTERN_2 r\[\s*(\d\.\d{6})\]# -------------------- Data Classes --------------------class LogRecord:Data class for a single log record.def __init__(self):Initialize a new log record with empty fields.self.reason self.logs [ for _ in range(4)]self.times [ for _ in range(4)]self.kernel_times [ for _ in range(4)]self.delta_t12 0self.delta_t13 0# -------------------- Parser Class --------------------class LogParser:Parser for log files.REASON_PATTERN rreason([^,])TABLE_FORMAT | {:^3} | {:^24} | {:^18} | {:^18} | {:^18} | {:^18} | {:^5} | {:^5} |SEPARATE_LINE |-{0:-^3}--{0:-^24}--{0:-^18}--{0:-^18}--{0:-^18}--{0:-^18}--{0:-^5}--{0:-^5}-|.format()def __init__(self, log_keywords):Initialize parser.Args:log_keywords (tuple): Keywords to search for in logs.self.log_keywords log_keywordsself.records []self.current_record LogRecord()def parse_file(self, file_path):Parse log file.Args:file_path (str): Path to the log file.logger.info(fStart parsing file: {file_path})# Reset stateself.records []self.current_record LogRecord()try:with open(file_path, r, encodingISO-8859-1) as file:lines file.readlines()except FileNotFoundError:logger.error(fFile not found: {file_path})returnexcept Exception as e:logger.error(fError reading file: {e})return# Process each linefor line in lines:self._process_line(line)# Save the last recordself._save_current_record()logger.info(fParse completed, found {len(self.records)} records)# Debug outputself._debug_print_records()# Display resultsself.show_results()# self.show_results(analysis_report.txt)def _process_line(self, line):Process a single line from the log file.Args:line (str): A line of log text.# Check if need to save current record (start new record)if self.log_keywords APP_KEYWORDS:if self.log_keywords[0] in line or self.log_keywords[1] in line:self._save_current_record()else:if self.log_keywords[1] in line:self._save_current_record()# Loop over all four keywordsfor i in range(4):if self.log_keywords KERNEL_KEYWORDS and i 0:continueif self.log_keywords[i] in line:if i 0: # First keyword: extract reasonret re.search(self.REASON_PATTERN, line)if ret:self.current_record.reason ret.group(1)logger.debug(fret {ret} ret.group(1) {ret.group(1)} fline {line[:-1]})# Additional checks for keywords 2 and 3if i 2 and (self.current_record.logs[1] orself.current_record.logs[2] ! ):breakif i 3 and (self.current_record.logs[2] orself.current_record.logs[3] ! ):break# Store log and timeself.current_record.logs[i] linetime_match re.search(TIME_PATTERN_1, line)if time_match:self.current_record.times[i] time_match.group()if self.log_keywords KERNEL_KEYWORDS:time_match re.search(TIME_PATTERN_2, line)if time_match:self.current_record.kernel_times[i] float(time_match.group(1))logger.debug(flen(records) {len(self.records)} flogs[{i}] {self.current_record.logs[i][:-1]} ftimes[{i}] {self.current_record.times[i]} fkernel_times[{i}] {self.current_record.kernel_times[i]})break # Stop after first matching keyworddef _calculate_time_difference(self, index1, index2):Calculate time difference between two timestamps.Args:index1 (int): Index of the first timestamp.index2 (int): Index of the second timestamp.Returns:float: Time difference in seconds.if self.log_keywords APP_KEYWORDS:try:time_format %Y-%m-%d %H:%M:%S.%f# Write a leap year to avoid errors when the date is 2/29t1 f2024-{self.current_record.times[index1]}t2 f2024-{self.current_record.times[index2]}t1 datetime.datetime.strptime(t1, time_format)t2 datetime.datetime.strptime(t2, time_format)delta_t12 round((t2 - t1).total_seconds(), 4)except ValueError as e:logger.error(fTime parse error: {e})delta_t12 0logger.debug(f{self.current_record.times[index1]} f{self.current_record.times[index2]} {delta_t12})else:delta_t12 round(self.current_record.kernel_times[index2] -self.current_record.kernel_times[index1], 4)logger.debug(f{self.current_record.kernel_times[index1]} f{self.current_record.kernel_times[index2]} {delta_t12})return delta_t12def _save_current_record(self):Save current record to records list if it is complete.# Check if record is complete (has log1 and log2)if self.current_record.logs[1] ! and self.current_record.logs[2] ! :self.current_record.delta_t12 self._calculate_time_difference(1, 2)if self.current_record.logs[3] ! :self.current_record.delta_t13 self._calculate_time_difference(1, 3)if (self.current_record.delta_t12 0 andself.current_record.delta_t13 0):logger.debug(fcurrent_record {self.current_record.__dict__})self.records.append(self.current_record)self.current_record LogRecord()def _debug_print_records(self):Print all records for debugging purposes.logger.debug(flen(records) {len(self.records)})for i, record in enumerate(self.records):log_output []for j in range(4):log_output.append(frecord.logs[{j}] f{record.logs[j][:-1] if record.logs[j] else })logger.debug(fi {i}\n \n.join(log_output) \n frecord.times {record.times}\nfrecord.kernel_times {record.kernel_times}\nfrecord.delta_t12 {record.delta_t12}\nfrecord.delta_t13 {record.delta_t13}\n)def show_results(self, file_pathNone):Display all parse results, optionally writing to a file.Args:file_path (str, optional): Path to output file. If None, prints to stdout.out_file Noneif file_path:try:out_file open(file_path, w, encodingutf-8)except Exception as e:logger.error(fFailed to open file {file_path}: {e})returnelse:out_file sys.stdout# Display by reason groupsunique_reasons set(record.reason for record in self.records)try:for reason in unique_reasons:self.show_by_reason(reason, out_file)# Display summary tableself.show_summary(out_file)finally:if file_path:out_file.close()def show_by_reason(self, reason, out_file):Show records filtered by reason.Args:reason (str): Reason to filter by.out_file: File-like object to write output to.matched_records [record.delta_t12 for record in self.recordsif record.reason reason]if not matched_records:returnaver_t12 round(sum(matched_records) / len(matched_records) * 1000)print(fTable for reason {reason}, average_t12 {aver_t12} ms, fileout_file)print(f{- * 134}, fileout_file)print(self.TABLE_FORMAT.format(num, reason, t0, t1, t2, t3, t2-t1, t3-t1),fileout_file)print(self.SEPARATE_LINE, fileout_file)print(self.TABLE_FORMAT.format(, ,f({self.log_keywords[0][:16]}),f({self.log_keywords[1][:16]}),f({self.log_keywords[2][:16]}),f({self.log_keywords[3][:16]}), (ms), (ms)),fileout_file)for i, record in enumerate(self.records):if record.reason reason:print(self.SEPARATE_LINE, fileout_file)t12_ms round(record.delta_t12 * 1000)t13_ms (round(record.delta_t13 * 1000)if record.delta_t13 ! 0 else )print(self.TABLE_FORMAT.format(i, record.reason, record.times[0], record.times[1],record.times[2], record.times[3], t12_ms, t13_ms),fileout_file)print(f{- * 134}\n\n, fileout_file)def show_summary(self, out_file):Show summary table of all records.Args:out_file: File-like object to write output to.if not self.records:logger.warn(No records found)returntotal_t12 sum(record.delta_t12 for record in self.records)aver_t12 round(total_t12 / len(self.records) * 1000)print(fSummary table - records: {len(self.records)}, faverage_t12: {aver_t12} ms,fileout_file)print(- * 134, fileout_file)print(self.TABLE_FORMAT.format(num, reason, t0, t1, t2, t3, t2-t1, t3-t1),fileout_file)print(self.SEPARATE_LINE, fileout_file)print(self.TABLE_FORMAT.format(, ,f({self.log_keywords[0][:16]}),f({self.log_keywords[1][:16]}),f({self.log_keywords[2][:16]}),f({self.log_keywords[3][:16]}), (ms), (ms)),fileout_file)for i, record in enumerate(self.records):print(self.SEPARATE_LINE, fileout_file)t12_ms round(record.delta_t12 * 1000)t13_ms (round(record.delta_t13 * 1000)if record.delta_t13 ! 0 else )if self.log_keywords APP_KEYWORDS:print(self.TABLE_FORMAT.format(i, record.reason, record.times[0], record.times[1],record.times[2], record.times[3], t12_ms, t13_ms),fileout_file)else:print(self.TABLE_FORMAT.format(i, , , record.times[1],record.times[2], record.times[3], , ),fileout_file)print(self.TABLE_FORMAT.format(, , , record.kernel_times[1],record.kernel_times[2], record.kernel_times[3],t12_ms, t13_ms),fileout_file)print(f{- * 134}\n\n, fileout_file)# -------------------- GUI Application --------------------class LogAnalyzerApp:GUI application class.def __init__(self):Initialize the GUI application.self.root Tk()self.setup_ui()def setup_ui(self):Setup user interface.self.root.geometry(600x40000)self.root.title(Log Parser v1.0)radio_frame Frame(self.root)radio_frame.pack(pady10)Label(radio_frame, textSelect keywords group:).pack(sideleft, padx(0, 10))self.keywords_group IntVar(value0)Radiobutton(radio_frame,textApp log keywords,variableself.keywords_group,value0,commandself.update_keywords_display).pack(sideleft, padx(0, 20))Radiobutton(radio_frame,textKernel log keywords,variableself.keywords_group,value1,commandself.update_keywords_display).pack(sideleft)self.keywords_label Label(self.root,width60,height5,bgwhite,wraplength500,justifyleft,padx0,pady0)self.keywords_label.pack(pady2)self.update_keywords_display()Button(self.root,textClick to select log file,width30,commandself.select_log).pack(pady10)self.file_label Label(self.root,width60,height5,bgwhite,wraplength400,justifyleft,padx0,pady0)self.file_label.pack(pady2)log_level_frame Frame(self.root)log_level_frame.pack(sideleft, pady10)Label(log_level_frame, textLog level:).pack(sideleft, padx(0, 10))self.log_level StringVar()self.log_level.set(CURRENT_LOG_LEVEL)OptionMenu(log_level_frame,self.log_level,*logger.LEVELS.keys(),commandlambda level: logger.set_level(level)).pack(sideleft)Button(self.root,textClear console,width15,commandlambda: os.system(cls if os.name nt else clear)).pack(sideright, pady10)def update_keywords_display(self):Update keywords display based on selection.if self.keywords_group.get() 0:keywords APP_KEYWORDSelse:keywords KERNEL_KEYWORDStext_lines []for i, keyword in enumerate(keywords):text_lines.append(fkeywords[{i}] {keyword})self.keywords_label.config(text\n.join(text_lines))logger.info(fKeywords group changed to: {keywords})def select_log(self):Select log file based on radio button selection.if self.keywords_group.get() 0:keywords APP_KEYWORDSlog_type_name App Logelse:keywords KERNEL_KEYWORDSlog_type_name Kernel Loglog_file askopenfilename(titlefSelect {log_type_name},filetypes[(All files, *.*), (Log, *.log), (Text, *.txt)])if log_file:self.file_label.config(textfFile {log_file})logger.info(fSelected file: {log_file})parser LogParser(keywords)parser.parse_file(log_file)else:self.file_label.config(textNo file selected)logger.info(File selection cancelled)def run(self):Run the application.self.root.mainloop()def main():Main function.app LogAnalyzerApp()app.run()if __name__ __main__:main()测试用例applogcat-log.txt内容:11-01 12:34:56.123 1386 2091 I PowerManagerService: Waking up from Asleep (uid1000, reasonWAKE_REASON_POWER_BUTTON, detailsandroid.policy:POWER)...2025-11-01 12:34:57.123 895 982 I SurfaceFlinger: setPowerMode to 2 from pid1386, uid100011-01 12:34:58.124 895 895 I SurfaceFlinger: Finished setting power mode 2 on display 462703942230018764811-01 12:34:59.125 1386 2303 I SurfaceControl: setDisplayBrightness. {0.1313524, 68.101685, 0.1313524, 68.101685, 0.13545427, 68.101685}11-01 12:34:59.456 1386 2303 I SurfaceControl: setDisplayBrightness. {0.1313524, 68.101685, 0.1313524, 68.101685, 0.13545427, 68.101685}11-01 13:34:57.123 895 982 I SurfaceFlinger: setPowerMode to 2 from pid1386, uid100011-01 13:34:58.123 895 895 I SurfaceFlinger: Finished setting power mode 2 on display 462703942230018764811-01 13:34:59.123 1386 2303 I SurfaceControl: setDisplayBrightness. {0.1313524, 68.101685, 0.1313524, 68.101685, 0.13545427, 68.101685}11-01 14:34:57.123 895 982 I SurfaceFlinger: setPowerMode to 2 from pid1386, uid100011-01 14:34:58.123 895 895 I SurfaceFlinger: Finished setting power mode 2 on display 4627039422300187648模拟其它无关log 11-01 14:34:59.123 Parse LCD screen-on records and time, click to select log file行 4266: 10-25 10:26:30.125 1386 2091 I PowerManagerService: Waking up from Asleep (uid1000, reasonWAKE_REASON_TAP, detailsandroid.policy:POWER)...行 4602: 10-25 10:26:30.176 895 982 I SurfaceFlinger: setPowerMode to 2 from pid1386, uid1000行 6002: 10-25 10:26:30.333 895 895 I SurfaceFlinger: Finished setting power mode 2 on display 4627039422300187648行 6094: 10-25 10:26:30.339 1386 2303 I SurfaceControl: setDisplayBrightness. {0.1313524, 68.101685, 0.1313524, 68.101685, 0.13545427, 68.101685}测试用例kmsgcat-log.txt 内容2025-11-01 12:34:57.123 3[ 180.123456][ T791] [2025:11:28 02:43:56](0)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: enter11-01 12:34:58.124 3[ 181.124456][ T791] [2025:11:28 02:43:56](3)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: exit11-01 12:34:59.125 3[ 182.125456][ T1362] [2025:11:28 02:43:56](3)[1362:AALMain][LCD_KIT/I]lcm_setbacklight_cmdq: bl level:86311-01 12:34:59.456 3[ 183.126456][ T1362] [2025:11:28 02:43:56](3)[1362:AALMain][LCD_KIT/I]lcm_setbacklight_cmdq: bl level:86311-01 13:34:57.123 3[ 181.123456][ T791] [2025:11:28 02:43:56](0)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: enter11-01 13:34:58.123 3[ 182.124456][ T791] [2025:11:28 02:43:56](3)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: exit11-01 13:34:59.123 3[ 183.125456][ T1362] [2025:11:28 02:43:56](3)[1362:AALMain][LCD_KIT/I]lcm_setbacklight_cmdq: bl level:86311-01 14:34:57.123 3[ 182.123456][ T791] [2025:11:28 02:43:56](0)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: enter11-01 14:34:58.123 3[ 183.124456][ T791] [2025:11:28 02:43:56](3)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: exit模拟其它无关log [ 2102.724133] 11-01 14:34:59.123 Parse LCD screen-on records and time, click to select log file行 140: 11-28 02:43:57.268 3[ 180.082243][ T791] [2025:11:28 02:43:56](0)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: enter行 320: 11-28 02:43:57.396 3[ 180.211307][ T791] [2025:11:28 02:43:56](3)[791:composer3.1-se][LCD_KIT/I]lcm_prepare: exit行 456: 11-28 02:43:57.451 3[ 180.262656][ T1362] [2025:11:28 02:43:56](3)[1362:AALMain][LCD_KIT/I]lcm_setbacklight_cmdq: bl level:863成品效果