log.py
1 # This file is part of DarkFi (https://dark.fi) 2 # 3 # Copyright (C) 2020-2025 Dyne.org foundation 4 # 5 # This program is free software: you can redistribute it and/or modify 6 # it under the terms of the GNU Affero General Public License as 7 # published by the Free Software Foundation, either version 3 of the 8 # License, or (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 import os 19 import logging 20 21 from logging.handlers import RotatingFileHandler 22 23 """ 24 Module: log.py 25 26 This module provides functionality to setup logging for the explorer Flask application. 27 """ 28 29 def setup_logger(app, env): 30 """ 31 Sets up logging for the explorer Flask app by setting up error, application, and request logging. 32 33 The error logger captures application errors and logs them to a dedicated error log file. The application 34 logger handles general application-level logs such as debug or informational messages. Additionally, the 35 request logger, derived from the Werkzeug logger, manages HTTP request logs and directs them to the 36 same file as the application logger. 37 38 The overall log level is determined by the LOG_LEVEL environment variable, defaulting to INFO if the 39 variable is not set or contains an invalid value. The path where logs are stored is obtained from the 40 application's TOML configuration file under the 'log_path' entry. If not specified, it defaults to the 41 current directory. 42 43 Args: 44 app (Flask): The Flask application instance. 45 env (str): The environment (e.g., 'localnet', 'mainnet', 'testnet', etc.). 46 """ 47 log_path = app.config.get('log_path', '.') 48 49 # Expand the path if home directory is specified 50 log_path = os.path.expanduser(log_path) 51 52 # Ensure the log path exists or create it if not 53 if not os.path.exists(log_path): 54 try: 55 os.makedirs(log_path) 56 print(f"created log dir: {log_path}") 57 except OSError as e: 58 raise RuntimeError(f"Unable to create log directory at '{log_path}': {e}") 59 60 # Get log level from environment variable, default to INFO 61 log_level_name = os.environ.get('LOG_LEVEL', 'INFO').upper() 62 try: 63 log_level = getattr(logging, log_level_name) 64 except AttributeError: 65 if log_level_name: 66 app.logger.warning(f"Invalid LOG_LEVEL '{log_level_name}'. Defaulting to INFO.") 67 log_level = logging.INFO 68 69 # App logger setup 70 app_logger = setup_app_logger(log_path, env, log_level) 71 app.logger = app_logger 72 73 # Request logger setup 74 app_log_file = os.path.join(log_path, 'app.log') 75 setup_request_logger(app_log_file, env, log_level) 76 77 # Error logger setup 78 error_logger = setup_error_logger(log_path, env) 79 app.error_logger = error_logger 80 81 def setup_error_logger(log_path, env): 82 """ 83 Configures the error logger to capture application errors, returning an error logger instance. 84 85 Args: 86 log_path (str): Path to the directory where logs are stored. 87 env (str): The application environment. 88 89 Returns: 90 logging.Logger: Configured error logger instance. 91 """ 92 error_logger = logging.getLogger('error_logger') 93 error_log_file = os.path.join(log_path, 'error.log') 94 error_handler = initialize_log_handler(error_log_file, env) 95 error_handler.setLevel(logging.ERROR) 96 error_formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]') 97 error_handler.setFormatter(error_formatter) 98 error_logger.addHandler(error_handler) 99 error_logger.setLevel(logging.ERROR) 100 error_logger.propagate = False 101 102 add_console_handler_if_localnet(env, error_logger, logging.ERROR) 103 104 return error_logger 105 106 def setup_app_logger(log_path, env, log_level=logging.INFO): 107 """ 108 Configures the app logger for general application logging, returning an app logger instance. 109 110 Args: 111 log_path (str): Path to the directory where logs are stored. 112 env (str): The application environment. 113 log_level (int): The logging level (default is INFO). 114 """ 115 app_logger = logging.getLogger('app_logger') 116 app_log_file = os.path.join(log_path, 'app.log') 117 app_handler = initialize_log_handler(app_log_file, env) 118 app_handler.setLevel(log_level) 119 app_formatter = logging.Formatter('%(asctime)s %(message)s') 120 app_handler.setFormatter(app_formatter) 121 app_logger.addHandler(app_handler) 122 app_logger.setLevel(log_level) 123 app_logger.propagate = False 124 125 add_console_handler_if_localnet(env, app_logger, log_level) 126 127 return app_logger 128 129 def setup_request_logger(log_file, env, log_level=logging.INFO): 130 """ 131 Configures the request logger to handle HTTP request logs based on the specified environment. 132 133 If the environment is set to 'localnet', HTTP requests are logged to the console to facilitate 134 local development and debugging. For all other environments, such as 'testnet' or 'mainnet', 135 HTTP requests are logged to the specified log file, ensuring that logs are persisted in a location 136 appropriate for testing or production use. 137 138 Args: 139 log_file (str): Path to the log file where HTTP requests should be logged. 140 env (str): The application environment (e.g., 'localnet', 'testnet', 'mainnet'). 141 log_level (int): The logging level (default is INFO). 142 """ 143 # Get the werkzeug logger that logs requests 144 request_logger = logging.getLogger('werkzeug') 145 request_logger.setLevel(log_level) 146 request_logger.propagate = False 147 148 file_handler = logging.FileHandler(log_file) 149 file_handler.setLevel(log_level) 150 request_logger.addHandler(file_handler) 151 152 add_console_handler_if_localnet(env, request_logger, log_level) 153 154 def initialize_log_handler(log_file, env): 155 """ 156 Initializes and returns a log handler based on the environment. 157 158 Args: 159 log_file (str): Path to the log file. 160 env (str): The environment (e.g., 'mainnet', 'testnet', etc.). 161 """ 162 if env == "mainnet": 163 return RotatingFileHandler(log_file, maxBytes=100_000_000, backupCount=5) 164 else: 165 return logging.FileHandler(log_file) 166 167 def add_console_handler_if_localnet(env, logger, log_level=logging.INFO): 168 """ 169 Adds a console handler to the given logger if the environment is 'localnet'. 170 171 Args: 172 env (str): The current environment (e.g., 'localnet', 'mainnet'). 173 logger (logging.Logger): The logger to which the console handler should be added. 174 log_level (int): The logging level for the console handler. 175 """ 176 177 # If localnet, also log to console 178 if env == 'localnet': 179 console_handler = logging.StreamHandler() 180 console_handler.setLevel(log_level) 181 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 182 console_handler.setFormatter(formatter) 183 logger.addHandler(console_handler)