Merge pull request #13618 from LabNConsulting/chopps/fixlogging

This commit is contained in:
Martin Winter 2023-06-06 02:35:20 -07:00 committed by GitHub
commit 6a771192de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 165 additions and 99 deletions

View File

@ -4,6 +4,7 @@ Topotest conftest.py file.
""" """
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
import contextlib
import glob import glob
import logging import logging
import os import os
@ -12,6 +13,7 @@ import resource
import subprocess import subprocess
import sys import sys
import time import time
from pathlib import Path
import lib.fixtures import lib.fixtures
import pytest import pytest
@ -41,6 +43,30 @@ except (AttributeError, ImportError):
pass pass
# Remove this and use munet version when we move to pytest_asyncio
@contextlib.contextmanager
def chdir(ndir, desc=""):
odir = os.getcwd()
os.chdir(ndir)
if desc:
logging.debug("%s: chdir from %s to %s", desc, odir, ndir)
try:
yield
finally:
if desc:
logging.debug("%s: chdir back from %s to %s", desc, ndir, odir)
os.chdir(odir)
@contextlib.contextmanager
def log_handler(basename, logpath):
topolog.logstart(basename, logpath)
try:
yield
finally:
topolog.logfinish(basename, logpath)
def pytest_addoption(parser): def pytest_addoption(parser):
""" """
Add topology-only option to the topology tester. This option makes pytest Add topology-only option to the topology tester. This option makes pytest
@ -271,6 +297,20 @@ def check_for_memleaks():
pytest.fail("memleaks found for daemons: " + " ".join(daemons)) pytest.fail("memleaks found for daemons: " + " ".join(daemons))
@pytest.fixture(autouse=True, scope="module")
def module_autouse(request):
basename = get_test_logdir(request.node.nodeid, True)
logdir = Path(topotest.g_pytest_config.option.rundir) / basename
logpath = logdir / "exec.log"
subprocess.check_call("mkdir -p -m 1777 {}".format(logdir), shell=True)
with log_handler(basename, logpath):
sdir = os.path.dirname(os.path.realpath(request.fspath))
with chdir(sdir, "module autouse fixture"):
yield
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")
def module_check_memtest(request): def module_check_memtest(request):
yield yield
@ -282,14 +322,19 @@ def module_check_memtest(request):
check_for_memleaks() check_for_memleaks()
def pytest_runtest_logstart(nodeid, location): #
# location is (filename, lineno, testname) # Disable per test function logging as FRR CI system can't handle it.
topolog.logstart(nodeid, location, topotest.g_pytest_config.option.rundir) #
# @pytest.fixture(autouse=True, scope="function")
# def function_autouse(request):
def pytest_runtest_logfinish(nodeid, location): # # For tests we actually use the logdir name as the logfile base
# location is (filename, lineno, testname) # logbase = get_test_logdir(nodeid=request.node.nodeid, module=False)
topolog.logfinish(nodeid, location) # logbase = os.path.join(topotest.g_pytest_config.option.rundir, logbase)
# logpath = Path(logbase)
# path = Path(f"{logpath.parent}/exec-{logpath.name}.log")
# subprocess.check_call("mkdir -p -m 1777 {}".format(logpath.parent), shell=True)
# with log_handler(request.node.nodeid, path):
# yield
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
@ -340,8 +385,10 @@ def pytest_configure(config):
os.environ["PYTEST_TOPOTEST_WORKER"] = "" os.environ["PYTEST_TOPOTEST_WORKER"] = ""
is_xdist = os.environ["PYTEST_XDIST_MODE"] != "no" is_xdist = os.environ["PYTEST_XDIST_MODE"] != "no"
is_worker = False is_worker = False
wname = ""
else: else:
os.environ["PYTEST_TOPOTEST_WORKER"] = os.environ["PYTEST_XDIST_WORKER"] wname = os.environ["PYTEST_XDIST_WORKER"]
os.environ["PYTEST_TOPOTEST_WORKER"] = wname
is_xdist = True is_xdist = True
is_worker = True is_worker = True
@ -375,6 +422,16 @@ def pytest_configure(config):
if not config.getoption("--log-file") and not config.getini("log_file"): if not config.getoption("--log-file") and not config.getini("log_file"):
config.option.log_file = os.path.join(rundir, "exec.log") config.option.log_file = os.path.join(rundir, "exec.log")
# Handle pytest-xdist each worker get's it's own top level log file
# `exec-worker-N.log`
if wname:
wname = wname.replace("gw", "worker-")
cpath = Path(config.option.log_file).absolute()
config.option.log_file = f"{cpath.parent}/{cpath.stem}-{wname}{cpath.suffix}"
elif is_xdist:
cpath = Path(config.option.log_file).absolute()
config.option.log_file = f"{cpath.parent}/{cpath.stem}-xdist{cpath.suffix}"
# Turn on live logging if user specified verbose and the config has a CLI level set # Turn on live logging if user specified verbose and the config has a CLI level set
if config.getoption("--verbose") and not is_xdist and not config.getini("log_cli"): if config.getoption("--verbose") and not is_xdist and not config.getini("log_cli"):
if config.getoption("--log-cli-level", None) is None: if config.getoption("--log-cli-level", None) is None:
@ -433,6 +490,10 @@ def pytest_configure(config):
@pytest.fixture(autouse=True, scope="session") @pytest.fixture(autouse=True, scope="session")
def setup_session_auto(): def setup_session_auto():
# Aligns logs nicely
logging.addLevelName(logging.WARNING, " WARN")
logging.addLevelName(logging.INFO, " INFO")
if "PYTEST_TOPOTEST_WORKER" not in os.environ: if "PYTEST_TOPOTEST_WORKER" not in os.environ:
is_worker = False is_worker = False
elif not os.environ["PYTEST_TOPOTEST_WORKER"]: elif not os.environ["PYTEST_TOPOTEST_WORKER"]:

