Initial code for log analysis

- we agreed to basically throw away the old log checker (as it is
  unmaintainable)
- now: provide list of analysis to do
  * all checkers in cls_log_analysis, can be unit tested (see also next
    commit)
  * looked up dynamically to simplify adding of new tests + config
  * configured with

      <analysis>
        <services>DESCRIPTION [DESCRIPTION...]</services>
        <service>DESCRIPTION</service>
        ...
      </analysis>

    where DESCRIPTION follows service=func[=options]:
    - service is name of service to check
    - func is function to call, which receives filename
    - option are arbitary options to pass to func
    <services> is whitespace delimited (so can take multiple service
    analysis definitions)
    <service> exists to allow service definitions with whitespace
    changes that would not work in <services>
- I initially planned to check return code, but most softmodems actually
  exit with non-zero return code, so this is still TODO
- Default analyzer (checking for assertions, ...) is always run if a
  service is listed
- checks for file size
- add unit tests
This commit is contained in:
Robert Schmidt
2026-02-26 14:17:28 +01:00
parent 03f56c10e7
commit fe464ec6d0
11 changed files with 257 additions and 32 deletions
+98
View File
@@ -24,10 +24,13 @@ import re
import os
import logging
import yaml
import signal
import xml.etree.ElementTree as ET
import json
import importlib, inspect
class Analysis():
def _get_test_description(properties):
@@ -147,3 +150,98 @@ class Analysis():
logging.debug(f'\u001B[1;30;43m normalized metric {k}={valnorm} deviates by more than {dev}\u001B[0m')
success = False
return success, datalog_rt_stats
# returns tuple of (service, analyzer, option string)
def _lookupServiceAnalyzerOpt(s, analyzers):
res = s.split("=", 2)
name = res[0]
if len(res) == 1:
return res[0], analyzers["Default"], None
opt = res[2] if len(res) > 2 else None
func = res[1]
a = analyzers[func] if func in analyzers else None
return name, a, opt
# groups requested service analysis (service=func[=option]) on per service
# basis and looks up analyzer func. If log analysis is requested, will always
# run Default log analysis
def _groupServices(to_analyze):
req_analysis = {}
# get content of cls_loganalysis module, then get all analyzers (classes) in this module
mod = importlib.import_module("cls_loganalysis")
analyzers = {name:cl for name, cl in inspect.getmembers(mod, inspect.isclass)}
for req in to_analyze:
s, func, opt = _lookupServiceAnalyzerOpt(req, analyzers)
logging.debug(f"requested check '{req}' => service {s}, function {func.__name__}, options '{opt}'")
# always put default analyzer first
l = req_analysis[s] if s in req_analysis else [(analyzers["Default"], None, "default")]
if func is not analyzers["Default"]:
l.append((func, opt, req))
req_analysis[s] = l
return req_analysis
def _describe_exit_code(code):
if code > 128:
sig = code - 128
try:
return f"terminated by signal {sig} ({signal.Signals(sig).name})"
except ValueError:
return f"terminated by unknown signal {sig}"
else:
return f"custom exit code"
def AnalyzeServices(HTML, service_desc, to_analyze):
success = True
# hack: we want to give as a description "log analysis", but the description
# is set outside, so retain what was set before
orig_html_desc = HTML.desc
# group analysis on a per-service basis, then iterate
for serv, list_analysis in _groupServices(to_analyze).items():
HTML.desc = f"Log analysis for service {serv}"
if serv not in service_desc:
success = False
logging.error(f"requested service {serv} not in list of services")
HTML.CreateHtmlTestRowQueue("N/A", 'KO', ["service not detected"])
continue
# pre-initialize with return code
rc = service_desc[serv]["returncode"]
logging.info(f"analyze service {serv}: return code {rc}")
service_success = True # TODO rc == 0: too many functions (eNB, lteUE, RIC, nrUE) fail non-zero
logs = []
if rc != 0:
logs.append(f"=> return code {rc}, likely " + _describe_exit_code(rc) + " [ignored by CI]")
# skip if the file is too big
logfile = service_desc[serv]["logfile"]
filename = os.path.basename(logfile)
b = os.path.getsize(logfile)
logging.debug(f"using logfile {logfile} of size {b} bytes")
if b > 10 * 1024 * 1024:
success = False
logs.append(f"logfile too big (>10MB)")
logging.error(logs)
HTML.CreateHtmlTestRowQueue(filename, 'KO', ["\n".join(logs)])
continue
# run each analyzer with its options on the logfile
for (func, opt, desc) in list_analysis:
if func is None:
service_success = False
logging.error(f"request analysis function for desc {desc} not found")
HTML.CreateHtmlTestRowQueue(serv, 'KO', [f"no analysis function for {desc}"])
continue
result, l = func.run(logfile, opt)
logging.info(f"service {serv}: analysis with func {func}, result {result}, logs '{l}'")
service_success = service_success and result
if not result:
logs.append(f"=> {func.__name__} check (options '{opt}') FAILED")
logs.append(l)
logs = '\n'.join(logs)
if not service_success:
logging.error(l)
else:
logging.info(l)
all_funcs = ", ".join([f.__name__ for (f, _, _) in list_analysis])
HTML.CreateHtmlTestRowQueue(f"Check {all_funcs} on {filename}", 'OK' if service_success else 'KO', [logs])
success = success and service_success
HTML.desc = orig_html_desc
return success