blazer 1.7.7 → 2.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +242 -33
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +621 -211
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +0 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Chart.js +15658 -10011
  13. data/app/assets/javascripts/blazer/Sortable.js +3413 -848
  14. data/app/assets/javascripts/blazer/ace/ace.js +21294 -4
  15. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1991 -3
  16. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -1
  17. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -1
  18. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -1
  19. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -1
  20. data/app/assets/javascripts/blazer/application.js +5 -3
  21. data/app/assets/javascripts/blazer/bootstrap.js +842 -628
  22. data/app/assets/javascripts/blazer/chartkick.js +2015 -1244
  23. data/app/assets/javascripts/blazer/daterangepicker.js +372 -299
  24. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  25. data/app/assets/javascripts/blazer/{jquery_ujs.js → jquery-ujs.js} +161 -75
  26. data/app/assets/javascripts/blazer/jquery.js +10126 -9562
  27. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +321 -259
  28. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1546 -0
  29. data/app/assets/javascripts/blazer/moment.js +5085 -2460
  30. data/app/assets/javascripts/blazer/queries.js +18 -4
  31. data/app/assets/javascripts/blazer/routes.js +3 -0
  32. data/app/assets/javascripts/blazer/selectize.js +3828 -3604
  33. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  34. data/app/assets/javascripts/blazer/stupidtable.js +254 -87
  35. data/app/assets/javascripts/blazer/vue.js +11175 -6676
  36. data/app/assets/stylesheets/blazer/application.css +51 -6
  37. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  38. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  39. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +1337 -711
  40. data/app/assets/stylesheets/blazer/{daterangepicker-bs3.css → daterangepicker.css} +207 -172
  41. data/app/assets/stylesheets/blazer/{selectize.default.css → selectize.css} +26 -10
  42. data/app/controllers/blazer/base_controller.rb +73 -46
  43. data/app/controllers/blazer/checks_controller.rb +1 -1
  44. data/app/controllers/blazer/dashboards_controller.rb +7 -13
  45. data/app/controllers/blazer/queries_controller.rb +171 -51
  46. data/app/controllers/blazer/uploads_controller.rb +147 -0
  47. data/app/helpers/blazer/base_helper.rb +6 -16
  48. data/app/models/blazer/audit.rb +3 -3
  49. data/app/models/blazer/check.rb +31 -5
  50. data/app/models/blazer/dashboard.rb +6 -2
  51. data/app/models/blazer/dashboard_query.rb +1 -1
  52. data/app/models/blazer/query.rb +30 -4
  53. data/app/models/blazer/record.rb +5 -0
  54. data/app/models/blazer/upload.rb +11 -0
  55. data/app/models/blazer/uploads_connection.rb +7 -0
  56. data/app/views/blazer/_nav.html.erb +3 -1
  57. data/app/views/blazer/_variables.html.erb +48 -23
  58. data/app/views/blazer/check_mailer/failing_checks.html.erb +1 -0
  59. data/app/views/blazer/check_mailer/state_change.html.erb +1 -0
  60. data/app/views/blazer/checks/_form.html.erb +17 -9
  61. data/app/views/blazer/checks/edit.html.erb +2 -0
  62. data/app/views/blazer/checks/index.html.erb +37 -5
  63. data/app/views/blazer/checks/new.html.erb +2 -0
  64. data/app/views/blazer/dashboards/_form.html.erb +5 -5
  65. data/app/views/blazer/dashboards/edit.html.erb +2 -0
  66. data/app/views/blazer/dashboards/new.html.erb +2 -0
  67. data/app/views/blazer/dashboards/show.html.erb +13 -7
  68. data/app/views/blazer/queries/_caching.html.erb +16 -0
  69. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  70. data/app/views/blazer/queries/_form.html.erb +23 -13
  71. data/app/views/blazer/queries/docs.html.erb +137 -0
  72. data/app/views/blazer/queries/home.html.erb +21 -7
  73. data/app/views/blazer/queries/run.html.erb +64 -29
  74. data/app/views/blazer/queries/schema.html.erb +44 -7
  75. data/app/views/blazer/queries/show.html.erb +15 -8
  76. data/app/views/blazer/uploads/_form.html.erb +27 -0
  77. data/app/views/blazer/uploads/edit.html.erb +3 -0
  78. data/app/views/blazer/uploads/index.html.erb +55 -0
  79. data/app/views/blazer/uploads/new.html.erb +3 -0
  80. data/app/views/layouts/blazer/application.html.erb +10 -5
  81. data/config/routes.rb +10 -1
  82. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  83. data/lib/blazer/adapters/base_adapter.rb +24 -1
  84. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  85. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  86. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  87. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  88. data/lib/blazer/adapters/elasticsearch_adapter.rb +30 -18
  89. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  90. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  91. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  92. data/lib/blazer/adapters/mongodb_adapter.rb +5 -1
  93. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  94. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  95. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  96. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  97. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  98. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  99. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  100. data/lib/blazer/adapters/sql_adapter.rb +187 -20
  101. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  102. data/lib/blazer/data_source.rb +107 -30
  103. data/lib/blazer/engine.rb +21 -23
  104. data/lib/blazer/result.rb +95 -29
  105. data/lib/blazer/run_statement.rb +8 -4
  106. data/lib/blazer/run_statement_job.rb +8 -9
  107. data/lib/blazer/slack_notifier.rb +94 -0
  108. data/lib/blazer/statement.rb +75 -0
  109. data/lib/blazer/version.rb +1 -1
  110. data/lib/blazer.rb +154 -26
  111. data/lib/generators/blazer/install_generator.rb +7 -18
  112. data/lib/generators/blazer/templates/{config.yml → config.yml.tt} +26 -3
  113. data/lib/generators/blazer/templates/{install.rb → install.rb.tt} +6 -4
  114. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  115. data/lib/generators/blazer/uploads_generator.rb +18 -0
  116. data/lib/tasks/blazer.rake +11 -1
  117. data/licenses/LICENSE-ace.txt +24 -0
  118. data/licenses/LICENSE-bootstrap.txt +21 -0
  119. data/licenses/LICENSE-chart.js.txt +9 -0
  120. data/licenses/LICENSE-chartkick.js.txt +22 -0
  121. data/licenses/LICENSE-daterangepicker.txt +21 -0
  122. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  123. data/licenses/LICENSE-highlight.js.txt +29 -0
  124. data/licenses/LICENSE-jquery-ujs.txt +20 -0
  125. data/licenses/LICENSE-jquery.txt +20 -0
  126. data/licenses/LICENSE-moment-timezone.txt +20 -0
  127. data/licenses/LICENSE-moment.txt +22 -0
  128. data/licenses/LICENSE-selectize.txt +202 -0
  129. data/licenses/LICENSE-sortable.txt +21 -0
  130. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  131. data/licenses/LICENSE-stupidtable.txt +19 -0
  132. data/licenses/LICENSE-vue.txt +21 -0
  133. metadata +83 -53
  134. data/.gitignore +0 -14
  135. data/Gemfile +0 -4
  136. data/Rakefile +0 -1
  137. data/app/assets/javascripts/blazer/highlight.pack.js +0 -1
  138. data/app/assets/javascripts/blazer/moment-timezone.js +0 -1007
  139. data/blazer.gemspec +0 -26
