result.rb 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. module Blazer
  2. class Result
  3. attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached
  4. def initialize(data_source, columns, rows, error, cached_at, just_cached)
  5. @data_source = data_source
  6. @columns = columns
  7. @rows = rows
  8. @error = error
  9. @cached_at = cached_at
  10. @just_cached = just_cached
  11. end
  12. def timed_out?
  13. error == Blazer::TIMEOUT_MESSAGE
  14. end
  15. def cached?
  16. cached_at.present?
  17. end
  18. def boom
  19. @boom ||= begin
  20. boom = {}
  21. columns.each_with_index do |key, i|
  22. query = data_source.smart_columns[key]
  23. if query
  24. values = rows.map { |r| r[i] }.compact.uniq
  25. result = data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
  26. boom[key] = Hash[result.rows.map { |k, v| [k.to_s, v] }]
  27. end
  28. end
  29. boom
  30. end
  31. end
  32. def column_types
  33. @column_types ||= begin
  34. columns.each_with_index.map do |k, i|
  35. v = (rows.find { |r| r[i] } || {})[i]
  36. if boom[k]
  37. "string"
  38. elsif v.is_a?(Numeric)
  39. "numeric"
  40. elsif v.is_a?(Time) || v.is_a?(Date)
  41. "time"
  42. elsif v.nil?
  43. nil
  44. else
  45. "string"
  46. end
  47. end
  48. end
  49. end
  50. def chart_type
  51. @chart_type ||= begin
  52. if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
  53. "line"
  54. elsif column_types == ["time", "string", "numeric"]
  55. "line2"
  56. elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
  57. "bar"
  58. elsif column_types == ["string", "string", "numeric"]
  59. "bar2"
  60. end
  61. end
  62. end
  63. def detect_anomaly
  64. anomaly = nil
  65. message = nil
  66. if rows.empty?
  67. message = "No data"
  68. else
  69. if chart_type == "line" || chart_type == "line2"
  70. series = []
  71. if chart_type == "line"
  72. columns[1..-1].each_with_index.each do |k, i|
  73. series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
  74. end
  75. else
  76. rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
  77. series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
  78. end
  79. end
  80. current_series = nil
  81. begin
  82. anomalies = []
  83. series.each do |s|
  84. current_series = s[:name]
  85. anomalies << s[:name] if anomaly?(s[:data])
  86. end
  87. anomaly = anomalies.any?
  88. if anomaly
  89. if anomalies.size == 1
  90. message = "Anomaly detected in #{anomalies.first}"
  91. else
  92. message = "Anomalies detected in #{anomalies.to_sentence}"
  93. end
  94. else
  95. message = "No anomalies detected"
  96. end
  97. rescue => e
  98. message = "#{current_series}: #{e.message}"
  99. raise e if Rails.env.development?
  100. end
  101. else
  102. message = "Bad format"
  103. end
  104. end
  105. [anomaly, message]
  106. end
  107. def anomaly?(series)
  108. series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
  109. csv_str =
  110. CSV.generate do |csv|
  111. csv << ["timestamp", "count"]
  112. series.each do |row|
  113. csv << row
  114. end
  115. end
  116. r_script = %x[which Rscript].chomp
  117. type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
  118. args = [type, csv_str]
  119. raise "R not found" if r_script.empty?
  120. command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
  121. output = %x[#{command}]
  122. if output.empty?
  123. raise "Unknown R error"
  124. end
  125. rows = CSV.parse(output, headers: true)
  126. error = rows.first && rows.first["x"]
  127. raise error if error
  128. timestamps = []
  129. if type == "ts"
  130. rows.each do |row|
  131. timestamps << Time.parse(row["timestamp"])
  132. end
  133. timestamps.include?(series.last[0].to_time)
  134. else
  135. rows.each do |row|
  136. timestamps << row["index"].to_i
  137. end
  138. timestamps.include?(series.length)
  139. end
  140. end
  141. end
  142. end