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.

196 lines
5.5 KiB

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