2017-03-22 13:03:26 +01:00
#!/usr/bin/env python3
2020-01-16 21:57:24 +01:00
# Copyright (c) 2017-2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
2017-03-22 13:03:26 +01:00
""" Combine logs from multiple bitcoin nodes as well as the test_framework log.
This streams the combined log output to stdout . Use combine_logs . py > outputfile
2018-11-30 16:30:24 +01:00
to write to an outputfile .
If no argument is provided , the most recent t est directory will be used . """
2017-03-22 13:03:26 +01:00
import argparse
from collections import defaultdict , namedtuple
2021-01-22 15:58:07 +01:00
import glob
2017-03-22 13:03:26 +01:00
import heapq
2017-03-28 14:44:53 +02:00
import itertools
2017-03-22 13:03:26 +01:00
import os
2019-05-01 14:37:19 +02:00
import pathlib
2017-03-22 13:03:26 +01:00
import re
import sys
2018-11-30 16:30:24 +01:00
import tempfile
# N.B.: don't import any local modules here - this script must remain executable
# without the parent module installed.
# Should match same symbol in `test_framework.test_framework`.
TMPDIR_PREFIX = " dash_func_test_ "
2017-03-22 13:03:26 +01:00
# Matches on the date format at the start of the log event
2018-08-28 10:43:24 +02:00
TIMESTAMP_PATTERN = re . compile ( r " ^ \ d {4} - \ d {2} - \ d {2} T \ d {2} : \ d {2} : \ d {2} ( \ . \ d {6} )?Z " )
2017-03-22 13:03:26 +01:00
LogEvent = namedtuple ( ' LogEvent ' , [ ' timestamp ' , ' source ' , ' event ' ] )
def main ( ) :
""" Main function. Parses args, reads the log files and renders them as text or html. """
2018-11-30 16:30:24 +01:00
parser = argparse . ArgumentParser (
description = __doc__ , formatter_class = argparse . RawTextHelpFormatter )
parser . add_argument (
' testdir ' , nargs = ' ? ' , default = ' ' ,
help = ( ' temporary test directory to combine logs from. '
' Defaults to the most recent ' ) )
2017-03-22 13:03:26 +01:00
parser . add_argument ( ' -c ' , ' --color ' , dest = ' color ' , action = ' store_true ' , help = ' outputs the combined log with events colored by source (requires posix terminal colors. Use less -r for viewing) ' )
parser . add_argument ( ' --html ' , dest = ' html ' , action = ' store_true ' , help = ' outputs the combined log as html. Requires jinja2. pip install jinja2 ' )
2018-11-30 16:30:24 +01:00
args = parser . parse_args ( )
2017-03-22 13:03:26 +01:00
if args . html and args . color :
print ( " Only one out of --color or --html should be specified " )
sys . exit ( 1 )
2018-11-30 16:30:24 +01:00
testdir = args . testdir or find_latest_test_dir ( )
if not testdir :
print ( " No test directories found " )
2017-03-22 13:03:26 +01:00
sys . exit ( 1 )
2018-11-30 16:30:24 +01:00
if not args . testdir :
print ( " Opening latest test directory: {} " . format ( testdir ) , file = sys . stderr )
2019-05-01 14:37:19 +02:00
colors = defaultdict ( lambda : ' ' )
if args . color :
colors [ " test " ] = " \033 [0;36m " # CYAN
colors [ " node0 " ] = " \033 [0;34m " # BLUE
colors [ " node1 " ] = " \033 [0;32m " # GREEN
colors [ " node2 " ] = " \033 [0;31m " # RED
colors [ " node3 " ] = " \033 [0;33m " # YELLOW
colors [ " reset " ] = " \033 [0m " # Reset font color
2018-11-30 16:30:24 +01:00
log_events = read_logs ( testdir )
2017-03-22 13:03:26 +01:00
2019-05-01 14:37:19 +02:00
if args . html :
print_logs_html ( log_events )
else :
print_logs_plain ( log_events , colors )
print_node_warnings ( testdir , colors )
2017-03-22 13:03:26 +01:00
def read_logs ( tmp_dir ) :
""" Reads log files.
Delegates to generator function get_log_events ( ) to provide individual log events
for each of the input log files . """
2021-01-22 15:58:07 +01:00
# Find out what the folder is called that holds the debug.log file
chain = glob . glob ( " {} /node0/*/debug.log " . format ( tmp_dir ) )
if chain :
chain = chain [ 0 ] # pick the first one if more than one chain was found (should never happen)
2021-09-10 02:45:16 +02:00
chain = re . search ( r ' node0/(.+?)/debug \ .log$ ' , chain ) . group ( 1 ) # extract the chain name
2021-01-22 15:58:07 +01:00
else :
chain = ' regtest ' # fallback to regtest (should only happen when none exists)
2017-03-22 13:03:26 +01:00
files = [ ( " test " , " %s /test_framework.log " % tmp_dir ) ]
2017-03-28 14:44:53 +02:00
for i in itertools . count ( ) :
2021-01-22 15:58:07 +01:00
logfile = " {} /node {} / {} /debug.log " . format ( tmp_dir , i , chain )
2017-03-28 14:44:53 +02:00
if not os . path . isfile ( logfile ) :
break
2017-03-22 13:03:26 +01:00
files . append ( ( " node %d " % i , logfile ) )
return heapq . merge ( * [ get_log_events ( source , f ) for source , f in files ] )
2018-11-30 16:30:24 +01:00
2019-05-01 14:37:19 +02:00
def print_node_warnings ( tmp_dir , colors ) :
""" Print nodes ' errors and warnings """
warnings = [ ]
for stream in [ ' stdout ' , ' stderr ' ] :
for i in itertools . count ( ) :
folder = " {} /node {} / {} " . format ( tmp_dir , i , stream )
if not os . path . isdir ( folder ) :
break
for ( _ , _ , fns ) in os . walk ( folder ) :
for fn in fns :
warning = pathlib . Path ( ' {} / {} ' . format ( folder , fn ) ) . read_text ( ) . strip ( )
if warning :
warnings . append ( ( " node {} {} " . format ( i , stream ) , warning ) )
print ( )
for w in warnings :
print ( " {} {} {} {} " . format ( colors [ w [ 0 ] . split ( ) [ 0 ] ] , w [ 0 ] , w [ 1 ] , colors [ " reset " ] ) )
2018-11-30 16:30:24 +01:00
def find_latest_test_dir ( ) :
""" Returns the latest tmpfile test directory prefix. """
tmpdir = tempfile . gettempdir ( )
def join_tmp ( basename ) :
return os . path . join ( tmpdir , basename )
def is_valid_test_tmpdir ( basename ) :
fullpath = join_tmp ( basename )
return (
os . path . isdir ( fullpath )
and basename . startswith ( TMPDIR_PREFIX )
and os . access ( fullpath , os . R_OK )
)
testdir_paths = [
join_tmp ( name ) for name in os . listdir ( tmpdir ) if is_valid_test_tmpdir ( name )
]
return max ( testdir_paths , key = os . path . getmtime ) if testdir_paths else None
2017-03-22 13:03:26 +01:00
def get_log_events ( source , logfile ) :
""" Generator function that returns individual log events.
Log events may be split over multiple lines . We use the timestamp
regex match as the marker for a new log event . """
try :
2018-06-12 14:52:26 +02:00
with open ( logfile , ' r ' , encoding = ' utf-8 ' ) as infile :
2017-03-22 13:03:26 +01:00
event = ' '
timestamp = ' '
for line in infile :
# skip blank lines
if line == ' \n ' :
continue
# if this line has a timestamp, it's the start of a new log event.
time_match = TIMESTAMP_PATTERN . match ( line )
if time_match :
if event :
yield LogEvent ( timestamp = timestamp , source = source , event = event . rstrip ( ) )
timestamp = time_match . group ( )
2018-08-28 10:43:24 +02:00
if time_match . group ( 1 ) is None :
# timestamp does not have microseconds. Add zeroes.
timestamp_micro = timestamp . replace ( " Z " , " .000000Z " )
line = line . replace ( timestamp , timestamp_micro )
timestamp = timestamp_micro
event = line
2017-03-22 13:03:26 +01:00
# if it doesn't have a timestamp, it's a continuation line of the previous log.
else :
2018-08-28 10:43:24 +02:00
# Add the line. Prefix with space equivalent to the source + timestamp so log lines are aligned
event + = " " + line
2017-03-22 13:03:26 +01:00
# Flush the final event
yield LogEvent ( timestamp = timestamp , source = source , event = event . rstrip ( ) )
except FileNotFoundError :
print ( " File %s could not be opened. Continuing without it. " % logfile , file = sys . stderr )
2019-05-01 14:37:19 +02:00
def print_logs_plain ( log_events , colors ) :
""" Renders the iterator of log events into text. """
2017-03-22 13:03:26 +01:00
for event in log_events :
2018-08-28 10:43:24 +02:00
lines = event . event . splitlines ( )
print ( " {0} {1: <5} {2} {3} " . format ( colors [ event . source . rstrip ( ) ] , event . source , lines [ 0 ] , colors [ " reset " ] ) )
if len ( lines ) > 1 :
for line in lines [ 1 : ] :
print ( " {0} {1} {2} " . format ( colors [ event . source . rstrip ( ) ] , line , colors [ " reset " ] ) )
2017-03-22 13:03:26 +01:00
2019-05-01 14:37:19 +02:00
def print_logs_html ( log_events ) :
""" Renders the iterator of log events into html. """
2017-03-22 13:03:26 +01:00
try :
import jinja2
except ImportError :
print ( " jinja2 not found. Try `pip install jinja2` " )
sys . exit ( 1 )
2019-10-16 11:48:46 +02:00
print ( jinja2 . Environment ( loader = jinja2 . FileSystemLoader ( os . path . dirname ( os . path . abspath ( __file__ ) ) ) )
2017-03-22 13:03:26 +01:00
. get_template ( ' combined_log_template.html ' )
. render ( title = " Combined Logs from testcase " , log_events = [ event . _asdict ( ) for event in log_events ] ) )
if __name__ == ' __main__ ' :
main ( )