✨feat(project): add logging in project
add logging in modules classier.py, parser.py, reporter.py. Update logging in cli.
This commit is contained in:
@@ -1,23 +1,24 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from logwatcher import logger
|
||||||
from logwatcher.models import LogEntry
|
from logwatcher.models import LogEntry
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Atomic patterns — reusable building blocks (no named groups)
|
# Atomic patterns — reusable building blocks (no named groups)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
_GRP = r"GRP_\d+" # store group
|
_GRP = r"GRP_\d+" # store group
|
||||||
_STORE = r"\d{5}/\d{3}" # store code
|
_STORE = r"\d{5}/\d{3}" # store code
|
||||||
_SAP = r"\d{4}" # SAP code (4 digits)
|
_SAP = r"\d{4}" # SAP code (4 digits)
|
||||||
_ORD = r"\w+" # order number (alphanumeric)
|
_ORD = r"\w+" # order number (alphanumeric)
|
||||||
_BL = rf"{_SAP}_{_ORD}_DESADV" # BL number
|
_BL = rf"{_SAP}_{_ORD}_DESADV" # BL number
|
||||||
_SIZE = r"SizeRange \d+\.\d+" # size range
|
_SIZE = r"SizeRange \d+\.\d+" # size range
|
||||||
_BRACKETS = r"(?:\[[^\]]+\])+" # one or more [Tag] blocks
|
_BRACKETS = r"(?:\[[^\]]+\])+" # one or more [Tag] blocks
|
||||||
|
|
||||||
# Composite prefixes — reused across multiple patterns
|
# Composite prefixes — reused across multiple patterns
|
||||||
_STORE_PREFIX_ERR = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) ERREUR"
|
_STORE_PREFIX_ERR = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) ERREUR"
|
||||||
_STORE_PREFIX_WRN = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) WARNING"
|
_STORE_PREFIX_WRN = rf"(?P<store_group>{_GRP}) / (?P<store_code>{_STORE}) (?P<sap_code>{_SAP})_(?P<order_number>{_ORD}) WARNING"
|
||||||
_BL_PREFIX = rf"(?P<bl_number>{_BL}) : / (?P<store_code>{_STORE}) {_BL}"
|
_BL_PREFIX = rf"(?P<bl_number>{_BL}) : / (?P<store_code>{_STORE}) {_BL}"
|
||||||
|
|
||||||
# error message patterns
|
# error message patterns
|
||||||
N2_PATTERNS = {
|
N2_PATTERNS = {
|
||||||
@@ -35,37 +36,50 @@ N2_PATTERNS = {
|
|||||||
"PARTIAL_INTEGRATION": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>\w+)_DESADV) : ISC ERROR CODE:(?P<isc_error_code>\d+) \(attention intégration partielle du BL ou d'une commande\)",
|
"PARTIAL_INTEGRATION": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>\w+)_DESADV) : ISC ERROR CODE:(?P<isc_error_code>\d+) \(attention intégration partielle du BL ou d'une commande\)",
|
||||||
"SAME_LINE_EXCEPT_DISCOUNT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SetCOMBCDE -> OrderNumber : \d+ / Une commande ne peut pas contenir plusieurs lignes avec le même modèle, la même collection, la même date de livraison et avoir des remises différentes",
|
"SAME_LINE_EXCEPT_DISCOUNT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SetCOMBCDE -> OrderNumber : \d+ / Une commande ne peut pas contenir plusieurs lignes avec le même modèle, la même collection, la même date de livraison et avoir des remises différentes",
|
||||||
"MULTIPLE_ROWS_IN_SINGLETON_SELECT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SizeRange (?P<size_range>\d+\.\d+) / SetMrkFourn -> (?: \[[^\]]+\])+multiple rows in singleton select",
|
"MULTIPLE_ROWS_IN_SINGLETON_SELECT": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})) / SizeRange (?P<size_range>\d+\.\d+) / SetMrkFourn -> (?: \[[^\]]+\])+multiple rows in singleton select",
|
||||||
|
|
||||||
# low priority errors
|
# low priority errors
|
||||||
"SIZE_NOT_FOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (\d+ \w+) / SizeRange (?P<size_range>\d+\.\d+) - Columnx \d+ / Taille Inexistante",
|
"SIZE_NOT_FOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (\d+ \w+) / SizeRange (?P<size_range>\d+\.\d+) - Columnx \d+ / Taille Inexistante",
|
||||||
"UNSYNCED_ORD_SD_FILES": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+)(?P<brand_code>\w{3}))) / OrderNumber : \d+ / Le modèle n'existe pas dans le fichier SD correspondant",
|
"UNSYNCED_ORD_SD_FILES": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+)(?P<brand_code>\w{3}))) / OrderNumber : \d+ / Le modèle n'existe pas dans le fichier SD correspondant",
|
||||||
"CODE_MODEL_SIZE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING [\w ]+ \w+ / SetArticle -> SizeRange \d+\.\d+ / (?: \[[^\]]+\])+-\w+\. Données trop volumineuses pour la variable (?: \[[^\]]+\]). Long. max. = \[\d+\], long. réelle = \[\d+\] Conseil : attribuez une valeur supérieure au paramètre TFDParam.Size",
|
"CODE_MODEL_SIZE_OUT_OF_BOUND": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING [\w ]+ \w+ / SetArticle -> SizeRange \d+\.\d+ / (?: \[[^\]]+\])+-\w+\. Données trop volumineuses pour la variable (?: \[[^\]]+\]). Long. max. = \[\d+\], long. réelle = \[\d+\] Conseil : attribuez une valeur supérieure au paramètre TFDParam.Size",
|
||||||
"USER_NOT_CREATED_ON_NOSYMAG": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR / OrderNumber : \d+ / Utilisateur : configuration incorrecte \(valeur 0\) - vérifier dans IF_Config",
|
"USER_NOT_CREATED_ON_NOSYMAG": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR / OrderNumber : \d+ / Utilisateur : configuration incorrecte \(valeur 0\) - vérifier dans IF_Config",
|
||||||
"INCORRECT_MDC_XML_FILES": r"Exception : Impossible de convertir le variant de type \(Null\) en type \(OleStr\)"
|
"INCORRECT_MDC_XML_FILES": r"Exception : Impossible de convertir le variant de type \(Null\) en type \(OleStr\)",
|
||||||
}
|
}
|
||||||
|
|
||||||
# compiled all patterns
|
# compiled all patterns
|
||||||
_COMPILED_N2_PATTERNS = {error_name: re.compile(pattern) for error_name, pattern in N2_PATTERNS.items()}
|
_COMPILED_N2_PATTERNS = {
|
||||||
|
error_name: re.compile(pattern) for error_name, pattern in N2_PATTERNS.items()
|
||||||
|
}
|
||||||
|
|
||||||
def _match_n2_pattern(log_entry: LogEntry) -> str|None:
|
|
||||||
|
def _match_n2_pattern(log_entry: LogEntry) -> str | None:
|
||||||
for error_code, pattern in _COMPILED_N2_PATTERNS.items():
|
for error_code, pattern in _COMPILED_N2_PATTERNS.items():
|
||||||
if pattern.search(log_entry.error_message):
|
if pattern.search(log_entry.error_message):
|
||||||
return error_code
|
return error_code
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def classify_log_entries(log_entries: list[LogEntry]) -> tuple[list[LogEntry], list[LogEntry]]:
|
|
||||||
|
def classify_log_entries(
|
||||||
|
log_entries: list[LogEntry],
|
||||||
|
) -> tuple[list[LogEntry], list[LogEntry]]:
|
||||||
"""
|
"""
|
||||||
Separate logs that require N2 intervention from those that don't.
|
Separate logs that require N2 intervention from those that don't.
|
||||||
"""
|
"""
|
||||||
relevant_log_entries = []
|
relevant_log_entries = []
|
||||||
irrelevant_log_entries = []
|
irrelevant_log_entries = []
|
||||||
|
|
||||||
|
logger.info("\tclassification job started.")
|
||||||
for log_entry in log_entries:
|
for log_entry in log_entries:
|
||||||
error_name = _match_n2_pattern(log_entry)
|
error_name = _match_n2_pattern(log_entry)
|
||||||
if error_name:
|
if error_name:
|
||||||
|
logger.info(
|
||||||
|
f"\t\tError '{error_name}' associated with log entry '{log_entry.error_message}'. This is a n2 log entry"
|
||||||
|
)
|
||||||
log_entry.error_name = error_name
|
log_entry.error_name = error_name
|
||||||
relevant_log_entries.append(log_entry)
|
relevant_log_entries.append(log_entry)
|
||||||
else:
|
else:
|
||||||
irrelevant_log_entries.append(log_entry)
|
irrelevant_log_entries.append(log_entry)
|
||||||
|
|
||||||
return relevant_log_entries, irrelevant_log_entries
|
logger.info(
|
||||||
|
f"\t\tn2 logs: {len(relevant_log_entries)}, other logs: {len(irrelevant_log_entries)}"
|
||||||
|
)
|
||||||
|
logger.info("\tclassification job completed.")
|
||||||
|
return relevant_log_entries, irrelevant_log_entries
|
||||||
|
|||||||
@@ -97,25 +97,27 @@ def cli(
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
logger.info("logwatcher starts its work.")
|
logger.info("============ logwatcher started ============")
|
||||||
if input_files and input_dir:
|
if input_files and input_dir:
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter(
|
||||||
"Only one of input_files or input_dir must be provided."
|
"Only one of input_files or input_dir must be provided."
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_files:
|
if input_files:
|
||||||
logger.info(f"input_files option selected.\n input_files: '{input_files}'")
|
logger.info(f"input_files option selected. input_files: {input_files}")
|
||||||
log_files = input_files
|
log_files = input_files
|
||||||
elif input_dir:
|
elif input_dir:
|
||||||
logger.info(f"input_dir option selected with value '{input_dir}'")
|
logger.info(f"input_dir option selected with value {input_dir}")
|
||||||
log_files = _get_files_from_dir(input_dir)
|
log_files = _get_files_from_dir(input_dir)
|
||||||
else:
|
else:
|
||||||
raise typer.BadParameter("Either input_files or input_dir must be provided")
|
raise typer.BadParameter("Either input_files or input_dir must be provided")
|
||||||
|
|
||||||
|
logger.info(f"reports will be located at '{output_dir.cwd()}'")
|
||||||
|
|
||||||
log_entries = _files_to_log_entries(log_files)
|
log_entries = _files_to_log_entries(log_files)
|
||||||
relevant, irrelevant = classify_log_entries(log_entries)
|
relevant, irrelevant = classify_log_entries(log_entries)
|
||||||
write_log_report(relevant, irrelevant, len(log_files), output_dir)
|
write_log_report(relevant, irrelevant, len(log_files), output_dir)
|
||||||
logger.info("logwatcher has finished its task.")
|
logger.info("============ logwatcher ended ============")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+29
-24
@@ -36,53 +36,58 @@ effective_file_path = Path.cwd() / "output/effective.log"
|
|||||||
def parse_log_line(log_line: str) -> LogEntry | None:
|
def parse_log_line(log_line: str) -> LogEntry | None:
|
||||||
"""Parse a raw log line into a LogEntry object.
|
"""Parse a raw log line into a LogEntry object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_line: a line in a log file received by N2 technicians
|
log_line: a line in a log file received by N2 technicians
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LogEntry if the line is a valid line, None if
|
LogEntry if the line is a valid line, None if
|
||||||
it is empty or not a valid line.
|
it is empty or not a valid line.
|
||||||
"""
|
"""
|
||||||
match = LOG_PATTERN.search(log_line)
|
match = LOG_PATTERN.search(log_line)
|
||||||
if not match:
|
if not match:
|
||||||
logger.debug(f"Unrecognize format. Ignoring line {log_line}.")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = match.groupdict()
|
data = match.groupdict()
|
||||||
return LogEntry(
|
return LogEntry(
|
||||||
server_ip=data["server_ip"],
|
server_ip=data["server_ip"],
|
||||||
mdc_server_name=data["mdc_server_name"],
|
mdc_server_name=data["mdc_server_name"],
|
||||||
start_time=datetime.strptime(data["start_time"], DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE),
|
start_time=datetime.strptime(data["start_time"], DATETIME_FORMAT).astimezone(
|
||||||
|
FRENCH_TIMEZONE
|
||||||
|
),
|
||||||
store_name=data["store_name"],
|
store_name=data["store_name"],
|
||||||
error_time=datetime.strptime(data["error_time"], DATETIME_FORMAT).astimezone(FRENCH_TIMEZONE),
|
error_time=datetime.strptime(data["error_time"], DATETIME_FORMAT).astimezone(
|
||||||
|
FRENCH_TIMEZONE
|
||||||
|
),
|
||||||
error_message=data["error_message"],
|
error_message=data["error_message"],
|
||||||
raw_line=log_line
|
raw_line=log_line,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_log_file(log_file_path: Path) -> list[LogEntry]:
|
def parse_log_file(log_file_path: Path) -> list[LogEntry]:
|
||||||
"""Transforms the content of a log file into a list of LogEntry.
|
"""Transforms the content of a log file into a list of LogEntry.
|
||||||
Uses `parse_log_line` to parse each line of the choosen log file.
|
Uses `parse_log_line` to parse each line of the choosen log file.
|
||||||
Raise exception if a log line encounters an error.
|
Raise exception if a log line encounters an error.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_file_path: Path of a log file
|
log_file_path: Path of a log file
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of LogEntry
|
A list of LogEntry
|
||||||
"""
|
"""
|
||||||
log_entry_list: list[LogEntry] = []
|
log_entry_list: list[LogEntry] = []
|
||||||
|
|
||||||
with open(log_file_path, 'r', encoding="windows-1252") as log_file:
|
with open(log_file_path, "r", encoding="windows-1252") as log_file:
|
||||||
|
logger.info(f"\tparsing log file '{log_file_path.name}' started.")
|
||||||
for index, log_line in enumerate(log_file):
|
for index, log_line in enumerate(log_file):
|
||||||
try:
|
try:
|
||||||
log_entry = parse_log_line(log_line)
|
log_entry = parse_log_line(log_line)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
logger.error(f"Error while parsing the log file {log_file.name} at {log_line}: {error}")
|
logger.error(
|
||||||
|
f"\t\tline {index}: error while parsing the log file '{log_file.name}': {error}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if log_entry:
|
if log_entry:
|
||||||
log_entry_list.append(log_entry)
|
log_entry_list.append(log_entry)
|
||||||
logger.debug(f"Line {index} was successfully parsed.")
|
logger.info(f"\t\t{len(log_entry_list)} lines were parsed.")
|
||||||
else:
|
logger.info("\tparsing completed.")
|
||||||
logger.debug(f"Line {index} is empty. Skip.")
|
return log_entry_list
|
||||||
|
|
||||||
return log_entry_list
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
|
from logwatcher import logger
|
||||||
from logwatcher.config import DATETIME_FORMAT, RESULT_PATH
|
from logwatcher.config import DATETIME_FORMAT, RESULT_PATH
|
||||||
from logwatcher.models import LogEntry
|
from logwatcher.models import LogEntry
|
||||||
|
|
||||||
@@ -59,15 +60,30 @@ def write_log_report(
|
|||||||
file.
|
file.
|
||||||
Use the range date of relevant and irrelevant lists to get the period of time
|
Use the range date of relevant and irrelevant lists to get the period of time
|
||||||
the logs were generated.
|
the logs were generated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
relevant: List of n2 log entry
|
||||||
|
irrelevant: List of not n2 log entry
|
||||||
|
nb_files: Number of files scanned
|
||||||
|
output_dir: Location where all reports will be written
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
"""
|
"""
|
||||||
|
logger.info("\twriting reports job started.")
|
||||||
start_date, end_date = _get_period(relevant + irrelevant)
|
start_date, end_date = _get_period(relevant + irrelevant)
|
||||||
|
logger.info(f"\t\tperiod found: {start_date} -> {end_date}")
|
||||||
|
|
||||||
reports_dict = build_reports(relevant, irrelevant, start_date, end_date, nb_files)
|
reports_dict = build_reports(relevant, irrelevant, start_date, end_date, nb_files)
|
||||||
|
|
||||||
for target in reports_dict:
|
for target in reports_dict:
|
||||||
|
logger.info(f"\t\tgenerating report '{target}'...")
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.debug(f"\t\t\t{output_dir} exists.")
|
||||||
target_file = output_dir / str(target + ".log")
|
target_file = output_dir / str(target + ".log")
|
||||||
target_file.write_text(reports_dict[target], encoding="windows-1252")
|
target_file.write_text(reports_dict[target], encoding="windows-1252")
|
||||||
|
logger.debug(f"\t\t\treport was written in {target_file.name}")
|
||||||
|
logger.info(f"\t\treport '{target}' was generated.")
|
||||||
|
logger.info("\twriting reports job completed.")
|
||||||
|
|
||||||
|
|
||||||
def build_reports(
|
def build_reports(
|
||||||
@@ -77,7 +93,17 @@ def build_reports(
|
|||||||
Build the three output reports: n2, other, and all.
|
Build the three output reports: n2, other, and all.
|
||||||
"all" report contains both "n2" and "other" sections.
|
"all" report contains both "n2" and "other" sections.
|
||||||
"n2" and "other" are two unique sections.
|
"n2" and "other" are two unique sections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
relevant: List of n2 log entry
|
||||||
|
irrelevant: List of not n2 log entry
|
||||||
|
start_date: Date of the oldest log entry in relevant + irrelevant list
|
||||||
|
end_date: Date of the newest log entry in relevant + irrelevant list
|
||||||
|
nb_files: number of files scanned
|
||||||
|
Returns:
|
||||||
|
Dictionary of reports in string format
|
||||||
"""
|
"""
|
||||||
|
logger.info("\t\tbuilding reports job started.")
|
||||||
relevant_report = _render_target_report(
|
relevant_report = _render_target_report(
|
||||||
log_entries=relevant, target_template=N2_SUPPORT_TEMPLATE
|
log_entries=relevant, target_template=N2_SUPPORT_TEMPLATE
|
||||||
)
|
)
|
||||||
@@ -85,6 +111,8 @@ def build_reports(
|
|||||||
irrelevant_report = _render_target_report(
|
irrelevant_report = _render_target_report(
|
||||||
log_entries=irrelevant, target_template=OTHER_TEMPLATE
|
log_entries=irrelevant, target_template=OTHER_TEMPLATE
|
||||||
)
|
)
|
||||||
|
logger.info("\t\t\tn2 and other reports were generated.")
|
||||||
|
logger.info("\t\tbuilding reports job completed.")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"n2": BASE_TEMPLATE.substitute(
|
"n2": BASE_TEMPLATE.substitute(
|
||||||
@@ -112,11 +140,19 @@ def build_reports(
|
|||||||
|
|
||||||
|
|
||||||
# build templates
|
# build templates
|
||||||
def _render_target_report(log_entries: list[LogEntry], target_template) -> str:
|
def _render_target_report(
|
||||||
|
log_entries: list[LogEntry], target_template: Template
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Render report using either N2_SUPPORT_TEMPLATE or OTHER_TEMPLATE.
|
Render report using either N2_SUPPORT_TEMPLATE or OTHER_TEMPLATE.
|
||||||
Call _render_entries() and include rendered log entries in the
|
Call _render_entries() and include rendered log entries in the
|
||||||
new report.
|
new report.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_entries: List of log entries
|
||||||
|
target_template: Template to use for N2_SUPPORT_TEMPLATE
|
||||||
|
Returns:
|
||||||
|
content of report in string format
|
||||||
"""
|
"""
|
||||||
error_list = _render_entries(log_entries)
|
error_list = _render_entries(log_entries)
|
||||||
return target_template.substitute(nb_errors=len(log_entries), error_list=error_list)
|
return target_template.substitute(nb_errors=len(log_entries), error_list=error_list)
|
||||||
@@ -126,6 +162,11 @@ def _render_entries(log_entries: list[LogEntry]) -> str:
|
|||||||
"""
|
"""
|
||||||
Render a list of log entries using the `ERROR_TEMPLATE`.
|
Render a list of log entries using the `ERROR_TEMPLATE`.
|
||||||
Used in generated report files in the `output` directory
|
Used in generated report files in the `output` directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_entries: List of log entries
|
||||||
|
Returns:
|
||||||
|
error report in string format
|
||||||
"""
|
"""
|
||||||
error_list = []
|
error_list = []
|
||||||
for index, log_entry in enumerate(log_entries, start=1):
|
for index, log_entry in enumerate(log_entries, start=1):
|
||||||
|
|||||||
Reference in New Issue
Block a user