data/lib/blazer/result.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Blazer
2
2
  class Result
3
- attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached
3
+ attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached, :forecast_error
4
4
 
5
5
  def initialize(data_source, columns, rows, error, cached_at, just_cached)
6
6
  @data_source = data_source
@@ -56,6 +56,8 @@ module Blazer
56
56
  "time"
57
57
  elsif v.nil?
58
58
  nil
59
+ elsif v.is_a?(String) && v.encoding == Encoding::BINARY
60
+ "binary"
59
61
  else
60
62
  "string"
61
63
  end
@@ -69,6 +71,8 @@ module Blazer
69
71
  "line"
70
72
  elsif column_types == ["time", "string", "numeric"]
71
73
  "line2"
74
+ elsif column_types == ["string", "numeric"] && @columns.last == "pie"
75
+ "pie"
72
76
  elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
73
77
  "bar"
74
78
  elsif column_types == ["string", "string", "numeric"]
@@ -79,6 +83,47 @@ module Blazer
79
83
  end
80
84
  end
81
85
 
86
+ def forecastable?
87
+ @forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
88
+ end
89
+
90
+ # TODO cache it?
91
+ # don't want to put result data (even hashed version)
92
+ # into cache without developer opt-in
93
+ def forecast
94
+ count = (@rows.size * 0.25).round.clamp(30, 365)
95
+
96
+ case Blazer.forecasting
97
+ when "prophet"
98
+ require "prophet"
99
+ forecast = Prophet.forecast(@rows.to_h, count: count)
100
+ else
101
+ require "trend"
102
+ forecast = Trend.forecast(@rows.to_h, count: count)
103
+ end
104
+
105
+ # round integers
106
+ if @rows[0][1].is_a?(Integer)
107
+ forecast = forecast.map { |k, v| [k, v.round] }.to_h
108
+ end
109
+
110
+ @rows.each do |row|
111
+ row[2] = nil
112
+ end
113
+ @rows.unshift(*forecast.map { |k, v| [k, nil, v] })
114
+ @columns << "forecast"
115
+
116
+ # reset cache
117
+ @column_types = nil
118
+ @chart_type = nil
119
+
120
+ forecast
121
+ rescue => e
122
+ @forecast_error = String.new("Error generating forecast")
123
+ @forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
124
+ nil
125
+ end
126
+
82
127
  def detect_anomaly
