/ bin / explorer / site / log.py
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)