View File

@ -121,7 +121,7 @@ class Mininet(BaseMunet):
g_mnet_inst = None g_mnet_inst = None
def __init__(self, rundir=None, pytestconfig=None): def __init__(self, rundir=None, pytestconfig=None, logger=None):
""" """
Create a Micronet. Create a Micronet.
""" """
@ -140,7 +140,7 @@ class Mininet(BaseMunet):
# os.umask(0) # os.umask(0)
super(Mininet, self).__init__( super(Mininet, self).__init__(
pid=False, rundir=rundir, pytestconfig=pytestconfig pid=False, rundir=rundir, pytestconfig=pytestconfig, logger=logger
) )
# From munet/munet/native.py # From munet/munet/native.py

View File

@ -84,7 +84,7 @@ def get_exabgp_cmd(commander=None):
"""Return the command to use for ExaBGP version < 4.""" """Return the command to use for ExaBGP version < 4."""
if commander is None: if commander is None:
commander = Commander("topogen") commander = Commander("exabgp", logger=logging.getLogger("exabgp"))
def exacmd_version_ok(exacmd): def exacmd_version_ok(exacmd):
logger.debug("checking %s for exabgp < version 4", exacmd) logger.debug("checking %s for exabgp < version 4", exacmd)
@ -107,7 +107,7 @@ def get_exabgp_cmd(commander=None):
exacmd = py2_path + " -m exabgp" exacmd = py2_path + " -m exabgp"
if exacmd_version_ok(exacmd): if exacmd_version_ok(exacmd):
return exacmd return exacmd
py2_path = commander.get_exec_path("python") py2_path = commander.get_exec_path("python")
if py2_path: if py2_path:
exacmd = py2_path + " -m exabgp" exacmd = py2_path + " -m exabgp"
if exacmd_version_ok(exacmd): if exacmd_version_ok(exacmd):
@ -209,7 +209,11 @@ class Topogen(object):
# Mininet(Micronet) to build the actual topology. # Mininet(Micronet) to build the actual topology.
assert not inspect.isclass(topodef) assert not inspect.isclass(topodef)
self.net = Mininet(rundir=self.logdir, pytestconfig=topotest.g_pytest_config) self.net = Mininet(
rundir=self.logdir,
pytestconfig=topotest.g_pytest_config,
logger=topolog.get_logger("mu", log_level="debug"),
)
# Adjust the parent namespace # Adjust the parent namespace
topotest.fix_netns_limits(self.net) topotest.fix_netns_limits(self.net)
@ -1090,8 +1094,9 @@ class TopoSwitch(TopoGear):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
def __init__(self, tgen, name, **params): def __init__(self, tgen, name, **params):
logger = topolog.get_logger(name, log_level="debug")
super(TopoSwitch, self).__init__(tgen, name, **params) super(TopoSwitch, self).__init__(tgen, name, **params)
tgen.net.add_switch(name) tgen.net.add_switch(name, logger=logger)
def __str__(self): def __str__(self):
gear = super(TopoSwitch, self).__str__() gear = super(TopoSwitch, self).__str__()

