Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2017-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Combine logs from multiple bitcoin nodes as well as the test_framework log.7This streams the combined log output to stdout. Use combine_logs.py > outputfile8to write to an outputfile.10If no argument is provided, the most recent test directory will be used."""12import argparse13from collections import defaultdict, namedtuple14import heapq15import itertools16import os17import pathlib18import re19import sys20import tempfile22# N.B.: don't import any local modules here - this script must remain executable23# without the parent module installed.25# Should match same symbol in `test_framework.test_framework`.26TMPDIR_PREFIX = "bitcoin_func_test_"28# Matches on the date format at the start of the log event29TIMESTAMP_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{6})?Z")31LogEvent = namedtuple('LogEvent', ['timestamp', 'source', 'event'])33def main():34"""Main function. Parses args, reads the log files and renders them as text or html."""35parser = argparse.ArgumentParser(36description=__doc__, formatter_class=argparse.RawTextHelpFormatter)37parser.add_argument(38'testdir', nargs='?', default='',39help=('temporary test directory to combine logs from. '40'Defaults to the most recent'))41parser.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)')42parser.add_argument('--html', dest='html', action='store_true', help='outputs the combined log as html. Requires jinja2. pip install jinja2')43args = parser.parse_args()45if args.html and args.color:46print("Only one out of --color or --html should be specified")47sys.exit(1)49testdir = args.testdir or find_latest_test_dir()51if not testdir:52print("No test directories found")53sys.exit(1)55if not args.testdir:56print("Opening latest test directory: {}".format(testdir), file=sys.stderr)58colors = defaultdict(lambda: '')59if args.color:60colors["test"] = "\033[0;36m" # CYAN61colors["node0"] = "\033[0;34m" # BLUE62colors["node1"] = "\033[0;32m" # GREEN63colors["node2"] = "\033[0;31m" # RED64colors["node3"] = "\033[0;33m" # YELLOW65colors["reset"] = "\033[0m" # Reset font color67log_events = read_logs(testdir)69if args.html:70print_logs_html(log_events)71else:72print_logs_plain(log_events, colors)73print_node_warnings(testdir, colors)76def read_logs(tmp_dir):77"""Reads log files.79Delegates to generator function get_log_events() to provide individual log events80for each of the input log files."""82# Find out what the folder is called that holds node 0's debug.log file83debug_logs = list(pathlib.Path(tmp_dir).glob('node0/**/debug.log'))84match len(debug_logs):85case 0:86chain = 'regtest' # fallback to regtest87case 1:88chain = re.search(r'node0/(.+?)/debug\.log$', debug_logs[0].as_posix()).group(1)89case _:90raise RuntimeError('Max one debug.log is supported, found several:\n\t' +91'\n\t'.join(map(str, debug_logs)))93files = [("test", "%s/test_framework.log" % tmp_dir)]94for i in itertools.count():95logfile = "{}/node{}/{}/debug.log".format(tmp_dir, i, chain)96if not os.path.isfile(logfile):97break98files.append(("node%d" % i, logfile))100return heapq.merge(*[get_log_events(source, f) for source, f in files])103def print_node_warnings(tmp_dir, colors):104"""Print nodes' errors and warnings"""106warnings = []107for stream in ['stdout', 'stderr']:108for i in itertools.count():109folder = "{}/node{}/{}".format(tmp_dir, i, stream)110if not os.path.isdir(folder):111break112for (_, _, fns) in os.walk(folder):113for fn in fns:114warning = pathlib.Path('{}/{}'.format(folder, fn)).read_text().strip()115if warning:116warnings.append(("node{} {}".format(i, stream), warning))118print()119for w in warnings:120print("{} {} {} {}".format(colors[w[0].split()[0]], w[0], w[1], colors["reset"]))123def find_latest_test_dir():124"""Returns the latest tmpfile test directory prefix."""125tmpdir = tempfile.gettempdir()127def join_tmp(basename):128return os.path.join(tmpdir, basename)130def is_valid_test_tmpdir(basename):131fullpath = join_tmp(basename)132return (133os.path.isdir(fullpath)134and basename.startswith(TMPDIR_PREFIX)135and os.access(fullpath, os.R_OK)136)138testdir_paths = [139join_tmp(name) for name in os.listdir(tmpdir) if is_valid_test_tmpdir(name)140]142return max(testdir_paths, key=os.path.getmtime) if testdir_paths else None145def get_log_events(source, logfile):146"""Generator function that returns individual log events.148Log events may be split over multiple lines. We use the timestamp149regex match as the marker for a new log event."""150try:151with open(logfile, 'r') as infile:152event = ''153timestamp = ''154for line in infile:155# skip blank lines156if line == '\n':157continue158# if this line has a timestamp, it's the start of a new log event.159time_match = TIMESTAMP_PATTERN.match(line)160if time_match:161if event:162yield LogEvent(timestamp=timestamp, source=source, event=event.rstrip())163timestamp = time_match.group()164if time_match.group(1) is None:165# timestamp does not have microseconds. Add zeroes.166timestamp_micro = timestamp.replace("Z", ".000000Z")167line = line.replace(timestamp, timestamp_micro)168timestamp = timestamp_micro169event = line170# if it doesn't have a timestamp, it's a continuation line of the previous log.171else:172# Add the line. Prefix with space equivalent to the source + timestamp so log lines are aligned173event += " " + line174# Flush the final event175yield LogEvent(timestamp=timestamp, source=source, event=event.rstrip())176except FileNotFoundError:177print("File %s could not be opened. Continuing without it." % logfile, file=sys.stderr)180def print_logs_plain(log_events, colors):181"""Renders the iterator of log events into text."""182for event in log_events:183lines = event.event.splitlines()184print("{0} {1: <5} {2} {3}".format(colors[event.source.rstrip()], event.source, lines[0], colors["reset"]))185if len(lines) > 1:186for line in lines[1:]:187print("{0}{1}{2}".format(colors[event.source.rstrip()], line, colors["reset"]))190def print_logs_html(log_events):191"""Renders the iterator of log events into html."""192try:193import jinja2 #type:ignore194except ImportError:195print("jinja2 not found. Try `pip install jinja2`")196sys.exit(1)197print(jinja2.Environment(loader=jinja2.FileSystemLoader('./'))198.get_template('combined_log_template.html')199.render(title="Combined Logs from testcase", log_events=[event._asdict() for event in log_events]))202if __name__ == '__main__':203main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.