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.

115 lines
4.5 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. module Semantic
  2. module Subscribers
  3. # LogSubscriber for event_group :active_record
  4. class ActiveRecord < LogSubscriber
  5. include AnsiColors
  6. IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze
  7. TRANSACTION_TAINT = AnsiColors::DARK_TEXT_CYAN
  8. def sql(event)
  9. name = event.payload[:name]
  10. if name.nil?
  11. logger.debug ' ', dimensions: Semantic::FancyDimensions.new(rails: '╔═╗', before: 1)
  12. logger.debug 'could be a migration running by...', event.payload[:sql]
  13. Rails.logger.info ' ', dimensions: Semantic::FancyDimensions.new(rails: '╚═╝')
  14. return
  15. end
  16. return if IGNORE_PAYLOAD_NAMES.include?(name)
  17. category, model, *remaining = name.split.reverse
  18. return if category == 'TRANSACTION'
  19. statement_taint = TEXT_BLUE
  20. case category
  21. when 'Count'
  22. name = "#{category} #{model}"
  23. when 'Load'
  24. if event.payload[:cached]
  25. statement_taint = TEXT_GRAY_300
  26. name = "Cache Read #{model}"
  27. no_stats = true
  28. elsif model == ::ActiveRecord::SchemaMigration.to_s
  29. category_taint = TEXT_MAGENTA
  30. name = 'Migration required'
  31. logger.debug ' ', dimensions: Semantic::FancyDimensions.new(rails: '╔═╗', before: 1)
  32. else
  33. row_count = event.payload[:row_count]
  34. name = "Read #{row_count} #{model.pluralize(row_count)}"
  35. end
  36. when 'Update', 'Create', 'Destroy'
  37. statement_taint = TEXT_COLOR_PINK
  38. name = "#{category} #{model}"
  39. category_taint = TRANSACTION_TAINT
  40. increment_transaction_local(event.payload[:transaction].uuid, category.downcase.to_sym, event.duration)
  41. else raise "unknown sql category: <#{category}>"
  42. end
  43. name = "#{name} #{stats_event(event.duration)}" unless no_stats
  44. name = "#{name} #{remaining.join(' ')}" if remaining.any?
  45. name = colorize(name, category_taint) if category_taint
  46. statement = colorize(pretty_binded_statement(event), statement_taint)
  47. logger.debug("#{name} #{statement}")
  48. end
  49. def start_transaction(_event)
  50. logger.info(colorize('Begin', TRANSACTION_TAINT))
  51. end
  52. def transaction(event)
  53. outcome = colorize(event.payload[:outcome].capitalize, TRANSACTION_TAINT)
  54. transaction = event.payload[:transaction]
  55. summary = transaction_summary(transaction)
  56. stats = stats_event(event.duration - transaction_internal_duration(transaction))
  57. logger.info("#{outcome} #{summary} #{stats}")
  58. end
  59. def instantiation(event); end
  60. def strict_loading_violation(event) = any_hook event
  61. private
  62. def pretty_binded_statement(event)
  63. statement = event.payload[:sql].dup
  64. bounds = event
  65. .payload[:binds]
  66. .select { |item| item.is_a?(::ActiveModel::Attribute) }
  67. .map(&:value)
  68. return statement if bounds.empty?
  69. bounds.map { |b| boolean_or_numeric?(b) ? b.to_s : "'#{b}'" }
  70. .each_with_index { |sb, index| statement.gsub!("$#{index + 1}", sb) }
  71. statement.gsub!(/ LIMIT 1$/, '') # LIMIT 1 is useless!
  72. statement
  73. rescue StandardError => e
  74. logger.debug event.payload[:binds]
  75. logger.error 'an error occured during pretty binded statement', e
  76. end
  77. def boolean_or_numeric?(value) = value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
  78. def stats_event(duration)= colorize("(#{number_to_ms(duration)})", TEXT_GRAY_400)
  79. def number_to_ms(number) = "#{::ActionController::Base.helpers.number_with_precision(number, precision: 1)}ms"
  80. def transaction_internal_duration(transaction) = transaction_local(transaction.uuid)[:total_duration]
  81. def transaction_summary(transaction)
  82. transaction_local(transaction.uuid)
  83. .except(:total_duration)
  84. .select { |_, value| value.positive? }
  85. .map { |k, v| "#{v} #{k.to_s.pluralize(v)}" }
  86. .join(',')
  87. end
  88. def increment_transaction_local(transaction_id, sql_command, duration)
  89. transaction_local(transaction_id)[sql_command] += 1
  90. transaction_local(transaction_id)[:total_duration] += duration
  91. end
  92. def thread_local = Thread.current[self.class.to_s] ||= {}
  93. def transaction_local(transaction_id)
  94. thread_local[transaction_id] ||= { update: 0, create: 0, destroy: 0, total_duration: 0 }
  95. end
  96. end
  97. end
  98. end