import json import subprocess import click from pathlib import PurePath, Path import configparser import logging from logging.handlers import RotatingFileHandler import sys APP_NAME = 'iottb' DB_NAME = 'iottb.db' CONSOLE_LOG_FORMATS = { 0: '%(levelname)s - %(message)s', 1: '%(levelname)s - %(module)s - %(message)s', 2: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' } LOGFILE_LOG_FORMAT = { 0: '%(levelname)s - %(asctime)s - %(module)s - %(message)s', 1: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(message)s', 2: '%(levelname)s - %(asctime)s - %(module)s - %(funcName)s - %(lineno)d - %(message)s' } MAX_VERBOSITY = len(CONSOLE_LOG_FORMATS) - 1 assert len(LOGFILE_LOG_FORMAT) == len(CONSOLE_LOG_FORMATS), 'Log formats must be same size' logger = logging.getLogger(__name__) def setup_logging(verbosity, debug): """ Setup root logger for iottb """ log_level = logging.ERROR handlers = [] date_format = '%Y-%m-%d %H:%M:%S' if verbosity > 0: log_level = logging.WARNING if verbosity > MAX_VERBOSITY: verbosity = MAX_VERBOSITY log_level = logging.INFO assert verbosity <= MAX_VERBOSITY, f'Verbosity must be <= {MAX_VERBOSITY}' console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(logging.Formatter(CONSOLE_LOG_FORMATS[verbosity], datefmt=date_format)) console_handler.setLevel(logging.DEBUG) # can keep at debug since it depends on global level? handlers.append(console_handler) if debug: log_level = logging.DEBUG # Logfile logs INFO+, no debugs though file_handler = RotatingFileHandler(f'{APP_NAME}.log', maxBytes=10240, backupCount=5) file_handler.setFormatter(logging.Formatter(LOGFILE_LOG_FORMAT[verbosity], datefmt=date_format)) file_handler.setLevel(logging.INFO) # finnish root logger setup handlers.append(file_handler) # Force this config to be applied to root logger logging.basicConfig(level=log_level, handlers=handlers, force=True) def create_default_config(cfg_file): """Create default iottb config file.""" assert logger is not None, 'Logger must be initialized' logger.info(f'Creating default config file at {cfg_file}') # By default, create iottb.db in user home defaults = { 'DefaultDatabase': DB_NAME, 'DatabaseLocations': { DB_NAME: str(Path.home() / DB_NAME) } } with open(cfg_file, 'w') as config_file: json.dump(defaults, config_file) return defaults def load_config(ctx, param, value): """ Try to load iottb config file. If the file does not exist, it will be created with default values.` Try to load the iottb config file from the given path. If the file does not exist, it will be created with default values. The only value set is the path to the iottb.db file. """ logger.info(f'Loading config from {value}') cfg_file = value if not cfg_file.is_file(): defaults = create_default_config(cfg_file) else: defaults = json.load(cfg_file) ctx.obj['path']['cfg'] = cfg_file ctx.obj['path']['db'] = defaults['DatabaseLocations'][defaults['DefaultDatabase']] @click.group() @click.option('-v', '--verbosity', type=click.IntRange(0, MAX_VERBOSITY), default=0, help='Set verbosity') @click.option('-d', '--debug', is_flag=True, default=False, help='Enable debug mode') @click.option('--cfg-file', type=click.Path(), default=Path(click.get_app_dir(APP_NAME)).joinpath('iottb.cfg'), envvar='IOTTB_CONF_HOME', is_eager=True, callback=load_config, help='Path to iottb config file') @click.pass_context def main(verbosity, debug): setup_logging(verbosity, debug) if __name__ == '__main__': main(auto_envvar_prefix='IOTTB')