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.

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