mirror of https://github.com/mongodb/mongo
359 lines
12 KiB
Python
359 lines
12 KiB
Python
"""Module to hold the logger instances themselves."""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
from buildscripts.resmokelib import config
|
|
from buildscripts.resmokelib.core import redirect as redirect_lib
|
|
from buildscripts.resmokelib.logging import formatters
|
|
from buildscripts.resmokelib.logging.handlers import (
|
|
BufferedFileHandler,
|
|
ExceptionExtractionHandler,
|
|
ExceptionExtractor,
|
|
Truncate,
|
|
)
|
|
|
|
_DEFAULT_FORMAT = "[%(name)s] %(message)s"
|
|
|
|
# Executor logger logs information from the testing infrastructure.
|
|
EXECUTOR_LOGGER_NAME = "executor"
|
|
|
|
# Fixture logger captures the log output of server fixtures.
|
|
FIXTURE_LOGGER_NAME = "fixture"
|
|
|
|
# Test logger logs output from actual client-side tests.
|
|
TESTS_LOGGER_NAME = "tests"
|
|
|
|
ROOT_EXECUTOR_LOGGER = None
|
|
ROOT_FIXTURE_LOGGER = None
|
|
ROOT_TESTS_LOGGER = None
|
|
|
|
# Maps job nums to fixture loggers.
|
|
_FIXTURE_LOGGER_REGISTRY: dict = {}
|
|
|
|
# URL of parsley logs.
|
|
RAW_TEST_LOGS_URL = "https://evergreen.mongodb.com/rest/v2/tasks/{task_id}/build/TestLogs/job{job_num}%2F{test_id}.log?execution={execution}&print_time=true"
|
|
RAW_JOBS_LOGS_URL = "https://evergreen.mongodb.com/rest/v2/tasks/{task_id}/build/TestLogs/job{job_num}?execution={execution}&print_time=true"
|
|
PARSLEY_JOBS_LOGS_URL = "https://parsley.mongodb.com/test/{task_id}/{execution}/job{job_num}/all"
|
|
|
|
|
|
def _setup_redirects():
|
|
redirect_cmds = []
|
|
redirects = []
|
|
if config.MRLOG:
|
|
redirect_cmds.append(config.MRLOG)
|
|
|
|
if config.USER_FRIENDLY_OUTPUT:
|
|
if not config.MRLOG and shutil.which("mrlog"):
|
|
redirect_cmds.append("mrlog")
|
|
|
|
redirect_cmds.append(["tee", config.USER_FRIENDLY_OUTPUT])
|
|
redirect_cmds.append(
|
|
[
|
|
"grep",
|
|
"-Ea",
|
|
r"Summary of|Running.*\.\.\.|invariant|fassert|BACKTRACE|Invalid access|Workload\(s\) started|Workload\(s\)|WiredTiger error|AddressSanitizer|threads with tids|failed to load|Completed cmd|Completed stepdown",
|
|
]
|
|
)
|
|
|
|
for idx, redirect in enumerate(redirect_cmds):
|
|
# The first redirect reads from stdout. Otherwise read from the previous redirect.
|
|
stdin = sys.stdout if idx == 0 else redirects[idx - 1].get_stdout()
|
|
# The last redirect writes back to real stdout file descriptor.
|
|
stdout = sys.__stdout__ if idx + 1 == len(redirect_cmds) else subprocess.PIPE
|
|
redirects.append(redirect_lib.Pipe(redirect, stdin, stdout))
|
|
|
|
|
|
def configure_loggers():
|
|
"""Configure the loggers and setup redirects."""
|
|
_setup_redirects()
|
|
|
|
global ROOT_TESTS_LOGGER
|
|
ROOT_TESTS_LOGGER = new_root_logger(TESTS_LOGGER_NAME)
|
|
global ROOT_FIXTURE_LOGGER
|
|
ROOT_FIXTURE_LOGGER = new_root_logger(FIXTURE_LOGGER_NAME)
|
|
global ROOT_EXECUTOR_LOGGER
|
|
ROOT_EXECUTOR_LOGGER = new_root_logger(EXECUTOR_LOGGER_NAME)
|
|
|
|
_write_evergreen_log_spec()
|
|
|
|
|
|
def get_evergreen_log_name(job_num, test_id=None):
|
|
"""Return the log name, relative to the reserved test log directory, on the Evergreen task host."""
|
|
return f"job{job_num}/" + (f"{test_id}.log" if test_id else "global.log")
|
|
|
|
|
|
def new_root_logger(name):
|
|
"""
|
|
Create and configure a new root logger.
|
|
|
|
:param name: The name of the new root logger.
|
|
"""
|
|
if name not in config.LOGGING_CONFIG:
|
|
raise ValueError("Logging configuration should contain the %s component" % name)
|
|
|
|
logger = logging.getLogger(name)
|
|
logger.setLevel(logging.DEBUG)
|
|
logger_info = config.LOGGING_CONFIG[name]
|
|
formatter = _get_formatter(logger_info)
|
|
|
|
for handler_info in logger_info.get("handlers", []):
|
|
_add_handler(logger, handler_info, formatter)
|
|
|
|
return logger
|
|
|
|
|
|
def new_resmoke_logger():
|
|
"""Create a child logger of the executor logger with the name "resmoke"."""
|
|
logger = logging.Logger("resmoke")
|
|
logger.parent = ROOT_EXECUTOR_LOGGER
|
|
return logger
|
|
|
|
|
|
def new_job_logger(test_kind, job_num) -> logging.Logger:
|
|
"""Create a new logger for a given job thread."""
|
|
name = "executor:%s:job%d" % (test_kind, job_num)
|
|
logger = logging.Logger(name)
|
|
logger.parent = ROOT_EXECUTOR_LOGGER
|
|
|
|
return logger
|
|
|
|
|
|
# Fixture loggers
|
|
|
|
|
|
class FixtureLogger(logging.Logger):
|
|
"""Custom fixture logger."""
|
|
|
|
def __init__(self, name, full_name, external_sut_hostname=None):
|
|
"""Initialize fixture logger."""
|
|
self.full_name = full_name
|
|
self.external_sut_hostname = external_sut_hostname
|
|
super().__init__(name)
|
|
|
|
|
|
def new_fixture_logger(fixture_class, job_num):
|
|
"""Create a logger for a particular fixture class."""
|
|
full_name = "%s:job%d" % (fixture_class, job_num)
|
|
external_sut_hostname = full_name.replace(":", "_").lower()
|
|
logger = FixtureLogger(_shorten(full_name), full_name, external_sut_hostname)
|
|
logger.parent = ROOT_FIXTURE_LOGGER
|
|
_add_evergreen_handler(logger, job_num)
|
|
|
|
_FIXTURE_LOGGER_REGISTRY[job_num] = logger
|
|
return logger
|
|
|
|
|
|
def new_fixture_node_logger(fixture_class, job_num, node_name):
|
|
"""Create a logger for a particular element in a multi-process fixture."""
|
|
full_name = "%s:job%d:%s" % (fixture_class, job_num, node_name)
|
|
external_sut_hostname = node_name.replace(":", "_")
|
|
logger = FixtureLogger(_shorten(full_name), full_name, external_sut_hostname)
|
|
logger.parent = _FIXTURE_LOGGER_REGISTRY[job_num]
|
|
return logger
|
|
|
|
|
|
# Test loggers
|
|
|
|
|
|
def new_testqueue_logger(test_kind):
|
|
"""Create a new test queue logger that associates the test kind to tests."""
|
|
logger = logging.Logger(name=test_kind)
|
|
logger.parent = ROOT_TESTS_LOGGER
|
|
return logger
|
|
|
|
|
|
def configure_exception_capture(test_logger):
|
|
"""Configure the test logger to extract exceptions and return the exception extractors."""
|
|
|
|
# Capturing exceptions only makes sense for logs in plain format.
|
|
if config.LOG_FORMAT != "plain":
|
|
return []
|
|
|
|
js_exception = ExceptionExtractor(
|
|
start_regex=r"^uncaught exception:",
|
|
end_regex=r"^exiting with code",
|
|
truncate=Truncate.LAST,
|
|
)
|
|
py_exception = ExceptionExtractor(
|
|
start_regex=r"^Traceback",
|
|
end_regex=r"^\S*:",
|
|
truncate=Truncate.FIRST,
|
|
)
|
|
|
|
# py_exception extracts Python exception messages from the stdout of a Python subprocess.
|
|
# Both it and js_exception are registered as logging.Handlers to avoid specializing which
|
|
# programming language exceptions to expect from certain kinds of tests. Python exceptions
|
|
# from resmoke hooks and fixtures are handled separately through TestReport.addError().
|
|
test_logger.addHandler(ExceptionExtractionHandler(js_exception))
|
|
test_logger.addHandler(ExceptionExtractionHandler(py_exception))
|
|
|
|
return [js_exception, py_exception]
|
|
|
|
|
|
def new_test_logger(test_shortname, test_basename, parent, job_num, test_id, job_logger):
|
|
"""Create a new test logger that will be a child of the given parent."""
|
|
name = "%s:%s" % (parent.name, test_shortname)
|
|
logger = logging.Logger(name)
|
|
logger.parent = parent
|
|
_add_evergreen_handler(logger, job_num, test_id, test_basename)
|
|
|
|
return logger
|
|
|
|
|
|
def new_test_thread_logger(parent, test_kind, thread_id, tenant_id=None):
|
|
"""Create a new test thread logger that will be the child of the given parent."""
|
|
name = (
|
|
"%s:%s:%s" % (test_kind, thread_id, tenant_id)
|
|
if tenant_id
|
|
else "%s:%s" % (test_kind, thread_id)
|
|
)
|
|
logger = logging.Logger(name)
|
|
logger.parent = parent
|
|
return logger
|
|
|
|
|
|
def new_hook_logger(hook_class, job_num):
|
|
"""Create a new hook logger from a given fixture logger."""
|
|
name = "hook:{}:job{:d}".format(hook_class, job_num)
|
|
logger = logging.Logger(name)
|
|
logger.parent = _FIXTURE_LOGGER_REGISTRY[job_num]
|
|
return logger
|
|
|
|
|
|
# Util methods
|
|
|
|
|
|
def _add_handler(logger, handler_info, formatter):
|
|
"""Add handlers to a logger based on configuration."""
|
|
handler_class = handler_info["class"]
|
|
if handler_class == "logging.FileHandler":
|
|
handler = logging.FileHandler(
|
|
filename=handler_info["filename"], mode=handler_info.get("mode", "w")
|
|
)
|
|
elif handler_class == "logging.NullHandler":
|
|
handler = logging.NullHandler()
|
|
elif handler_class == "logging.StreamHandler":
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
elif handler_class == "evergreen":
|
|
return # Evergreen handlers are applied when creating specific child loggers
|
|
else:
|
|
raise ValueError("Unknown handler class '%s'" % handler_class)
|
|
handler.setFormatter(formatter)
|
|
logger.addHandler(handler)
|
|
|
|
|
|
def _get_formatter(logger_info):
|
|
"""Return formatter."""
|
|
if config.LOG_FORMAT == "json":
|
|
# Override existing configuration with predefined formatter.
|
|
return formatters.JsonLogFormatter()
|
|
|
|
if "format" in logger_info:
|
|
log_format = logger_info["format"]
|
|
else:
|
|
log_format = _DEFAULT_FORMAT
|
|
return formatters.TimestampFormatter(fmt=log_format)
|
|
|
|
|
|
def _shorten(logger_name):
|
|
"""Modify logger name."""
|
|
# TODO: this function is only called for the fixture and fixture node loggers.
|
|
# If additional abbreviation is desired, we will need to call _shorten() in the appropriate places.
|
|
|
|
removes = config.SHORTEN_LOGGER_NAME_CONFIG.get("remove", [])
|
|
for remove in removes:
|
|
logger_name = logger_name.replace(remove, "")
|
|
|
|
replaces = config.SHORTEN_LOGGER_NAME_CONFIG.get("replace", {})
|
|
for key, value in replaces.items():
|
|
logger_name = logger_name.replace(key, value)
|
|
|
|
# remove leading and trailing colons and underscores
|
|
logger_name = re.sub(r"(^[:_]+|[:_]+$)", "", logger_name)
|
|
|
|
return logger_name
|
|
|
|
|
|
# Utility functions for Evergreen file system logging.
|
|
# See `https://docs.devprod.prod.corp.mongodb.com/evergreen/Project-Configuration/Task-Output-Directory#test-logs`
|
|
# for more information.
|
|
|
|
|
|
def _write_evergreen_log_spec():
|
|
"""Configure file system logging for Evergreen tasks."""
|
|
if not config.EVERGREEN_WORK_DIR:
|
|
return
|
|
|
|
fp = f"{_get_evergreen_log_dirname()}/log_spec.yaml"
|
|
os.makedirs(os.path.dirname(fp), exist_ok=True)
|
|
|
|
ROOT_EXECUTOR_LOGGER.info("Writing Evergreen test log spec to %s.", fp)
|
|
|
|
log_spec = {
|
|
"version": 0,
|
|
"format": "text-timestamp",
|
|
}
|
|
with open(fp, "w") as f:
|
|
yaml.dump(log_spec, f)
|
|
|
|
|
|
def _add_evergreen_handler(logger, job_num, test_id=None, test_name=None):
|
|
"""Add a new evergreen handler to a logger."""
|
|
logger_info = config.LOGGING_CONFIG[TESTS_LOGGER_NAME]
|
|
evergreen_handler_info = None
|
|
for handler_info in logger_info["handlers"]:
|
|
if handler_info["class"] == "evergreen":
|
|
evergreen_handler_info = handler_info
|
|
break
|
|
|
|
if evergreen_handler_info:
|
|
fp = f"{_get_evergreen_log_dirname()}/{get_evergreen_log_name(job_num, test_id)}"
|
|
os.makedirs(os.path.dirname(fp), exist_ok=True)
|
|
|
|
handler = BufferedFileHandler(fp)
|
|
if config.LOG_FORMAT == "json":
|
|
handler.setFormatter(formatters.EvergreenJsonLogFormatter())
|
|
else:
|
|
handler.setFormatter(
|
|
formatters.EvergreenLogFormatter(fmt=logger_info.get("format", _DEFAULT_FORMAT))
|
|
)
|
|
logger.addHandler(handler)
|
|
|
|
if test_id:
|
|
raw_url = RAW_TEST_LOGS_URL.format(
|
|
task_id=config.EVERGREEN_TASK_ID,
|
|
job_num=job_num,
|
|
test_id=test_id,
|
|
execution=config.EVERGREEN_EXECUTION,
|
|
)
|
|
ROOT_EXECUTOR_LOGGER.info("Writing output of %s to %s.", test_id, fp)
|
|
ROOT_EXECUTOR_LOGGER.info("Raw logs for %s can be viewed at %s", test_name, raw_url)
|
|
else:
|
|
parsley_url = PARSLEY_JOBS_LOGS_URL.format(
|
|
task_id=config.EVERGREEN_TASK_ID,
|
|
job_num=job_num,
|
|
execution=config.EVERGREEN_EXECUTION,
|
|
)
|
|
raw_url = RAW_JOBS_LOGS_URL.format(
|
|
task_id=config.EVERGREEN_TASK_ID,
|
|
job_num=job_num,
|
|
execution=config.EVERGREEN_EXECUTION,
|
|
)
|
|
ROOT_EXECUTOR_LOGGER.info("Writing output of job #%d to %s.", job_num, fp)
|
|
ROOT_EXECUTOR_LOGGER.info(
|
|
"Parsley logs for job #%s can be viewed at %s", job_num, parsley_url
|
|
)
|
|
ROOT_EXECUTOR_LOGGER.info("Raw logs for job #%s can be viewed at %s", job_num, raw_url)
|
|
|
|
|
|
def _get_evergreen_log_dirname():
|
|
"""Return the reserved directory for test logs on the Evergreen task host."""
|
|
return f"{config.EVERGREEN_WORK_DIR}/build/TestLogs"
|