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.

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