URI: 
       tlogging.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tlogging.py (9662B)
       ---
            1 # Copyright (C) 2019 The Electrum developers
            2 # Distributed under the MIT software license, see the accompanying
            3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php
            4 
            5 import logging
            6 import datetime
            7 import sys
            8 import pathlib
            9 import os
           10 import platform
           11 from typing import Optional
           12 import copy
           13 import subprocess
           14 
           15 
           16 class LogFormatterForFiles(logging.Formatter):
           17 
           18     def formatTime(self, record, datefmt=None):
           19         # timestamps follow ISO 8601 UTC
           20         date = datetime.datetime.fromtimestamp(record.created).astimezone(datetime.timezone.utc)
           21         if not datefmt:
           22             datefmt = "%Y%m%dT%H%M%S.%fZ"
           23         return date.strftime(datefmt)
           24 
           25     def format(self, record):
           26         record = _shorten_name_of_logrecord(record)
           27         return super().format(record)
           28 
           29 
           30 file_formatter = LogFormatterForFiles(fmt="%(asctime)22s | %(levelname)8s | %(name)s | %(message)s")
           31 
           32 
           33 class LogFormatterForConsole(logging.Formatter):
           34 
           35     def format(self, record):
           36         record = _shorten_name_of_logrecord(record)
           37         text = super().format(record)
           38         shortcut = getattr(record, 'custom_shortcut', None)
           39         if shortcut:
           40             text = text[:1] + f"/{shortcut}" + text[1:]
           41         return text
           42 
           43 
           44 # try to make console log lines short... no timestamp, short levelname, no "electrum."
           45 console_formatter = LogFormatterForConsole(fmt="%(levelname).1s | %(name)s | %(message)s")
           46 
           47 
           48 def _shorten_name_of_logrecord(record: logging.LogRecord) -> logging.LogRecord:
           49     record = copy.copy(record)  # avoid mutating arg
           50     # strip the main module name from the logger name
           51     if record.name.startswith("electrum."):
           52         record.name = record.name[9:]
           53     # manual map to shorten common module names
           54     record.name = record.name.replace("interface.Interface", "interface", 1)
           55     record.name = record.name.replace("network.Network", "network", 1)
           56     record.name = record.name.replace("synchronizer.Synchronizer", "synchronizer", 1)
           57     record.name = record.name.replace("verifier.SPV", "verifier", 1)
           58     record.name = record.name.replace("gui.qt.main_window.ElectrumWindow", "gui.qt.main_window", 1)
           59     return record
           60 
           61 
           62 # enable logs universally (including for other libraries)
           63 root_logger = logging.getLogger()
           64 root_logger.setLevel(logging.WARNING)
           65 
           66 # log to stderr; by default only WARNING and higher
           67 console_stderr_handler = logging.StreamHandler(sys.stderr)
           68 console_stderr_handler.setFormatter(console_formatter)
           69 console_stderr_handler.setLevel(logging.WARNING)
           70 root_logger.addHandler(console_stderr_handler)
           71 
           72 # creates a logger specifically for electrum library
           73 electrum_logger = logging.getLogger("electrum")
           74 electrum_logger.setLevel(logging.DEBUG)
           75 
           76 
           77 def _delete_old_logs(path, keep=10):
           78     files = sorted(list(pathlib.Path(path).glob("electrum_log_*.log")), reverse=True)
           79     for f in files[keep:]:
           80         os.remove(str(f))
           81 
           82 
           83 _logfile_path = None
           84 def _configure_file_logging(log_directory: pathlib.Path):
           85     global _logfile_path
           86     assert _logfile_path is None, 'file logging already initialized'
           87     log_directory.mkdir(exist_ok=True)
           88 
           89     _delete_old_logs(log_directory)
           90 
           91     timestamp = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
           92     PID = os.getpid()
           93     _logfile_path = log_directory / f"electrum_log_{timestamp}_{PID}.log"
           94 
           95     file_handler = logging.FileHandler(_logfile_path, encoding='utf-8')
           96     file_handler.setFormatter(file_formatter)
           97     file_handler.setLevel(logging.DEBUG)
           98     root_logger.addHandler(file_handler)
           99 
          100 
          101 def _configure_verbosity(*, verbosity, verbosity_shortcuts):
          102     if not verbosity and not verbosity_shortcuts:
          103         return
          104     console_stderr_handler.setLevel(logging.DEBUG)
          105     _process_verbosity_log_levels(verbosity)
          106     _process_verbosity_filter_shortcuts(verbosity_shortcuts)
          107 
          108 
          109 def _process_verbosity_log_levels(verbosity):
          110     if verbosity == '*' or not isinstance(verbosity, str):
          111         return
          112     # example verbosity:
          113     #   debug,network=error,interface=error      // effectively blacklists network and interface
          114     #   warning,network=debug,interface=debug    // effectively whitelists network and interface
          115     filters = verbosity.split(',')
          116     for filt in filters:
          117         if not filt: continue
          118         items = filt.split('=')
          119         if len(items) == 1:
          120             level = items[0]
          121             electrum_logger.setLevel(level.upper())
          122         elif len(items) == 2:
          123             logger_name, level = items
          124             logger = get_logger(logger_name)
          125             logger.setLevel(level.upper())
          126         else:
          127             raise Exception(f"invalid log filter: {filt}")
          128 
          129 
          130 def _process_verbosity_filter_shortcuts(verbosity_shortcuts):
          131     if not isinstance(verbosity_shortcuts, str):
          132         return
          133     if len(verbosity_shortcuts) < 1:
          134         return
          135     # depending on first character being '^', either blacklist or whitelist
          136     is_blacklist = verbosity_shortcuts[0] == '^'
          137     if is_blacklist:
          138         filters = verbosity_shortcuts[1:]
          139     else:  # whitelist
          140         filters = verbosity_shortcuts[0:]
          141     filt = ShortcutFilteringFilter(is_blacklist=is_blacklist, filters=filters)
          142     # apply filter directly (and only!) on stderr handler
          143     # note that applying on one of the root loggers directly would not work,
          144     # see https://docs.python.org/3/howto/logging.html#logging-flow
          145     console_stderr_handler.addFilter(filt)
          146 
          147 
          148 class ShortcutInjectingFilter(logging.Filter):
          149 
          150     def __init__(self, *, shortcut: Optional[str]):
          151         super().__init__()
          152         self.__shortcut = shortcut
          153 
          154     def filter(self, record):
          155         record.custom_shortcut = self.__shortcut
          156         return True
          157 
          158 
          159 class ShortcutFilteringFilter(logging.Filter):
          160 
          161     def __init__(self, *, is_blacklist: bool, filters: str):
          162         super().__init__()
          163         self.__is_blacklist = is_blacklist
          164         self.__filters = filters
          165 
          166     def filter(self, record):
          167         # all errors are let through
          168         if record.levelno >= logging.ERROR:
          169             return True
          170         # the logging module itself is let through
          171         if record.name == __name__:
          172             return True
          173         # do filtering
          174         shortcut = getattr(record, 'custom_shortcut', None)
          175         if self.__is_blacklist:
          176             if shortcut is None:
          177                 return True
          178             if shortcut in self.__filters:
          179                 return False
          180             return True
          181         else:  # whitelist
          182             if shortcut is None:
          183                 return False
          184             if shortcut in self.__filters:
          185                 return True
          186             return False
          187 
          188 
          189 # --- External API
          190 
          191 def get_logger(name: str) -> logging.Logger:
          192     if name.startswith("electrum."):
          193         name = name[9:]
          194     return electrum_logger.getChild(name)
          195 
          196 
          197 _logger = get_logger(__name__)
          198 _logger.setLevel(logging.INFO)
          199 
          200 
          201 class Logger:
          202 
          203     # Single character short "name" for this class.
          204     # Can be used for filtering log lines. Does not need to be unique.
          205     LOGGING_SHORTCUT = None  # type: Optional[str]
          206 
          207     def __init__(self):
          208         self.logger = self.__get_logger_for_obj()
          209 
          210     def __get_logger_for_obj(self) -> logging.Logger:
          211         cls = self.__class__
          212         if cls.__module__:
          213             name = f"{cls.__module__}.{cls.__name__}"
          214         else:
          215             name = cls.__name__
          216         try:
          217             diag_name = self.diagnostic_name()
          218         except Exception as e:
          219             raise Exception("diagnostic name not yet available?") from e
          220         if diag_name:
          221             name += f".[{diag_name}]"
          222         logger = get_logger(name)
          223         if self.LOGGING_SHORTCUT:
          224             logger.addFilter(ShortcutInjectingFilter(shortcut=self.LOGGING_SHORTCUT))
          225         return logger
          226 
          227     def diagnostic_name(self):
          228         return ''
          229 
          230 
          231 def configure_logging(config):
          232     verbosity = config.get('verbosity')
          233     verbosity_shortcuts = config.get('verbosity_shortcuts')
          234     _configure_verbosity(verbosity=verbosity, verbosity_shortcuts=verbosity_shortcuts)
          235 
          236     log_to_file = config.get('log_to_file', False)
          237     is_android = 'ANDROID_DATA' in os.environ
          238     if is_android:
          239         from jnius import autoclass
          240         build_config = autoclass("org.electrum.electrum.BuildConfig")
          241         log_to_file |= bool(build_config.DEBUG)
          242     if log_to_file:
          243         log_directory = pathlib.Path(config.path) / "logs"
          244         _configure_file_logging(log_directory)
          245 
          246     # if using kivy, avoid kivy's own logs to get printed twice
          247     logging.getLogger('kivy').propagate = False
          248 
          249     from . import ELECTRUM_VERSION
          250     from .constants import GIT_REPO_URL
          251     _logger.info(f"Electrum version: {ELECTRUM_VERSION} - https://electrum.org - {GIT_REPO_URL}")
          252     _logger.info(f"Python version: {sys.version}. On platform: {describe_os_version()}")
          253     _logger.info(f"Logging to file: {str(_logfile_path)}")
          254     _logger.info(f"Log filters: verbosity {repr(verbosity)}, verbosity_shortcuts {repr(verbosity_shortcuts)}")
          255 
          256 
          257 def get_logfile_path() -> Optional[pathlib.Path]:
          258     return _logfile_path
          259 
          260 
          261 def describe_os_version() -> str:
          262     if 'ANDROID_DATA' in os.environ:
          263         from kivy import utils
          264         if utils.platform != "android":
          265             return utils.platform
          266         import jnius
          267         bv = jnius.autoclass('android.os.Build$VERSION')
          268         b = jnius.autoclass('android.os.Build')
          269         return "Android {} on {} {} ({})".format(bv.RELEASE, b.BRAND, b.DEVICE, b.DISPLAY)
          270     else:
          271         return platform.platform()
          272 
          273 
          274 def get_git_version() -> Optional[str]:
          275     dir = os.path.dirname(os.path.realpath(__file__))
          276     try:
          277         version = subprocess.check_output(
          278             ['git', 'describe', '--always', '--dirty'], cwd=dir)
          279         version = str(version, "utf8").strip()
          280     except Exception:
          281         version = None
          282     return version