logger.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import logging
  2. import os
  3. import re
  4. import sys
  5. import traceback
  6. from datetime import datetime
  7. from typing import Literal, Mapping
  8. from termcolor import colored
  9. LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
  10. DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
  11. if DEBUG:
  12. LOG_LEVEL = 'DEBUG'
  13. LOG_TO_FILE = os.getenv('LOG_TO_FILE', 'False').lower() in ['true', '1', 'yes']
  14. DISABLE_COLOR_PRINTING = False
  15. ColorType = Literal[
  16. 'red',
  17. 'green',
  18. 'yellow',
  19. 'blue',
  20. 'magenta',
  21. 'cyan',
  22. 'light_grey',
  23. 'dark_grey',
  24. 'light_red',
  25. 'light_green',
  26. 'light_yellow',
  27. 'light_blue',
  28. 'light_magenta',
  29. 'light_cyan',
  30. 'white',
  31. ]
  32. LOG_COLORS: Mapping[str, ColorType] = {
  33. 'ACTION': 'green',
  34. 'USER_ACTION': 'light_red',
  35. 'OBSERVATION': 'yellow',
  36. 'USER_OBSERVATION': 'light_green',
  37. 'DETAIL': 'cyan',
  38. 'ERROR': 'red',
  39. 'PLAN': 'light_magenta',
  40. }
  41. class ColoredFormatter(logging.Formatter):
  42. def format(self, record):
  43. msg_type = record.__dict__.get('msg_type')
  44. event_source = record.__dict__.get('event_source')
  45. if event_source:
  46. new_msg_type = f'{event_source.upper()}_{msg_type}'
  47. if new_msg_type in LOG_COLORS:
  48. msg_type = new_msg_type
  49. if msg_type in LOG_COLORS and not DISABLE_COLOR_PRINTING:
  50. msg_type_color = colored(msg_type, LOG_COLORS[msg_type])
  51. msg = colored(record.msg, LOG_COLORS[msg_type])
  52. time_str = colored(
  53. self.formatTime(record, self.datefmt), LOG_COLORS[msg_type]
  54. )
  55. name_str = colored(record.name, LOG_COLORS[msg_type])
  56. level_str = colored(record.levelname, LOG_COLORS[msg_type])
  57. if msg_type in ['ERROR'] or DEBUG:
  58. return f'{time_str} - {name_str}:{level_str}: {record.filename}:{record.lineno}\n{msg_type_color}\n{msg}'
  59. return f'{time_str} - {msg_type_color}\n{msg}'
  60. elif msg_type == 'STEP':
  61. msg = '\n\n==============\n' + record.msg + '\n'
  62. return f'{msg}'
  63. return super().format(record)
  64. console_formatter = ColoredFormatter(
  65. '\033[92m%(asctime)s - %(name)s:%(levelname)s\033[0m: %(filename)s:%(lineno)s - %(message)s',
  66. datefmt='%H:%M:%S',
  67. )
  68. file_formatter = logging.Formatter(
  69. '%(asctime)s - %(name)s:%(levelname)s: %(filename)s:%(lineno)s - %(message)s',
  70. datefmt='%H:%M:%S',
  71. )
  72. llm_formatter = logging.Formatter('%(message)s')
  73. class SensitiveDataFilter(logging.Filter):
  74. def filter(self, record):
  75. # start with attributes
  76. sensitive_patterns = [
  77. 'api_key',
  78. 'aws_access_key_id',
  79. 'aws_secret_access_key',
  80. 'e2b_api_key',
  81. 'github_token',
  82. 'jwt_secret',
  83. 'modal_api_token_id',
  84. 'modal_api_token_secret',
  85. ]
  86. # add env var names
  87. env_vars = [attr.upper() for attr in sensitive_patterns]
  88. sensitive_patterns.extend(env_vars)
  89. # and some special cases
  90. sensitive_patterns.append('JWT_SECRET')
  91. sensitive_patterns.append('LLM_API_KEY')
  92. sensitive_patterns.append('GITHUB_TOKEN')
  93. sensitive_patterns.append('SANDBOX_ENV_GITHUB_TOKEN')
  94. # this also formats the message with % args
  95. msg = record.getMessage()
  96. record.args = ()
  97. for attr in sensitive_patterns:
  98. pattern = rf"{attr}='?([\w-]+)'?"
  99. msg = re.sub(pattern, f"{attr}='******'", msg)
  100. # passed with msg
  101. record.msg = msg
  102. return True
  103. def get_console_handler(log_level=logging.INFO, extra_info: str | None = None):
  104. """Returns a console handler for logging."""
  105. console_handler = logging.StreamHandler()
  106. console_handler.setLevel(log_level)
  107. formatter_str = '%(asctime)s - %(levelname)s - %(message)s'
  108. if extra_info:
  109. formatter_str = f'{extra_info} - ' + formatter_str
  110. console_handler.setFormatter(logging.Formatter(formatter_str))
  111. return console_handler
  112. def get_file_handler(log_dir, log_level=logging.INFO):
  113. """Returns a file handler for logging."""
  114. os.makedirs(log_dir, exist_ok=True)
  115. timestamp = datetime.now().strftime('%Y-%m-%d')
  116. file_name = f'openhands_{timestamp}.log'
  117. file_handler = logging.FileHandler(os.path.join(log_dir, file_name))
  118. file_handler.setLevel(log_level)
  119. file_handler.setFormatter(file_formatter)
  120. return file_handler
  121. # Set up logging
  122. logging.basicConfig(level=logging.ERROR)
  123. def log_uncaught_exceptions(ex_cls, ex, tb):
  124. """Logs uncaught exceptions along with the traceback.
  125. Args:
  126. ex_cls (type): The type of the exception.
  127. ex (Exception): The exception instance.
  128. tb (traceback): The traceback object.
  129. Returns:
  130. None
  131. """
  132. logging.error(''.join(traceback.format_tb(tb)))
  133. logging.error('{0}: {1}'.format(ex_cls, ex))
  134. sys.excepthook = log_uncaught_exceptions
  135. openhands_logger = logging.getLogger('openhands')
  136. current_log_level = logging.INFO
  137. if LOG_LEVEL in logging.getLevelNamesMapping():
  138. current_log_level = logging.getLevelNamesMapping()[LOG_LEVEL]
  139. openhands_logger.setLevel(current_log_level)
  140. if current_log_level == logging.DEBUG:
  141. LOG_TO_FILE = True
  142. openhands_logger.info('DEBUG mode enabled.')
  143. openhands_logger.addHandler(get_console_handler(current_log_level))
  144. openhands_logger.addFilter(SensitiveDataFilter(openhands_logger.name))
  145. openhands_logger.propagate = False
  146. openhands_logger.debug('Logging initialized')
  147. LOG_DIR = os.path.join(
  148. # parent dir of openhands/core (i.e., root of the repo)
  149. os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
  150. 'logs',
  151. )
  152. if LOG_TO_FILE:
  153. openhands_logger.addHandler(
  154. get_file_handler(LOG_DIR, current_log_level)
  155. ) # default log to project root
  156. openhands_logger.info(f'Logging to file in: {LOG_DIR}')
  157. # Exclude LiteLLM from logging output
  158. logging.getLogger('LiteLLM').disabled = True
  159. logging.getLogger('LiteLLM Router').disabled = True
  160. logging.getLogger('LiteLLM Proxy').disabled = True
  161. class LlmFileHandler(logging.FileHandler):
  162. """# LLM prompt and response logging"""
  163. def __init__(self, filename, mode='a', encoding='utf-8', delay=False):
  164. """Initializes an instance of LlmFileHandler.
  165. Args:
  166. filename (str): The name of the log file.
  167. mode (str, optional): The file mode. Defaults to 'a'.
  168. encoding (str, optional): The file encoding. Defaults to None.
  169. delay (bool, optional): Whether to delay file opening. Defaults to False.
  170. """
  171. self.filename = filename
  172. self.message_counter = 1
  173. if DEBUG:
  174. self.session = datetime.now().strftime('%y-%m-%d_%H-%M')
  175. else:
  176. self.session = 'default'
  177. self.log_directory = os.path.join(LOG_DIR, 'llm', self.session)
  178. os.makedirs(self.log_directory, exist_ok=True)
  179. if not DEBUG:
  180. # Clear the log directory if not in debug mode
  181. for file in os.listdir(self.log_directory):
  182. file_path = os.path.join(self.log_directory, file)
  183. try:
  184. os.unlink(file_path)
  185. except Exception as e:
  186. openhands_logger.error(
  187. 'Failed to delete %s. Reason: %s', file_path, e
  188. )
  189. filename = f'{self.filename}_{self.message_counter:03}.log'
  190. self.baseFilename = os.path.join(self.log_directory, filename)
  191. super().__init__(self.baseFilename, mode, encoding, delay)
  192. def emit(self, record):
  193. """Emits a log record.
  194. Args:
  195. record (logging.LogRecord): The log record to emit.
  196. """
  197. filename = f'{self.filename}_{self.message_counter:03}.log'
  198. self.baseFilename = os.path.join(self.log_directory, filename)
  199. self.stream = self._open()
  200. super().emit(record)
  201. self.stream.close()
  202. openhands_logger.debug('Logging to %s', self.baseFilename)
  203. self.message_counter += 1
  204. def _get_llm_file_handler(name: str, log_level: int):
  205. # The 'delay' parameter, when set to True, postpones the opening of the log file
  206. # until the first log message is emitted.
  207. llm_file_handler = LlmFileHandler(name, delay=True)
  208. llm_file_handler.setFormatter(llm_formatter)
  209. llm_file_handler.setLevel(log_level)
  210. return llm_file_handler
  211. def _setup_llm_logger(name: str, log_level: int):
  212. logger = logging.getLogger(name)
  213. logger.propagate = False
  214. logger.setLevel(log_level)
  215. if LOG_TO_FILE:
  216. logger.addHandler(_get_llm_file_handler(name, log_level))
  217. return logger
  218. llm_prompt_logger = _setup_llm_logger('prompt', current_log_level)
  219. llm_response_logger = _setup_llm_logger('response', current_log_level)