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.

135 lines
5.3 KiB

4 weeks ago
  1. module Semantic
  2. module Subscribers
  3. # LogSubscriber for event_group :action_controller
  4. class ActionController < LogSubscriber
  5. include AnsiColors
  6. INTERNAL_PARAMS = %i[controller action format _method only_path].freeze
  7. DEFAULT_DEV_HOSTS = ['127.0.0.1', 'localhost'].freeze
  8. TERMINUS_STRING = '╙─╜'.freeze
  9. # options = { main_session_tag: 'ANY_SESSION_KEY' }
  10. def initialize(**options)
  11. super(:controller)
  12. @session_key = options[:main_session_tag]
  13. @transactions = {}
  14. end
  15. def start_processing(event)
  16. session_value = session_value(event)
  17. @transactions[event.transaction_id] = session_value # preserve session_value to help finish_processing
  18. SemanticLogger.tagged(session_value) do
  19. request = event.payload[:request]
  20. path = colorize(request.filtered_path, BOLD)
  21. dimensions = Semantic::FancyDimensions.new(rails: '╓─╖', before: 1)
  22. if defined?(@previously_redirect) && @previously_redirect
  23. dimensions = Semantic::FancyDimensions.new(rails: '╓║╖', before: 0)
  24. @previously_redirect = false
  25. end
  26. logger.info("Started #{request.raw_request_method} #{path}", dimensions:)
  27. format = event.payload[:format]
  28. format = format.to_s.upcase if format.is_a?(Symbol)
  29. format = '*/*' if format.nil?
  30. format = colorize(format, BOLD)
  31. logger.debug("Processing by #{event.payload[:controller]}##{event.payload[:action]} as #{format}")
  32. params = event.payload[:params].deep_symbolize_keys.except(*INTERNAL_PARAMS)
  33. unless params.empty?
  34. params = params.ai(ruby19_syntax: true, plain: true, multiline: false)
  35. params.gsub!(/(\w+):/, "#{TEXT_CYAN}\\1#{CLEAR}:")
  36. params.gsub!(/"(.*?)"/, "\"#{TEXT_BROWN}\\1#{CLEAR}\"")
  37. end
  38. logger.debug("Parameters: #{params}") unless params.empty?
  39. end
  40. end
  41. def process_action(event)
  42. session_value = @transactions.delete(event.transaction_id) # delete previous session_value from start_processing
  43. SemanticLogger.tagged(session_value) do
  44. payload = event.payload
  45. additions = ::ActionController::Base.log_process_action(payload)
  46. status = payload[:status]
  47. if status.nil? && (exception_class_name = payload[:exception]&.first)
  48. status = ::ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
  49. end
  50. additions << pop_active_record_summary
  51. additions << "GC: #{event.gc_time.round(1)}ms"
  52. additions.compact!
  53. if event.duration >= 1200
  54. logger.error process_duration(event, additions)
  55. elsif event.duration >= 600
  56. logger.warn process_duration(event, additions)
  57. elsif event.duration >= 300
  58. logger.info process_duration(event, additions)
  59. # elsif event.duration >= 100
  60. else
  61. logger.debug process_duration(event, additions)
  62. end
  63. status_family = status / 100
  64. dimensions = case status_family
  65. when 2
  66. Semantic::FancyDimensions.new(rails: TERMINUS_STRING)
  67. when 3, 5
  68. Semantic::FancyDimensions.new(rails: '╙║╜')
  69. when 4
  70. Semantic::FancyDimensions.new(rails: '╙╨╜')
  71. end
  72. logger.info("Completed #{colorize(status, BOLD)} #{Rack::Utils::HTTP_STATUS_CODES[status]}", dimensions:)
  73. logger.info(' ', dimensions: Semantic::FancyDimensions.new(rails: ' ║ ')) if status_family == 3
  74. logger.info(' ', dimensions: Semantic::FancyDimensions.new(rails: '╓║╖')) if status_family == 5
  75. end
  76. end
  77. def redirect_to(event)
  78. location = capture_path(event.payload[:location])
  79. logger.debug("Redirected to #{colorize(location, BOLD)}")
  80. @previously_redirect = true
  81. end
  82. private
  83. # FIXME: might be more accurate, multiple transactions, sum of CRUD
  84. def pop_active_record_summary
  85. active_record_transactions = Thread.current[ActiveRecord.to_s]
  86. return unless active_record_transactions
  87. # reset thread local
  88. Thread.current[ActiveRecord.to_s] = nil
  89. active_record_transactions.map do |k, art|
  90. art.except(:total_duration)
  91. .select { |_, value| value.positive? }
  92. .map { |k, v| "#{v} #{k.to_s.pluralize(v)}" }
  93. .join(',')
  94. end.compact.join('|')
  95. end
  96. def redirect_regex
  97. return @redirect_regex if defined?(@redirect_regex)
  98. options = Rails.application.routes.default_url_options
  99. dev_hosts = DEFAULT_DEV_HOSTS + Array.wrap(options[:host])
  100. dev_hosts_or = dev_hosts.uniq.join('|')
  101. dev_from = "http://(?:#{dev_hosts_or}):#{options[:port]}(.*)"
  102. @redirect_regex = /^#{dev_from}/
  103. end
  104. def capture_path(url)
  105. m = redirect_regex.match(url)
  106. m.nil? ? url : m[1]
  107. end
  108. def session_value(event) = event.payload[:headers]['rack.session'].fetch(@session_key, nil)
  109. def process_duration(event, additions) = "Processed in #{event.duration.round}ms (#{additions.join(' | ')})"
  110. end
  111. end
  112. end