You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
6.1 KiB

10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
10 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
11 months ago
11 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
10 months ago
  1. require_relative 'wrapper'
  2. require_relative 'base'
  3. require 'io/console'
  4. require 'amazing_print'
  5. # Opinioned Rails custom formatter
  6. class BasicFormatter < SemanticLogger::Formatters::Color # rubocop:disable Metrics/ClassLength
  7. NAME_MAX_SIZE = 25
  8. TERMINAL_PREFIX = ENV['TERMINAL_PREFIX'].to_i || 0
  9. CONTENT_PREFIX = ' '.freeze
  10. RENDERED_VIEW_DURATION = 100
  11. COMPLETED_DURATION = RENDERED_VIEW_DURATION * 5
  12. ANSI_RESET = "\e[0m".freeze
  13. ANSI_BOLD = "\e[1m".freeze
  14. ANSI_DEBUG = "\e[90m".freeze
  15. ANSI_INFO = SemanticLogger::AnsiColors::GREEN
  16. ANSI_WARN = SemanticLogger::AnsiColors::YELLOW
  17. ANSI_ERROR = "\e[91m".freeze
  18. ANSI_NEUTRAL_INFO = SemanticLogger::AnsiColors::WHITE
  19. ANSI_REVERSED_WARNING = "\e[0;30;43m".freeze
  20. ANSI_REVERSED_ERROR = "\e[1;30;41m".freeze
  21. ANSI_REVERSED_FATAL = "\e[1;30;41m".freeze
  22. CONTENT_COLOR_MAP = ColorMap.new(
  23. debug: ANSI_DEBUG,
  24. info: ANSI_NEUTRAL_INFO,
  25. warn: ANSI_REVERSED_WARNING,
  26. error: ANSI_REVERSED_ERROR,
  27. fatal: ANSI_REVERSED_FATAL
  28. )
  29. # exclude log eagerly!
  30. EXCLUDE_LAMBDA = lambda { |log|
  31. if log.name == 'ActionView::Base'
  32. !log.message.starts_with?(' Rendering')
  33. elsif log.name == 'Rails' && !log.message.nil?
  34. log.message.exclude?('Started GET "/rails/live/reload')
  35. elsif log.name == 'ActiveRecord::Base'
  36. log.message.exclude?('↳ lib/formatters/basic_formatter.rb')
  37. else
  38. true
  39. end
  40. }
  41. def initialize
  42. super(color_map: ColorMap.new(
  43. debug: ANSI_DEBUG,
  44. info: ANSI_INFO,
  45. warn: ANSI_WARN,
  46. error: ANSI_ERROR,
  47. fatal: ANSI_ERROR
  48. ))
  49. end
  50. def message
  51. return unless log.message
  52. message = wrap_message(log.message)
  53. "#{CONTENT_COLOR_MAP[log.level]}#{message}#{color_map.clear}"
  54. end
  55. def payload
  56. return unless log.payload
  57. lines = log.payload.ai(ruby19_syntax: true, indent: 2).split("\n")
  58. first_line = lines.shift
  59. space_prefix = first_line.match(/^\s*/)
  60. lines = lines.map do |l|
  61. "#{before_message(wrapped: true)}#{space_prefix}#{l}"
  62. end
  63. result = lines.unshift(first_line).join("\n")
  64. result.sub!(/\s*/) { |m| '-' * m.length }
  65. result
  66. end
  67. def level
  68. case log.level
  69. when :info, :debug then draw_rails ' '
  70. else draw_rails(log.level.to_s.chr.upcase)
  71. end
  72. end
  73. def name
  74. "#{ANSI_DEBUG}#{log.name.truncate(NAME_MAX_SIZE).center(NAME_MAX_SIZE)}#{color_map.clear}"
  75. end
  76. def exception # rubocop:disable Metrics/AbcSize
  77. return unless log.exception
  78. root_path = Rails.root.to_s
  79. stack = log.exception.backtrace.select { |line| line.starts_with?(root_path) }
  80. stack = stack.map { |line| line.delete_prefix("#{root_path}/") }
  81. "#{ANSI_REVERSED_WARNING}#{log.exception.class}#{color_map.clear} #{ANSI_REVERSED_ERROR}#{log.exception.message}#{color_map.clear}#{backtrace(stack)}" # rubocop:disable Layout/LineLength
  82. end
  83. def call(log, logger)
  84. self.log = transform_log(log)
  85. self.color = color_map[self.log.level]
  86. self.logger = logger
  87. before_message + [message, payload, exception].compact.join(' ')
  88. end
  89. private
  90. def draw_rails(char)
  91. "#{color}#{char}#{color_map.clear}"
  92. end
  93. def continuation
  94. draw_rails('┆')
  95. end
  96. # transform log before display
  97. def transform_log(log)
  98. if log.name == 'ActionView::Base'
  99. log = transform_action_view_base(log)
  100. elsif log.name == 'Rails' && log.message
  101. log.message = transform_rails_message(log.message.rstrip)
  102. log.level = :debug if log.message.lstrip =~ /^Processing/ || log.message.lstrip =~ /^Parameters/
  103. end
  104. log
  105. end
  106. def transform_rails_message(message)
  107. case message
  108. when /^Completed/
  109. transform_rails_completed(message)
  110. when /^Started/
  111. two_captures_last_as_bold(message, /(^Started \w* )"(.*?)"/)
  112. when /^Processing/
  113. " #{two_captures_last_as_bold(message, /(^Processing by \w*#\w* as )(.*)/)}"
  114. else
  115. message
  116. end
  117. end
  118. def transform_rails_completed(message)
  119. m1, m2, m3, m4 = message.match(/^Completed (\d*) (.*) in (\d*)ms(.*)$/).captures
  120. message = "Completed #{ANSI_BOLD}#{m1} #{m2}#{ANSI_RESET} in #{m3}ms"
  121. message += m4 if m3.to_i > COMPLETED_DURATION
  122. message += "\n" if m1 =~ /^[23]/
  123. message
  124. end
  125. def two_captures_last_as_bold(message, regex)
  126. m1, m2 = message.match(regex).captures
  127. "#{m1}#{ANSI_BOLD}#{m2}#{ANSI_RESET}"
  128. end
  129. def transform_action_view_base(log)
  130. log.level, message = transform_log_debug_lstrip(log)
  131. message = transform_rendered_message_with_filename(message)
  132. message = transform_duration_strip(message)
  133. log.message = message
  134. log
  135. end
  136. def transform_duration_strip(message)
  137. md2 = message.match(/( \(Duration: \d+\.?\d?ms.*\))/)
  138. md3 = md2.match(1).match(/Duration: (\d+\.?\d?)ms/)
  139. duration = md3.match(1).to_f
  140. duration < RENDERED_VIEW_DURATION ? md2.pre_match : message
  141. end
  142. def transform_rendered_message_with_filename(message)
  143. md = message.match(/^Rendered( layout| collection of|) (.*?\.erb)/)
  144. filename = "app/views/#{md.match(2)}"
  145. log.message = " Rendered#{md.match(1)} #{filename}#{md.post_match}"
  146. end
  147. def transform_log_debug_lstrip(log)
  148. [:debug, log.message.lstrip]
  149. end
  150. def wrap_message(message)
  151. message, space_prefix = split_spaces_in_front(message)
  152. message = Wrapper.wrap("#{CONTENT_COLOR_MAP[log.level]}#{message}",
  153. before_message(wrapped: true) + space_prefix.to_s,
  154. compute_useful_length - space_prefix.length)
  155. "#{space_prefix}#{message}"
  156. end
  157. def split_spaces_in_front(message)
  158. md = message.match(/^\s*/)
  159. [md.post_match, md.match(0)]
  160. end
  161. def compute_useful_length
  162. IO.console.winsize[1] - TERMINAL_PREFIX - before_message.length + CONTENT_PREFIX.length + 12
  163. rescue StandardError
  164. 100 # FIXME: CONSTANTIZE, only useful in DEBUGGER, no IO.console detected!
  165. end
  166. def before_message(wrapped: false)
  167. [name, wrapped ? continuation : level, tags, named_tags, duration].compact.join(' ') + CONTENT_PREFIX
  168. end
  169. def backtrace(stack)
  170. return "\n" unless stack.count.positive?
  171. "\n#{before_message} #{ANSI_ERROR}#{stack.join("\n#{before_message} #{ANSI_ERROR}")}#{color_map.clear}\n"
  172. end
  173. def color_content
  174. color
  175. end
  176. end