83
128
  anomaly = nil
84
129
  message = nil
@@ -131,39 +176,60 @@ module Blazer
131
176
  def anomaly?(series)
132
177
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
133
178
 
134
- csv_str =
135
- CSV.generate do |csv|
136
- csv << ["timestamp", "count"]
137
- series.each do |row|
138
- csv << row
179
+ case Blazer.anomaly_checks
180
+ when "prophet"
181
+ df = Rover::DataFrame.new(series[0..-2].map { |v| {"ds" => v[0], "y" => v[1]} })
182
+ m = Prophet.new(interval_width: 0.99)
183
+ m.logger.level = ::Logger::FATAL # no logging
184
+ m.fit(df)
185
+ future = Rover::DataFrame.new(series[-1..-1].map { |v| {"ds" => v[0]} })
186
+ forecast = m.predict(future).to_a[0]
187
+ lower = forecast["yhat_lower"]
188
+ upper = forecast["yhat_upper"]
189
+ value = series.last[1]
190
+ value < lower || value > upper
191
+ when "trend"
192
+ anomalies = Trend.anomalies(Hash[series])
193
+ anomalies.include?(series.last[0])
194
+ when "anomaly_detection"
195
+ period = 7 # TODO determine period
196
+ anomalies = AnomalyDetection.detect(Hash[series], period: period)
197
+ anomalies.include?(series.last[0])
198
+ else
199
+ csv_str =
200
+ CSV.generate do |csv|
201
+ csv << ["timestamp", "count"]
202
+ series.each do |row|
203
+ csv << row
204
+ end
139
205
  end
140
- end
141
206
 
