Files
o3de/Tools/LyTestTools/ly_test_tools/o3de/ap_log_parser.py
T
Steve Pham 38261d0800 Shorten copyright headers by splitting into 2 lines (#2213)
* Updated all copyright headers to split the longer original copyright line into 2 shorter lines

Signed-off-by: Steve Pham <spham@amazon.com>
2021-07-16 15:25:48 -07:00

267 lines
11 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import logging
import re
from typing import List, Optional, Dict, Generator, Tuple
import time
logger = logging.getLogger(__name__)
class APOutputParser:
"""
Asset Processor Parser to extract information from generic asset processor output.
Immutable.
"""
_SEPARATOR = ": "
_NONE_LINE = "none"
# Does support multiple output runs
_NEW_RUN_LINE = "AzFramework File Logging New Run"
# Regular expression constants and keys used for looking up information later.
_RE_INT_LINES = ( # Extract an integer
(re.compile(r"^Number of Assets Successfully Processed: (\d*)."), "Successes"),
(re.compile(r"^Number of Assets Failed to Process: (\d*)."), "Failures"),
(re.compile(r"^Number of Warnings Reported: (\d*)."), "Warnings"),
(re.compile(r"^Number of Errors Reported: (\d*)."), "Errors"),
(re.compile(r"^Control Port: (\d*)"), "Control Port"),
(re.compile(r"^Listening Port: (\d*)"), "Listening Port"),
)
_RE_FLOAT_LINES = ((re.compile(r"^Total Assets Processing Time: (\d*\.?\d*)s"), "Time"),) # Extract a float
_RE_TUPLE_LINES = ( # Extract a tuple of integers
(re.compile(r"^Builder optimization: (\d*) / (\d*)"), "Full Analysis"),
)
_RE_STR_LINES = ( # Extract a string
(re.compile(r"^AssetBuilder: Source = (.*)"), "Source"),
(re.compile(r"^AssetBuilder: Platforms = (.*)"), "Platforms"),
)
# JobLogs log both errors and warnings on the same line. Handled separately
_RE_ERRORS_WARNINGS = re.compile(r"^S: (\d*)\D*(\d*)")
def __init__(self, raw_output: str) -> None:
self._runs = []
self.log_type = "Output"
self._parse_lines(raw_output)
@property
def file_path(self) -> str:
"""Parsed log file path"""
return self._file_path
@property
def runs(self) -> List[Dict]:
"""
Returns all runs from the log as a list of dictionaries.
Dictionary keys are as follows:
["Lines"]: [str] - All lines from the log for that run.
["Timestamps"]: [int] - Timestamp from each entry in Lines list
["Errors"]: int - the number of errors produced.
["Warnings"]: int - the number of warnings produced.
["Successes"]: int - the recorded number of successful assets processed.
["Failures"]: int - the recorded number of assets that failed to process.
["Control Port"]: int - port for the control socket recorded in the ap gui log
["Listening Port"]: int - port asset processor is listening for incoming connections on,
written to the AP log for both batch and gui
["Time"]: float - the recorded time it took for the AP to complete the run.
["Full Analysis"]: (int, int) - A tuple where (x, y) is associated with the line:
"x / y files required full analysis..."
["Source"]: str - (JobLogs only) the source recorded for the build job.
["Platforms"]: str - (JobLogs only) The platforms recorded for the job.
"""
return self._runs
def get_line_type(self, line: str) -> str:
"""Parses the line type from a trimmed log line. See: APLogParser._trim_line(self, line)"""
split = line.split(self._SEPARATOR, 1)
if len(split) > 1:
return split[0]
return ""
def remove_line_type(self, line: str) -> str:
"""Removes the line type from a trimmed log line. See: APLogParser._trim_line(self, line)"""
split = line.split(self._SEPARATOR, 1)
if len(split) > 1:
return split[1]
return ""
# fmt:off
def get_lines(self, run: Optional[int], contains: Optional[List[str] or str] = None,
regex: Optional[str] = None) -> Generator[str, None, None] or Generator[re.Match, None, None]:
# fmt:on
"""
Iterate the lines in the log by specifying a run index (or None for all).
Filter results returned using the [contains] string.
Or return a regex match object via the [regex] string.
Prioritizes [regex] searching over [contains] searching.
:param run: The index of the run to search. If None, all runs are searched
:param contains: A string (or list of strings) to search for in the log
:param regex: A regular expression string to use to search the log.
:return: Each line that matches
"""
runs_to_search = []
if run is None:
runs_to_search = self._runs
else:
runs_to_search.append(self._runs[run])
for run in runs_to_search:
for line in run["Lines"]:
if regex is not None:
match = re.match(regex, line)
if match:
yield match
elif contains is not None:
if type(contains) == str:
contains = [contains]
# List comprehension returns empty list if all search strings are in the line
if not [True for search_string in contains if search_string not in line]:
yield line
else:
yield line
def _parse_lines(self, all_lines: str):
current_run = self._create_log_dict()
for i, line in enumerate(all_lines):
trimmed, timestamp = self._trim_line(line)
line_type = self.get_line_type(trimmed)
if trimmed and line_type:
if line_type != self._NONE_LINE:
# not a "None" line, digest the line
self._digest_line(trimmed, current_run, timestamp)
elif self._NEW_RUN_LINE in all_lines[i - 1] and current_run["Lines"]:
# Hit the end of a "run" in the log. Store it, clear it and continue
self._runs.append(current_run)
current_run = self._create_log_dict()
if current_run["Lines"]:
self._runs.append(current_run)
def _trim_line(self, line: str) -> str:
""" For raw input in APOutputParser simply returns the line (Already "trimmed")
APLogParser's implementation trims a raw log line to remove the first 3 sections when using a log
See APLogParser : _trim_line below
"""
return line, int(round(time.time() * 1000))
def _digest_line(self, trimmed_line: str, current_run: Dict, line_timestamp: int) -> None:
"""Extracts relevant information (if present) and adds line to list of all lines"""
line_type = self.get_line_type(trimmed_line)
# Store all lines under ["Lines"]
if line_type in current_run.keys():
current_run[line_type].append(trimmed_line.split(self._SEPARATOR)[1])
current_run["Lines"].append(trimmed_line)
current_run["Timestamps"].append(line_timestamp)
trimmed_line = self.remove_line_type(trimmed_line)
# Parse useful data if present. Parsing rules declared in static constants.
for pattern, run_key in self._RE_INT_LINES:
# Look for integer data (Errors, warnings... etc.)
result = pattern.match(trimmed_line)
if result:
current_run[run_key] = int(result.groups()[0])
return
for pattern, run_key in self._RE_FLOAT_LINES:
# Look for float data (Time... etc.)
result = pattern.match(trimmed_line)
if result:
current_run[run_key] = float(result.groups()[0])
return
for pattern, run_key in self._RE_TUPLE_LINES:
# Look for tuple data ("x / y files required full analysis"... etc.)
result = pattern.match(trimmed_line)
if result:
current_run[run_key] = (int(result.groups()[0]), int(result.groups()[1]))
return
for pattern, run_key in self._RE_STR_LINES:
# Look for string data (Source, Platform... etc.)
result = pattern.match(trimmed_line)
if result:
current_run[run_key] = result.groups()[0]
return
# JobLog Errors / Warnings are reported on a single line. Handle separately
result = self._RE_ERRORS_WARNINGS.match(trimmed_line)
if result and len(result.groups()) == 2:
current_run["Errors"] = int(result.groups()[0])
current_run["Warnings"] = int(result.groups()[1])
@staticmethod
def _create_log_dict() -> Dict:
"""Creates a dictionary ready to used to store log information"""
return {
"Lines": [],
"Errors": None,
"Successes": None,
"Warnings": None,
"Time": None,
"Full Analysis": None,
"Source": None,
"Platforms:": None,
"Timestamps": []
}
class APLogParser(APOutputParser):
"""
Asset Processor Log Parser to extract information from an asset processor log.
Immutable.
"""
_SEPARATOR = "~~"
_NONE_LINE = "none"
_NEW_RUN_LINE = "AzFramework File Logging New Run"
def __init__(self, file_path: str, raw_output: str = None) -> None:
self._runs = []
self._file_path = file_path
self.log_type = None
if "JobLogs" in file_path:
self.log_type = "JobLog"
elif "AP_Batch" in file_path:
self.log_type = "Batch"
elif "AP_GUI" in file_path:
self.log_type = "GUI"
self._parse_file()
@property
def file_path(self) -> str:
"""Parsed log file path"""
return self._file_path
def _parse_file(self) -> None:
"""
Parses the APLogParser's file and populates a "run" for every AP logging run present.
"""
try:
logger.info(f"Parsing log file file: {self._file_path}")
with open(self._file_path, "r") as log_file:
all_lines = log_file.readlines()
self._parse_lines(all_lines)
except OSError:
logger.error(f"Error opening file: {self._file_path}")
self._runs = []
def _trim_line(self, line: str) -> Tuple[str, int]:
"""Trims a raw log line to remove the first 3 sections
example:
~~1581617216532~~1~~0000000000002540~~AssetProcessor~~Loading (Gem) Module 'GemRegistry'...
trims to:
AssetProcessor~~Loading (Gem) Module 'GemRegistry'...
where the trimmed line is: <line-type>~~<line-contents>
"""
split = line.strip().split(self._SEPARATOR, 4)
if len(split) > 4:
return split[4], int(split[1])
return "", 0