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.

224 lines
6.7 KiB

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