142
- r_script = %x[which Rscript].chomp
143
- type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
144
- args = [type, csv_str]
145
- raise "R not found" if r_script.empty?
146
- command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
147
- output = %x[#{command}]
148
- if output.empty?
149
- raise "Unknown R error"
150
- end
207
+ r_script = %x[which Rscript].chomp
208
+ type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
209
+ args = [type, csv_str]
210
+ raise "R not found" if r_script.empty?
211
+ command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
212
+ output = %x[#{command}]
213
+ if output.empty?
214
+ raise "Unknown R error"
215
+ end
151
216
 
152
- rows = CSV.parse(output, headers: true)
153
- error = rows.first && rows.first["x"]
154
- raise error if error
217
+ rows = CSV.parse(output, headers: true)
218
+ error = rows.first && rows.first["x"]
219
+ raise error if error
155
220
 
156
- timestamps = []
157
- if type == "ts"
158
- rows.each do |row|
159
- timestamps << Time.parse(row["timestamp"])
160
- end
161
- timestamps.include?(series.last[0].to_time)
162
- else
163
- rows.each do |row|
164
- timestamps << row["index"].to_i
221
+ timestamps = []
222
+ if type == "ts"
223
+ rows.each do |row|
224
+ timestamps << Time.parse(row["timestamp"])
225
+ end
226
+ timestamps.include?(series.last[0].to_time)
227
+ else
228
+ rows.each do |row|
229
+ timestamps << row["index"].to_i
230
+ end
231
+ timestamps.include?(series.length)
165
232
  end
166
- timestamps.include?(series.length)
167
233
  end
168
234
  end
169
235
  end
@@ -1,12 +1,16 @@
1
1
  module Blazer
2
2
  class RunStatement
3
- def perform(data_source, statement, options = {})
3
+ def perform(statement, options = {})
4
4
  query = options[:query]
5
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
5
+
6
+ data_source = statement.data_source
7
+ statement.bind
6
8
 
7
9
  # audit
8
10
  if Blazer.audit
9
- audit = Blazer::Audit.new(statement: statement)
11
+ audit_statement = statement.bind_statement
12
+ audit_statement += "\n\n#{statement.bind_values.to_json}" if statement.bind_values.any?
13
+ audit = Blazer::Audit.new(statement: audit_statement)
10
14
  audit.query = query
11
15
  audit.data_source = data_source.id
12
16
  audit.user = options[:user]
@@ -28,7 +32,7 @@ module Blazer
28
32
  audit.save! if audit.changed?
29
33
  end
30
34
 
31
- if query && !result.timed_out?
35
+ if query && !result.timed_out? && !query.variables.any?
32
36
  query.checks.each do |check|
33
37
  check.update_state(result)
34
38
  end
@@ -1,18 +1,17 @@
1
- require "sucker_punch"
2
-
3
1
  module Blazer
4
- class RunStatementJob
5
- include SuckerPunch::Job
6
- workers 4
2
+ class RunStatementJob < ActiveJob::Base
3
+ self.queue_adapter = :async
7
4
 
8
- def perform(result, data_source, statement, options)
5
+ def perform(data_source_id, statement, options)
6
+ statement = Blazer::Statement.new(statement, data_source_id)
7
+ statement.values = options.delete(:values)
8
+ data_source = statement.data_source
9
9
  begin
10
10
  ActiveRecord::Base.connection_pool.with_connection do
11
- result << Blazer::RunStatement.new.perform(data_source, statement, options)
11
+ Blazer::RunStatement.new.perform(statement, options)
12
12
  end
13
13
  rescue Exception => e
14
- result.clear
15
- result << Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
14
+ Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
16
15
  Blazer.cache.write(data_source.run_cache_key(options[:run_id]), Marshal.dump([[], [], "Unknown error", nil]), expires_in: 30.seconds)
17
16
  raise e
18
17
  end
@@ -0,0 +1,94 @@
1
+ require "net/http"
2
+
3
+ module Blazer
4
+ class SlackNotifier
5
+ def self.state_change(check, state, state_was, rows_count, error, check_type)
6
+ check.split_slack_channels.each do |channel|
7
+ text =
8
+ if error
9
+ error
10
+ elsif rows_count > 0 && check_type == "bad_data"
11
+ pluralize(rows_count, "row")
12
+ end
13
+
14
+ payload = {
15
+ channel: channel,
16
+ attachments: [
17
+ {
18
+ title: escape("Check #{state.titleize}: #{check.query.name}"),
19
+ title_link: query_url(check.query_id),
20
+ text: escape(text),
21
+ color: state == "passing" ? "good" : "danger"
22
+ }
23
+ ]
24
+ }
25
+
26
+ post(payload)
27
+ end
28
+ end
29
+
30
+ def self.failing_checks(channel, checks)
31
+ text =
32
+ checks.map do |check|
33
+ "<#{query_url(check.query_id)}|#{escape(check.query.name)}> #{escape(check.state)}"
34
+ end
35
+
36
+ payload = {
37
+ channel: channel,
38
+ attachments: [
39
+ {
40
+ title: escape("#{pluralize(checks.size, "Check")} Failing"),
41
+ text: text.join("\n"),
42
+ color: "warning"
43
+ }
44
+ ]
45
+ }
46
+
47
+ post(payload)
48
+ end
49
+
50
+ # https://api.slack.com/docs/message-formatting#how_to_escape_characters
51
+ # - Replace the ampersand, &, with &amp;
52
+ # - Replace the less-than sign, < with &lt;
53
+ # - Replace the greater-than sign, > with &gt;
54
+ # That's it. Don't HTML entity-encode the entire message.
55
+ def self.escape(str)
56
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") if str
57
+ end
58
+
59
+ def self.pluralize(*args)
60
+ ActionController::Base.helpers.pluralize(*args)
61
+ end
62
+
63
+ # checks shouldn't have variables, but in any case,
64
+ # avoid passing variable params to url helpers
65
+ # (known unsafe parameters are removed, but still not ideal)
66
+ def self.query_url(id)
67
+ Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
68
+ end
69
+
70
+ # TODO use return value
71
+ def self.post(payload)
72
+ if Blazer.slack_webhook_url.present?
73
+ response = post_api(Blazer.slack_webhook_url, payload, {})
74
+ response.is_a?(Net::HTTPSuccess) && response.body == "ok"
75
+ else
76
+ headers = {
77
+ "Authorization" => "Bearer #{Blazer.slack_oauth_token}",
78
+ "Content-type" => "application/json"
79
+ }
80
+ response = post_api("https://slack.com/api/chat.postMessage", payload, headers)
81
+ response.is_a?(Net::HTTPSuccess) && (JSON.parse(response.body)["ok"] rescue false)
82
+ end
83
+ end
84
+
85
+ def self.post_api(url, payload, headers)
86
+ uri = URI.parse(url)
87
+ http = Net::HTTP.new(uri.host, uri.port)
88
+ http.use_ssl = true
89
+ http.open_timeout = 3
90
+ http.read_timeout = 5
91
+ http.post(uri.request_uri, payload.to_json, headers)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,75 @@
1
+ module Blazer
2
+ class Statement
3
+ attr_reader :statement, :data_source, :bind_statement, :bind_values
4
+ attr_accessor :values
5
+
6
+ def initialize(statement, data_source = nil)
7
+ @statement = statement
8
+ @data_source = data_source.is_a?(String) ? Blazer.data_sources[data_source] : data_source
9
+ @values = {}
10
+ end
11
+
12
+ def variables
13
+ @variables ||= Blazer.extract_vars(statement)
14
+ end
15
+
16
+ def add_values(var_params)
17
+ variables.each do |var|
18
+ value = var_params[var].presence
19
+ value = nil unless value.is_a?(String) # ignore arrays and hashes
20
+ if value
21
+ if ["start_time", "end_time"].include?(var)
22
+ value = value.to_s.gsub(" ", "+") # fix for Quip bug
23
+ end
24
+
25
+ if var.end_with?("_at")
26
+ begin
27
+ value = Blazer.time_zone.parse(value)
28
+ rescue
29
+ # do nothing
30
+ end
31
+ end
32
+
33
+ unless value.is_a?(ActiveSupport::TimeWithZone)
34
+ if value =~ /\A\d+\z/
35
+ value = value.to_i
36
+ elsif value =~ /\A\d+\.\d+\z/
37
+ value = value.to_f
38
+ end
39
+ end
40
+ end
41
+ value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable
42
+ @values[var] = value
43
+ end
44
+ end
45
+
46
+ def cohort_analysis?
47
+ /\/\*\s*cohort analysis\s*\*\//i.match?(statement)
48
+ end
49
+
50
+ def apply_cohort_analysis(period:, days:)
51
+ @statement = data_source.cohort_analysis_statement(statement, period: period, days: days).sub("{placeholder}") { statement }
52
+ end
53
+
54
+ # should probably transform before cohort analysis
55
+ # but keep previous order for now
56
+ def transformed_statement
57
+ statement = self.statement.dup
58
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
59
+ statement
60
+ end
61
+
62
+ def bind
63
+ @bind_statement, @bind_values = data_source.bind_params(transformed_statement, values)
64
+ end
65
+
66
+ def display_statement
67
+ data_source.sub_variables(transformed_statement, values)
68
+ end
69
+
70
+ def clear_cache
71
+ bind if bind_statement.nil?
72
+ data_source.clear_cache(self)
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,3 @@
1
1
  module Blazer
2
- VERSION = "1.7.7"
2
+ VERSION = "2.6.4"
3
3
  end
data/lib/blazer.rb CHANGED
@@ -1,45 +1,88 @@
1
- require "csv"
2
- require "yaml"
1
+ # dependencies
3
2
  require "chartkick"
4
3
  require "safely/core"
4
+
5
+ # stdlib
6
+ require "csv"
7
+ require "json"
8
+ require "yaml"
9
+
10
+ # modules
5
11
  require "blazer/version"
6
12
  require "blazer/data_source"
7
13
  require "blazer/result"
8
14
  require "blazer/run_statement"
15
+ require "blazer/statement"
16
+
17
+ # adapters
9
18
  require "blazer/adapters/base_adapter"
19
+ require "blazer/adapters/athena_adapter"
20
+ require "blazer/adapters/bigquery_adapter"
21
+ require "blazer/adapters/cassandra_adapter"
22
+ require "blazer/adapters/drill_adapter"
23
+ require "blazer/adapters/druid_adapter"
10
24
  require "blazer/adapters/elasticsearch_adapter"
25
+ require "blazer/adapters/hive_adapter"
26
+ require "blazer/adapters/ignite_adapter"
27
+ require "blazer/adapters/influxdb_adapter"
11
28
  require "blazer/adapters/mongodb_adapter"
29
+ require "blazer/adapters/neo4j_adapter"
30
+ require "blazer/adapters/opensearch_adapter"
12
31
  require "blazer/adapters/presto_adapter"
32
+ require "blazer/adapters/salesforce_adapter"
33
+ require "blazer/adapters/soda_adapter"
34
+ require "blazer/adapters/spark_adapter"
13
35
  require "blazer/adapters/sql_adapter"
36
+ require "blazer/adapters/snowflake_adapter"
37
+
38
+ # engine
14
39
  require "blazer/engine"
15
40
 
16
41
  module Blazer
17
42
  class Error < StandardError; end
43
+ class UploadError < Error; end
18
44
  class TimeoutNotSupported < Error; end
19
45
 
46
+ # actionmailer optional
47
+ autoload :CheckMailer, "blazer/check_mailer"
48
+ # net/http optional
49
+ autoload :SlackNotifier, "blazer/slack_notifier"
50
+ # activejob optional
51
+ autoload :RunStatementJob, "blazer/run_statement_job"
52
+
20
53
  class << self
21
54
  attr_accessor :audit
22
55
  attr_reader :time_zone
23
56
  attr_accessor :user_name
24
- attr_accessor :user_class
25
- attr_accessor :user_method
57
+ attr_writer :user_class
58
+ attr_writer :user_method
26
59
  attr_accessor :before_action
27
60
  attr_accessor :from_email
28
61
  attr_accessor :cache
29
62
  attr_accessor :transform_statement
63
+ attr_accessor :transform_variable
30
64
  attr_accessor :check_schedules
31
65
  attr_accessor :anomaly_checks
66
+ attr_accessor :forecasting
32
67
  attr_accessor :async
33
68
  attr_accessor :images
69
+ attr_accessor :query_viewable
34
70
  attr_accessor :query_editable
71
+ attr_accessor :override_csp
72
+ attr_accessor :slack_oauth_token
73
+ attr_accessor :slack_webhook_url
74
+ attr_accessor :mapbox_access_token
35
75
  end
36
76
  self.audit = true
37
77
  self.user_name = :name
38
78
  self.check_schedules = ["5 minutes", "1 hour", "1 day"]
39
79
  self.anomaly_checks = false
80
+ self.forecasting = false
40
81
  self.async = false
41
82
  self.images = false
83
+ self.override_csp = false
42
84
 
85
+ VARIABLE_MESSAGE = "Variable cannot be used in this position"
43
86
  TIMEOUT_MESSAGE = "Query timed out :("
44
87
  TIMEOUT_ERRORS = [
45
88
  "canceling statement due to statement timeout", # postgres
@@ -49,13 +92,28 @@ module Blazer
49
92
  "system requested abort", # redshift
50
93
  "maximum statement execution time exceeded" # mysql
51
94
  ]
52
- BELONGS_TO_OPTIONAL = {}
53
- BELONGS_TO_OPTIONAL[:optional] = true if Rails::VERSION::MAJOR >= 5
54
95
 
55
96
  def self.time_zone=(time_zone)
56
97
  @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
57
98
  end
58
99
 
100
+ def self.user_class
101
+ if !defined?(@user_class)
102
+ @user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
103
+ end
104
+ @user_class
105
+ end
106
+
107
+ def self.user_method
108
+ if !defined?(@user_method)
109
+ @user_method = settings["user_method"]
110
+ if user_class
111
+ @user_method ||= "current_#{user_class.to_s.downcase.singularize}"
112
+ end
113
+ end
114
+ @user_method
115
+ end
116
+
59
117
  def self.settings
60
118
  @settings ||= begin
61
119
  path = Rails.root.join("config", "blazer.yml").to_s
@@ -69,23 +127,21 @@ module Blazer
69
127
 
70
128
  def self.data_sources
71
129
  @data_sources ||= begin
72
- ds = Hash[
73
- settings["data_sources"].map do |id, s|
74
- [id, Blazer::DataSource.new(id, s)]
75
- end
76
- ]
77
- ds.default = ds.values.first
130
+ ds = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
131
+ settings["data_sources"].each do |id, s|
132
+ ds[id] = Blazer::DataSource.new(id, s)
133
+ end
78
134
  ds
79
-
80
- # TODO Blazer 2.0
81
- # ds2 = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
82
- # ds.each do |k, v|
83
- # ds2[k] = v
84
- # end
85
- # ds2
86
135
  end
87
136
  end
88
137
 
138
+ # TODO move to Statement and remove in 3.0.0
139
+ def self.extract_vars(statement)
140
+ # strip commented out lines
141
+ # and regex {1} or {1,2}
142
+ statement.to_s.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
143
+ end
144
+
89
145
  def self.run_checks(schedule: nil)
90
146
  checks = Blazer::Check.includes(:query)
91
147
  checks = checks.where(schedule: schedule) if schedule
@@ -96,15 +152,12 @@ module Blazer
96
152
  end
97
153
 
98
154
  def self.run_check(check)
99
- rows = nil
100
- error = nil
101
155
  tries = 1
102
156
 
103
157
  ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
104
158
  # try 3 times on timeout errors
105
- data_source = data_sources[check.query.data_source]
106
- statement = check.query.statement
107
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
159
+ statement = check.query.statement_object
160
+ data_source = statement.data_source
108
161
 
109
162
  while tries <= 3
110
163
  result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
@@ -121,11 +174,19 @@ module Blazer
121
174
  break
122
175
  end
123
176
  end
124
- check.update_state(result)
177
+
178
+ begin
179
+ check.reload # in case state has changed since job started
180
+ check.update_state(result)
181
+ rescue ActiveRecord::RecordNotFound
182
+ # check deleted
183
+ end
184
+
125
185
  # TODO use proper logfmt
126
186
  Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
127
187
 
128
- instrument[:statement] = statement
188
+ # should be no variables
189
+ instrument[:statement] = statement.bind_statement
129
190
  instrument[:data_source] = data_source
130
191
  instrument[:state] = check.state
131
192
  instrument[:rows] = result.rows.try(:size)
@@ -136,10 +197,15 @@ module Blazer
136
197
 
137
198
  def self.send_failing_checks
138
199
  emails = {}
200
+ slack_channels = {}
201
+
139
202
  Blazer::Check.includes(:query).where(state: ["failing", "error", "timed out", "disabled"]).find_each do |check|
140
203
  check.split_emails.each do |email|
141
204
  (emails[email] ||= []) << check
142
205
  end
206
+ check.split_slack_channels.each do |channel|
207
+ (slack_channels[channel] ||= []) << check
208
+ end
143
209
  end
144
210
 
145
211
  emails.each do |email, checks|
@@ -147,5 +213,67 @@ module Blazer
147
213
  Blazer::CheckMailer.failing_checks(email, checks).deliver_now
148
214
  end
149
215
  end
216
+
217
+ slack_channels.each do |channel, checks|
218
+ Safely.safely do
219
+ Blazer::SlackNotifier.failing_checks(channel, checks)
220
+ end
221
+ end
222
+ end
223
+
224
+ def self.slack?
225
+ slack_oauth_token.present? || slack_webhook_url.present?
226
+ end
227
+
228
+ def self.uploads?
229
+ settings.key?("uploads")
230
+ end
231
+
232
+ def self.uploads_connection
233
+ raise "Empty url for uploads" unless settings.dig("uploads", "url")
234
+ Blazer::UploadsConnection.connection
235
+ end
236
+
237
+ def self.uploads_schema
238
+ settings.dig("uploads", "schema") || "uploads"
239
+ end
240
+
241
+ def self.uploads_table_name(name)
242
+ uploads_connection.quote_table_name("#{uploads_schema}.#{name}")
243
+ end
244
+
245
+ def self.adapters
246
+ @adapters ||= {}
247
+ end
248
+
249
+ def self.register_adapter(name, adapter)
250
+ adapters[name] = adapter
251
+ end
252
+
253
+ def self.archive_queries
254
+ raise "Audits must be enabled to archive" unless Blazer.audit
255
+ raise "Missing status column - see https://github.com/ankane/blazer#23" unless Blazer::Query.column_names.include?("status")
256
+
257
+ viewed_query_ids = Blazer::Audit.where("created_at > ?", 90.days.ago).group(:query_id).count.keys.compact
258
+ Blazer::Query.active.where.not(id: viewed_query_ids).update_all(status: "archived")
150
259
  end
151
260
  end
261
+
262
+ Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
263
+ Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
264
+ Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
265
+ Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
266
+ Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
267
+ Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
268
+ Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
269
+ Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
270
+ Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
271
+ Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
272
+ Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
273
+ Blazer.register_adapter "opensearch", Blazer::Adapters::OpensearchAdapter
274
+ Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
275
+ Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
276
+ Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
277
+ Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
278
+ Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
279
+ Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter