logger.py 7.9 KB

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