logger.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. """
  2. Internal logging utility.
  3. """
  4. import datetime
  5. import logging
  6. import os
  7. import sys
  8. from functools import partial
  9. from typing import Optional
  10. from loguru import logger
  11. from rich.console import Console
  12. from rich.markup import escape
  13. from rich.progress import (BarColumn, MofNCompleteColumn, Progress,
  14. TaskProgressColumn, TextColumn, TimeRemainingColumn)
  15. import aphrodite.common.envs as envs
  16. RICH_CONSOLE = Console()
  17. LOG_LEVEL = os.getenv("APHRODITE_LOG_LEVEL", "INFO").upper()
  18. APHRODITE_CONFIGURE_LOGGING = envs.APHRODITE_CONFIGURE_LOGGING
  19. def unwrap(wrapped, default=None):
  20. """Unwrap function for Optionals."""
  21. if wrapped is None:
  22. return default
  23. return wrapped
  24. def get_loading_progress_bar():
  25. """Gets a pre-made progress bar for loading tasks."""
  26. return Progress(
  27. TextColumn("[progress.description]{task.description}"),
  28. BarColumn(),
  29. TaskProgressColumn(),
  30. MofNCompleteColumn(),
  31. TimeRemainingColumn(),
  32. console=RICH_CONSOLE,
  33. )
  34. def _log_formatter(record: dict):
  35. """Log message formatter."""
  36. color_map = {
  37. "TRACE": "dim blue",
  38. "DEBUG": "cyan",
  39. "INFO": "green",
  40. "SUCCESS": "bold green",
  41. "WARNING": "yellow",
  42. "ERROR": "red",
  43. "CRITICAL": "bold white on red",
  44. }
  45. level = record.get("level")
  46. level_color = color_map.get(level.name, "cyan")
  47. colored_level = f"[{level_color}]{level.name}[/{level_color}]:"
  48. separator = " " * (9 - len(level.name))
  49. message = unwrap(record.get("message"), "")
  50. # Replace once loguru allows for turning off str.format
  51. message = message.replace("{", "{{").replace("}", "}}").replace("<", "\<")
  52. # Escape markup tags from Rich
  53. message = escape(message)
  54. lines = message.splitlines()
  55. fmt = ""
  56. if len(lines) > 1:
  57. fmt = "\n".join(
  58. [f"{colored_level}{separator}{line}" for line in lines])
  59. else:
  60. fmt = f"{colored_level}{separator}{message}"
  61. return fmt
  62. _logged_messages = set()
  63. def log_once(level, message, *args, **kwargs):
  64. if message not in _logged_messages:
  65. _logged_messages.add(message)
  66. logger.log(level, message, *args, **kwargs)
  67. # Uvicorn log handler
  68. # Uvicorn log portions inspired from https://github.com/encode/uvicorn/discussions/2027#discussioncomment-6432362
  69. class UvicornLoggingHandler(logging.Handler):
  70. def emit(self, record: logging.LogRecord) -> None:
  71. logger.opt(exception=record.exc_info).log(record.levelname,
  72. self.format(record).rstrip())
  73. # Uvicorn config for logging. Passed into run when creating all loggers in
  74. # server
  75. UVICORN_LOG_CONFIG = {
  76. "version": 1,
  77. "disable_existing_loggers": False,
  78. "handlers": {
  79. "uvicorn": {
  80. "class":
  81. f"{UvicornLoggingHandler.__module__}.{UvicornLoggingHandler.__qualname__}", # noqa
  82. },
  83. },
  84. "root": {
  85. "handlers": ["uvicorn"],
  86. "propagate": False,
  87. "level": LOG_LEVEL
  88. },
  89. }
  90. def setup_logger():
  91. """Bootstrap the logger."""
  92. logger.remove()
  93. logger.add(
  94. RICH_CONSOLE.print,
  95. level=LOG_LEVEL,
  96. format=_log_formatter,
  97. colorize=True,
  98. )
  99. logger.log_once = log_once
  100. setup_logger()
  101. def _trace_calls(log_path, root_dir, frame, event, arg=None):
  102. if event in ['call', 'return']:
  103. # Extract the filename, line number, function name, and the code object
  104. filename = frame.f_code.co_filename
  105. lineno = frame.f_lineno
  106. func_name = frame.f_code.co_name
  107. if not filename.startswith(root_dir):
  108. # only log the functions in the aphrodite root_dir
  109. return
  110. # Log every function call or return
  111. try:
  112. last_frame = frame.f_back
  113. if last_frame is not None:
  114. last_filename = last_frame.f_code.co_filename
  115. last_lineno = last_frame.f_lineno
  116. last_func_name = last_frame.f_code.co_name
  117. else:
  118. # initial frame
  119. last_filename = ""
  120. last_lineno = 0
  121. last_func_name = ""
  122. with open(log_path, 'a') as f:
  123. if event == 'call':
  124. f.write(f"{datetime.datetime.now()} Call to"
  125. f" {func_name} in {filename}:{lineno}"
  126. f" from {last_func_name} in {last_filename}:"
  127. f"{last_lineno}\n")
  128. else:
  129. f.write(f"{datetime.datetime.now()} Return from"
  130. f" {func_name} in {filename}:{lineno}"
  131. f" to {last_func_name} in {last_filename}:"
  132. f"{last_lineno}\n")
  133. except NameError:
  134. # modules are deleted during shutdown
  135. pass
  136. return partial(_trace_calls, log_path, root_dir)
  137. def enable_trace_function_call(log_file_path: str,
  138. root_dir: Optional[str] = None):
  139. """
  140. Enable tracing of every function call in code under `root_dir`.
  141. This is useful for debugging hangs or crashes.
  142. `log_file_path` is the path to the log file.
  143. `root_dir` is the root directory of the code to trace. If None, it is the
  144. aphrodite root directory.
  145. Note that this call is thread-level, any threads calling this function
  146. will have the trace enabled. Other threads will not be affected.
  147. """
  148. logger.warning(
  149. "APHRODITE_TRACE_FUNCTION is enabled. It will record every"
  150. " function executed by Python. This will slow down the code. It "
  151. "is suggested to be used for debugging hang or crashes only.")
  152. logger.info(f"Trace frame log is saved to {log_file_path}")
  153. if root_dir is None:
  154. # by default, this is the aphrodite root directory
  155. root_dir = os.path.dirname(os.path.dirname(__file__))
  156. sys.settrace(partial(_trace_calls, log_file_path, root_dir))