View File

@ -15,13 +15,6 @@ This file defines our logging abstraction.
import logging import logging
import os import os
import subprocess
import sys
if sys.version_info[0] > 2:
pass
else:
pass
try: try:
from xdist import is_xdist_controller from xdist import is_xdist_controller
@ -31,8 +24,6 @@ except ImportError:
return False return False
BASENAME = "topolog"
# Helper dictionary to convert Topogen logging levels to Python's logging. # Helper dictionary to convert Topogen logging levels to Python's logging.
DEBUG_TOPO2LOGGING = { DEBUG_TOPO2LOGGING = {
"debug": logging.DEBUG, "debug": logging.DEBUG,
@ -42,13 +33,43 @@ DEBUG_TOPO2LOGGING = {
"error": logging.ERROR, "error": logging.ERROR,
"critical": logging.CRITICAL, "critical": logging.CRITICAL,
} }
FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s: %(name)s: %(message)s" FORMAT = "%(asctime)s %(levelname)s: %(name)s: %(message)s"
handlers = {} handlers = {}
logger = logging.getLogger("topolog") logger = logging.getLogger("topo")
def set_handler(l, target=None): # Remove this and use munet version when we move to pytest_asyncio
def get_test_logdir(nodeid=None, module=False):
"""Get log directory relative pathname."""
xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "")
mode = os.getenv("PYTEST_XDIST_MODE", "no")
# nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running
# may be missing "::testname" if module is True
if not nodeid:
nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
cur_test = nodeid.replace("[", "_").replace("]", "_")
if module:
idx = cur_test.rfind("::")
path = cur_test if idx == -1 else cur_test[:idx]
testname = ""
else:
path, testname = cur_test.split("::")
testname = testname.replace("/", ".")
path = path[:-3].replace("/", ".")
# We use different logdir paths based on how xdist is running.
if mode == "each":
if module:
return os.path.join(path, "worker-logs", xdist_worker)
return os.path.join(path, testname, xdist_worker)
assert mode in ("no", "load", "loadfile", "loadscope"), f"Unknown dist mode {mode}"
return path if module else os.path.join(path, testname)
def set_handler(lg, target=None):
if target is None: if target is None:
h = logging.NullHandler() h = logging.NullHandler()
else: else:
@ -59,106 +80,81 @@ def set_handler(l, target=None):
h.setFormatter(logging.Formatter(fmt=FORMAT)) h.setFormatter(logging.Formatter(fmt=FORMAT))
# Don't filter anything at the handler level # Don't filter anything at the handler level
h.setLevel(logging.DEBUG) h.setLevel(logging.DEBUG)
l.addHandler(h) lg.addHandler(h)
return h return h
def set_log_level(l, level): def set_log_level(lg, level):
"Set the logging level." "Set the logging level."
# Messages sent to this logger only are created if this level or above. # Messages sent to this logger only are created if this level or above.
log_level = DEBUG_TOPO2LOGGING.get(level, level) log_level = DEBUG_TOPO2LOGGING.get(level, level)
l.setLevel(log_level) lg.setLevel(log_level)
def get_logger(name, log_level=None, target=None): def reset_logger(lg):
l = logging.getLogger("{}.{}".format(BASENAME, name)) while lg.handlers:
x = lg.handlers.pop()
x.close()
lg.removeHandler(x)
def get_logger(name, log_level=None, target=None, reset=True):
lg = logging.getLogger(name)
if reset:
reset_logger(lg)
if log_level is not None: if log_level is not None:
set_log_level(l, log_level) set_log_level(lg, log_level)
if target is not None: if target is not None:
set_handler(l, target) set_handler(lg, target)
return l return lg
# nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running def logstart(nodeid, logpath):
def get_test_logdir(nodeid=None):
"""Get log directory relative pathname."""
xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "")
mode = os.getenv("PYTEST_XDIST_MODE", "no")
if not nodeid:
nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
cur_test = nodeid.replace("[", "_").replace("]", "_")
path, testname = cur_test.split("::")
path = path[:-3].replace("/", ".")
# We use different logdir paths based on how xdist is running.
if mode == "each":
return os.path.join(path, testname, xdist_worker)
elif mode == "load":
return os.path.join(path, testname)
else:
assert (
mode == "no" or mode == "loadfile" or mode == "loadscope"
), "Unknown dist mode {}".format(mode)
return path
def logstart(nodeid, location, rundir):
"""Called from pytest before module setup.""" """Called from pytest before module setup."""
mode = os.getenv("PYTEST_XDIST_MODE", "no")
worker = os.getenv("PYTEST_TOPOTEST_WORKER", "") worker = os.getenv("PYTEST_TOPOTEST_WORKER", "")
wstr = f" on worker {worker}" if worker else ""
# We only per-test log in the workers (or non-dist)
if not worker and mode != "no":
return
handler_id = nodeid + worker handler_id = nodeid + worker
assert handler_id not in handlers logpath = logpath.absolute()
rel_log_dir = get_test_logdir(nodeid) logging.debug("logstart: adding logging for %s%s at %s", nodeid, wstr, logpath)
exec_log_dir = os.path.join(rundir, rel_log_dir) root_logger = logging.getLogger()
subprocess.check_call( handler = logging.FileHandler(logpath, mode="w")
"mkdir -p {0} && chmod 1777 {0}".format(exec_log_dir), shell=True handler.setFormatter(logging.Formatter(FORMAT))
)
exec_log_path = os.path.join(exec_log_dir, "exec.log")
# Add test based exec log handler root_logger.addHandler(handler)
h = set_handler(logger, exec_log_path) handlers[handler_id] = handler
handlers[handler_id] = h
if worker: logging.debug("logstart: added logging for %s%s at %s", nodeid, wstr, logpath)
logger.info( return handler
"Logging on worker %s for %s into %s", worker, handler_id, exec_log_path
)
else:
logger.info("Logging for %s into %s", handler_id, exec_log_path)
def logfinish(nodeid, location): def logfinish(nodeid, logpath):
"""Called from pytest after module teardown.""" """Called from pytest after module teardown."""
# This function may not be called if pytest is interrupted.
worker = os.getenv("PYTEST_TOPOTEST_WORKER", "") worker = os.getenv("PYTEST_TOPOTEST_WORKER", "")
wstr = f" on worker {worker}" if worker else ""
root_logger = logging.getLogger()
handler_id = nodeid + worker handler_id = nodeid + worker
if handler_id in handlers: if handler_id not in handlers:
# Remove test based exec log handler logging.critical("can't find log handler to remove")
if worker: else:
logger.info("Closing logs for %s", handler_id) logging.debug(
"logfinish: removing logging for %s%s at %s", nodeid, wstr, logpath
)
h = handlers[handler_id] h = handlers[handler_id]
logger.removeHandler(handlers[handler_id]) root_logger.removeHandler(h)
h.flush() h.flush()
h.close() h.close()
del handlers[handler_id] del handlers[handler_id]
logging.debug(
"logfinish: removed logging for %s%s at %s", nodeid, wstr, logpath
)
console_handler = set_handler(logger, None) console_handler = set_handler(logger, None)

