mirror of
https://github.com/docker/docs.git
synced 2026-04-01 16:58:54 +07:00
388 lines
12 KiB
Python
388 lines
12 KiB
Python
#
|
|
# Copyright (C) 2016 Rolf Neugebauer <rolf.neugebauer@docker.com>
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
|
|
"""Logging and results reporting for the RT framework"""
|
|
|
|
# This configures the logging for the RT framework. The default is
|
|
# that everything is logged to a logfile and results are written to a
|
|
# CSV file, if a log directory is specified, and only warnings and
|
|
# higher are displayed on the console. There are also per test log
|
|
# files for easier debugging. Finally, the console log can optionally
|
|
# be redirected to a socket, provided something is listening on the
|
|
# other end.
|
|
#
|
|
# In addition to the standard log levels we register a couple of
|
|
# additional log levels:
|
|
# - LOG_LVL_STDERR: This is where stderr is logged to from commands invoked
|
|
# - LOG_LVL_STDOUT: This is where stdout is logged to from commands invoked
|
|
#
|
|
# - LOG_LVL_SKIP: Used to log the result of tests which have been skipped
|
|
# - LOG_LVL_PASS: Used to log the result of tests which pass
|
|
# - LOG_LVL_FAIL: Used to log the result of tests which fail
|
|
# - LOG_LVL_SUM: Used to log a summary of results
|
|
#
|
|
# This module deliberately does not implement a class and stores state
|
|
# in module global variables to make it easier to call it from
|
|
# anywhere in the framework without having to pass object references
|
|
# around.
|
|
|
|
|
|
import csv
|
|
import datetime
|
|
import logging
|
|
import logging.handlers
|
|
import os
|
|
import pickle
|
|
import platform
|
|
import struct
|
|
import subprocess
|
|
import time
|
|
import uuid
|
|
import sys
|
|
|
|
import rt.misc
|
|
from rt.colour import COLOUR_RESET, COLOUR_BOLD, COLOUR_FOREGROUND
|
|
from rt.colour import COLOUR_RED, COLOUR_GREEN, COLOUR_YELLOW, COLOUR_GREY
|
|
from rt.colour import COLOUR_MAGENTA
|
|
|
|
if sys.version_info < (3, 0):
|
|
import SocketServer as socketserver
|
|
else:
|
|
import socketserver
|
|
|
|
|
|
# Test return codes
|
|
TEST_RC_PASS = 0 # Test passed
|
|
TEST_RC_FAIL = 1 # Test failed
|
|
TEST_RC_SKIP = 2 # Test was skipped
|
|
TEST_RC_CANCEL = 3 # Test was cancelled
|
|
|
|
LOG_LVL_STDERR = logging.DEBUG + 5
|
|
LOG_LVL_STDOUT = logging.DEBUG + 6
|
|
|
|
LOG_LVL_SKIP = logging.WARNING + 4
|
|
LOG_LVL_PASS = logging.WARNING + 5
|
|
LOG_LVL_CANCEL = logging.WARNING + 6
|
|
LOG_LVL_FAIL = logging.WARNING + 7
|
|
LOG_LVL_SUM = logging.WARNING + 8
|
|
|
|
|
|
logger = logging.getLogger('rt')
|
|
|
|
|
|
_LVL_COLOURS = {
|
|
# Standard log levels
|
|
logging.CRITICAL: COLOUR_RESET + COLOUR_BOLD + COLOUR_RED,
|
|
logging.ERROR: COLOUR_RESET + COLOUR_RED,
|
|
logging.WARNING: COLOUR_RESET + COLOUR_YELLOW,
|
|
logging.INFO: COLOUR_RESET + COLOUR_FOREGROUND,
|
|
logging.DEBUG: COLOUR_RESET + COLOUR_GREY,
|
|
|
|
# Custom log-levels
|
|
LOG_LVL_STDERR: COLOUR_RESET + COLOUR_RED,
|
|
LOG_LVL_STDOUT: COLOUR_RESET + COLOUR_FOREGROUND,
|
|
|
|
LOG_LVL_SKIP: COLOUR_RESET + COLOUR_YELLOW,
|
|
LOG_LVL_PASS: COLOUR_RESET + COLOUR_GREEN,
|
|
LOG_LVL_CANCEL: COLOUR_RESET + COLOUR_MAGENTA,
|
|
LOG_LVL_FAIL: COLOUR_RESET + COLOUR_BOLD + COLOUR_RED,
|
|
}
|
|
|
|
|
|
# Other state
|
|
_UUID = uuid.uuid4()
|
|
ENV_RT_RESULTS = None
|
|
_START_TIME = datetime.datetime.utcnow()
|
|
_CSV_FILE = None
|
|
_CSV_WRITER = None
|
|
_SOCKET_HANDLER = None
|
|
|
|
|
|
class _ColouredFormatter(logging.Formatter):
|
|
"""Format a log messages with colour"""
|
|
|
|
# This is a bit of a hack as we use the normal formatter but
|
|
# override the level name with a colourised level name. We can
|
|
# make it a bit more elaborate, but this seems the quickest way to
|
|
# do what we want.
|
|
def format(self, record):
|
|
"""Replace the logname with a colourised version"""
|
|
if record.levelno in _LVL_COLOURS:
|
|
record.levelname = _LVL_COLOURS[record.levelno] + \
|
|
"[%-6s]" % record.levelname + COLOUR_RESET
|
|
return logging.Formatter.format(self, record)
|
|
|
|
|
|
class _LogRecvHandler(socketserver.StreamRequestHandler):
|
|
"""A simple handler for log records"""
|
|
formatter = _ColouredFormatter()
|
|
|
|
def handle(self):
|
|
"""The format is 4 Bytes followed by a pickled log record. Unpickle,
|
|
format and print."""
|
|
while True:
|
|
chunk = self.connection.recv(4)
|
|
if len(chunk) < 4:
|
|
break
|
|
slen = struct.unpack(">L", chunk)[0]
|
|
chunk = self.connection.recv(slen)
|
|
while len(chunk) < slen:
|
|
chunk = chunk + self.connection.recv(slen - len(chunk))
|
|
obj = pickle.loads(chunk)
|
|
record = logging.makeLogRecord(obj)
|
|
# run through formatter to get the colourised level name
|
|
msg = self.formatter.format(record)
|
|
print("%s %s" % (record.levelname, msg))
|
|
|
|
|
|
def log_svr(port):
|
|
"""A simple log server for remote logging. Handles one connection and
|
|
then exits."""
|
|
addr = ("", port)
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
server = socketserver.TCPServer(addr, _LogRecvHandler)
|
|
server.handle_request()
|
|
server.server_close()
|
|
|
|
|
|
def init(log_dir=None, verbose=0, remote=None):
|
|
"""Initialise logging for the RT framework."""
|
|
|
|
# Default LOG level is debug
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Add new log levels for stdout/stderr, and results
|
|
logging.addLevelName(LOG_LVL_STDERR, 'STDERR')
|
|
logging.addLevelName(LOG_LVL_STDOUT, 'STDOUT')
|
|
|
|
logging.addLevelName(LOG_LVL_SKIP, 'SKIP')
|
|
logging.addLevelName(LOG_LVL_PASS, 'PASS')
|
|
logging.addLevelName(LOG_LVL_CANCEL, 'CANCEL')
|
|
logging.addLevelName(LOG_LVL_FAIL, 'FAIL')
|
|
logging.addLevelName(LOG_LVL_SUM, 'SUMMARY')
|
|
|
|
# Create a handler for the logfile if requested
|
|
if log_dir:
|
|
global ENV_RT_RESULTS
|
|
log_fn = None
|
|
ENV_RT_RESULTS = os.path.join(log_dir, _UUID.__str__())
|
|
ENV_RT_RESULTS = os.path.abspath(ENV_RT_RESULTS)
|
|
if not os.path.isdir(ENV_RT_RESULTS):
|
|
os.makedirs(ENV_RT_RESULTS)
|
|
log_fn = os.path.join(ENV_RT_RESULTS, "TESTS.log")
|
|
|
|
logf = logging.FileHandler(log_fn)
|
|
logf.setLevel(logging.DEBUG)
|
|
# Add milliseconds with a . (instead of a ,)
|
|
logf_form = logging.Formatter(
|
|
fmt='[%(levelname)-8s] %(asctime)s.%(msecs)03d: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S')
|
|
# always use UTC
|
|
logf.converter = time.gmtime
|
|
logf.setFormatter(logf_form)
|
|
logger.addHandler(logf)
|
|
|
|
# open CSV file
|
|
global _CSV_FILE
|
|
global _CSV_WRITER
|
|
cols = ["ID", "Timestamp", "Duration", "Name", "Result", "Message"]
|
|
fn = os.path.join(ENV_RT_RESULTS, "TESTS.csv")
|
|
_CSV_FILE = open(fn, 'w')
|
|
_CSV_WRITER = csv.writer(_CSV_FILE)
|
|
_CSV_WRITER.writerow(cols)
|
|
|
|
# Create console handler, which by default only logs results
|
|
if remote:
|
|
host, port = remote.split(':')
|
|
logc = logging.handlers.SocketHandler(host, int(port))
|
|
global _SOCKET_HANDLER
|
|
_SOCKET_HANDLER = logc
|
|
else:
|
|
logc = logging.StreamHandler(sys.stdout)
|
|
if verbose == 0:
|
|
logc.setLevel(LOG_LVL_PASS)
|
|
elif verbose == 1:
|
|
logc.setLevel(LOG_LVL_SKIP)
|
|
elif verbose == 2:
|
|
logc.setLevel(logging.INFO)
|
|
else:
|
|
logc.setLevel(logging.DEBUG)
|
|
logc_form = _ColouredFormatter('%(levelname)s %(message)s')
|
|
logc.setFormatter(logc_form)
|
|
logger.addHandler(logc)
|
|
return
|
|
|
|
|
|
# Map return codes to log levels
|
|
_RC_TO_LVL = {
|
|
TEST_RC_PASS: LOG_LVL_PASS,
|
|
TEST_RC_FAIL: LOG_LVL_FAIL,
|
|
TEST_RC_SKIP: LOG_LVL_SKIP,
|
|
TEST_RC_CANCEL: LOG_LVL_CANCEL,
|
|
}
|
|
|
|
|
|
_TEST_HANDLER = None
|
|
|
|
|
|
def _darwin_asl(msg):
|
|
cmd = ["syslog", "-s",
|
|
"-k", "Facility", "com.docker.rt-local",
|
|
"-k", "Level", "Notice",
|
|
"-k", "Sender", "Docker",
|
|
"-k", "Message", msg]
|
|
subprocess.call(cmd)
|
|
|
|
|
|
def _test_start_darwin(name):
|
|
_darwin_asl("Start test: %s" % name)
|
|
|
|
|
|
def test_start(name):
|
|
"""Create an additional logger for the test with @name"""
|
|
if not ENV_RT_RESULTS:
|
|
return
|
|
global _TEST_HANDLER
|
|
log_fn = os.path.join(ENV_RT_RESULTS, "%s.log" % name)
|
|
_TEST_HANDLER = logging.FileHandler(log_fn)
|
|
_TEST_HANDLER.setLevel(logging.DEBUG)
|
|
# Add milliseconds with a . (instead of a ,)
|
|
logf_form = logging.Formatter(
|
|
fmt='[%(levelname)-8s] %(asctime)s.%(msecs)03d: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S')
|
|
# always use UTC
|
|
_TEST_HANDLER.converter = time.gmtime
|
|
_TEST_HANDLER.setFormatter(logf_form)
|
|
logger.addHandler(_TEST_HANDLER)
|
|
if platform.system() == "Darwin":
|
|
_test_start_darwin(name)
|
|
return
|
|
|
|
|
|
def _test_end_darwin(name):
|
|
_darwin_asl("End test: %s" % name)
|
|
|
|
|
|
def test_end(name):
|
|
"""Stop logger for a individual test"""
|
|
global _TEST_HANDLER
|
|
if platform.system() == "Darwin":
|
|
_test_end_darwin(name)
|
|
if not _TEST_HANDLER:
|
|
return
|
|
logger.removeHandler(_TEST_HANDLER)
|
|
_TEST_HANDLER.close()
|
|
_TEST_HANDLER = None
|
|
return
|
|
|
|
|
|
def _os_logs_darwin(name):
|
|
asllog_fn = os.path.join(ENV_RT_RESULTS, "%s.asl.log" % name)
|
|
cmd = ["syslog", "-F",
|
|
"$Time $Host $(Sender)[$(Facility)][$(PID)]<$((Level)(str))>: " +
|
|
"$Message",
|
|
"-k", "Sender", "Seq", "Docker", "-o",
|
|
"-k", "Sender", "Seq", "docker", "-o",
|
|
"-k", "Message", "Seq", "Docker", "-o",
|
|
"-k", "Message", "Seq", "docker"]
|
|
with open(asllog_fn, "w") as asllog:
|
|
subprocess.call(cmd, stdout=asllog)
|
|
|
|
|
|
def os_logs(name):
|
|
if platform.system() == "Darwin":
|
|
_os_logs_darwin(name)
|
|
|
|
|
|
def result(name, rc, duration=0, message=None):
|
|
"""Log the result of a test. @name is the test name, @rc must be one
|
|
of TEST_RC_*. @duration and @message is optional. @duration is
|
|
expected to be a float of the number of seconds.
|
|
"""
|
|
|
|
ts = datetime.datetime.utcnow()
|
|
_CSV_WRITER.writerow([_UUID, ts.isoformat(' '), duration, name, rc,
|
|
"" if not message else message])
|
|
|
|
msg = "%-50s" % name
|
|
if duration:
|
|
msg += " %7.3fs" % duration
|
|
if message:
|
|
msg += " %s" % message
|
|
|
|
logger.log(_RC_TO_LVL[rc], msg)
|
|
return
|
|
|
|
|
|
def _read_version():
|
|
"""A project can create a VERSION.txt in ENV_RT_RESULTS. This function
|
|
reads it and returns it as a string (or UNKNOWN/UNDEFINED if not
|
|
present)"""
|
|
|
|
if not ENV_RT_RESULTS:
|
|
return "UNKNOWN"
|
|
fn = os.path.join(ENV_RT_RESULTS, "VERSION.txt")
|
|
if not os.path.isfile(fn):
|
|
return "UNDEFINED"
|
|
f = open(fn, 'r')
|
|
version = f.readline()
|
|
f.close()
|
|
return version.strip()
|
|
|
|
|
|
def finish(results):
|
|
"""Log a summary of tests"""
|
|
|
|
end_time = datetime.datetime.utcnow()
|
|
diff = end_time - _START_TIME
|
|
duration = diff.total_seconds()
|
|
version = _read_version()
|
|
|
|
logger.log(LOG_LVL_SUM, "Logdir: %s" % _UUID)
|
|
logger.log(LOG_LVL_SUM, "Version: %s" % version)
|
|
logger.log(LOG_LVL_SUM, "Passed: %d" % results[TEST_RC_PASS])
|
|
logger.log(LOG_LVL_SUM, "Failed: %d" % results[TEST_RC_FAIL])
|
|
logger.log(LOG_LVL_SUM, "Cancelled: %d" % results[TEST_RC_CANCEL])
|
|
logger.log(LOG_LVL_SUM, "Skipped: %d" % results[TEST_RC_SKIP])
|
|
logger.log(LOG_LVL_SUM, "Duration: %.2fs" % duration)
|
|
|
|
if _SOCKET_HANDLER is not None:
|
|
_SOCKET_HANDLER.close()
|
|
|
|
if ENV_RT_RESULTS:
|
|
cols = ["ID", "Version", "Start Time", "End Time", "Duration",
|
|
"Passed", "Failed", "Skipped",
|
|
"Labels", "OS", "OS Name", "OS Version"]
|
|
fn = os.path.join(ENV_RT_RESULTS, "SUMMARY.csv")
|
|
f = open(fn, 'w')
|
|
writer = csv.writer(f)
|
|
|
|
os_info = rt.misc.get_os_info()
|
|
res = []
|
|
res.append(_UUID)
|
|
res.append(version)
|
|
res.append(_START_TIME.isoformat(' '))
|
|
res.append(end_time.isoformat(' '))
|
|
res.append(duration)
|
|
res.append(results[TEST_RC_PASS])
|
|
res.append(results[TEST_RC_FAIL])
|
|
res.append(results[TEST_RC_SKIP])
|
|
res.append(os_info["os"])
|
|
res.append(os_info["name"])
|
|
res.append(os_info["version"])
|
|
|
|
writer.writerow(cols)
|
|
writer.writerow(res)
|
|
|
|
f.close()
|
|
|
|
_CSV_FILE.close()
|
|
return
|