View File

@ -24,6 +24,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import logging
from collections.abc import Mapping from collections.abc import Mapping
from copy import deepcopy from copy import deepcopy
@ -38,7 +39,7 @@ g_pytest_config = None
def get_logs_path(rundir): def get_logs_path(rundir):
logspath = topolog.get_test_logdir() logspath = topolog.get_test_logdir(module=True)
return os.path.join(rundir, logspath) return os.path.join(rundir, logspath)
@ -1137,7 +1138,9 @@ def _sysctl_assure(commander, variable, value):
def sysctl_atleast(commander, variable, min_value, raises=False): def sysctl_atleast(commander, variable, min_value, raises=False):
try: try:
if commander is None: if commander is None:
commander = micronet.Commander("topotest") logger = logging.getLogger("topotest")
commander = micronet.Commander("sysctl", logger=logger)
return _sysctl_atleast(commander, variable, min_value) return _sysctl_atleast(commander, variable, min_value)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
logger.warning( logger.warning(
@ -1153,7 +1156,8 @@ def sysctl_atleast(commander, variable, min_value, raises=False):
def sysctl_assure(commander, variable, value, raises=False): def sysctl_assure(commander, variable, value, raises=False):
try: try:
if commander is None: if commander is None:
commander = micronet.Commander("topotest") logger = logging.getLogger("topotest")
commander = micronet.Commander("sysctl", logger=logger)
return _sysctl_assure(commander, variable, value) return _sysctl_assure(commander, variable, value)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
logger.warning( logger.warning(