blazer 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +147 -51
  4. data/app/assets/javascripts/blazer/Chart.js +14000 -13979
  5. data/app/assets/javascripts/blazer/bootstrap.js +300 -97
  6. data/app/assets/javascripts/blazer/vue.js +10754 -9687
  7. data/app/assets/stylesheets/blazer/application.css +5 -0
  8. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  9. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  10. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +527 -455
  11. data/app/controllers/blazer/base_controller.rb +45 -45
  12. data/app/controllers/blazer/dashboards_controller.rb +4 -11
  13. data/app/controllers/blazer/queries_controller.rb +30 -49
  14. data/app/models/blazer/query.rb +9 -3
  15. data/app/views/blazer/_variables.html.erb +5 -4
  16. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  17. data/app/views/blazer/dashboards/show.html.erb +4 -4
  18. data/app/views/blazer/queries/_caching.html.erb +1 -1
  19. data/app/views/blazer/queries/_form.html.erb +3 -3
  20. data/app/views/blazer/queries/run.html.erb +5 -3
  21. data/app/views/blazer/queries/show.html.erb +12 -7
  22. data/app/views/layouts/blazer/application.html.erb +7 -2
  23. data/lib/blazer/adapters/athena_adapter.rb +72 -20
  24. data/lib/blazer/adapters/base_adapter.rb +16 -1
  25. data/lib/blazer/adapters/bigquery_adapter.rb +14 -3
  26. data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
  27. data/lib/blazer/adapters/drill_adapter.rb +10 -0
  28. data/lib/blazer/adapters/druid_adapter.rb +36 -1
  29. data/lib/blazer/adapters/elasticsearch_adapter.rb +19 -4
  30. data/lib/blazer/adapters/hive_adapter.rb +10 -0
  31. data/lib/blazer/adapters/ignite_adapter.rb +12 -2
  32. data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
  33. data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
  34. data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
  35. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  36. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  37. data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
  38. data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
  39. data/lib/blazer/adapters/soda_adapter.rb +9 -0
  40. data/lib/blazer/adapters/spark_adapter.rb +5 -0
  41. data/lib/blazer/adapters/sql_adapter.rb +34 -4
  42. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  43. data/lib/blazer/data_source.rb +90 -8
  44. data/lib/blazer/engine.rb +1 -4
  45. data/lib/blazer/result.rb +17 -1
  46. data/lib/blazer/run_statement.rb +7 -3
  47. data/lib/blazer/run_statement_job.rb +4 -2
  48. data/{app/mailers → lib}/blazer/slack_notifier.rb +19 -4
  49. data/lib/blazer/statement.rb +75 -0
  50. data/lib/blazer/version.rb +1 -1
  51. data/lib/blazer.rb +32 -8
  52. data/lib/generators/blazer/templates/config.yml.tt +2 -2
  53. data/lib/tasks/blazer.rake +5 -5
  54. data/licenses/LICENSE-bootstrap.txt +1 -1
  55. metadata +10 -6
@@ -4,6 +4,11 @@ module Blazer
4
4
  def tables
5
5
  client.execute("SHOW TABLES").map { |r| r["tableName"] }
6
6
  end
7
+
8
+ # https://spark.apache.org/docs/latest/sql-ref-literals.html
9
+ def quoting
10
+ :backslash_escape
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -15,7 +15,7 @@ module Blazer
15
15
  end
16
16
  end
17
17
 
18
- def run_statement(statement, comment)
18
+ def run_statement(statement, comment, bind_params = [])
19
19
  columns = []
20
20
  rows = []
21
21
  error = nil
@@ -24,7 +24,8 @@ module Blazer
24
24
  in_transaction do
25
25
  set_timeout(data_source.timeout) if data_source.timeout
26
26
 
27
- result = select_all("#{statement} /*#{comment}*/")
27
+ binds = bind_params.map { |v| ActiveRecord::Relation::QueryAttribute.new(nil, v, ActiveRecord::Type::Value.new) }
28
+ result = connection_model.connection.select_all("#{statement} /*#{comment}*/", nil, binds)
28
29
  columns = result.columns
29
30
  result.rows.each do |untyped_row|
30
31
  rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
@@ -33,6 +34,7 @@ module Blazer
33
34
  rescue => e
34
35
  error = e.message.sub(/.+ERROR: /, "")
35
36
  error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
37
+ error = Blazer::VARIABLE_MESSAGE if error.include?("syntax error at or near \"$") || error.include?("Incorrect syntax near '@") || error.include?("your MySQL server version for the right syntax to use near '?")
36
38
  reconnect if error.include?("PG::ConnectionBad")
37
39
  end
38
40
 
@@ -146,7 +148,7 @@ module Blazer
146
148
  date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-01') AS DATE)"
147
149
  date_params = [tzname]
148
150
  end
149
- bucket_sql = "CAST(CEIL(TIMESTAMPDIFF(SECOND, cohorts.cohort_time, query.conversion_time) / ?) AS INTEGER)"
151
+ bucket_sql = "CAST(CEIL(TIMESTAMPDIFF(SECOND, cohorts.cohort_time, query.conversion_time) / ?) AS SIGNED)"
150
152
  else
151
153
  date_sql = "date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date"
152
154
  date_params = [period, tzname]
@@ -156,7 +158,7 @@ module Blazer
156
158
  # WITH not an optimization fence in Postgres 12+
157
159
  statement = <<~SQL
158
160
  WITH query AS (
159
- #{statement}
161
+ {placeholder}
160
162
  ),
161
163
  cohorts AS (
162
164
  SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
@@ -183,6 +185,27 @@ module Blazer
183
185
  connection_model.send(:sanitize_sql_array, params)
184
186
  end
185
187
 
188
+ def quoting
189
+ ->(value) { connection_model.connection.quote(value) }
190
+ end
191
+
192
+ # Redshift adapter silently ignores binds
193
+ def parameter_binding
194
+ if postgresql? || sqlite?
195
+ :numeric
196
+ elsif mysql? && connection_model.connection.prepared_statements?
197
+ # Active Record silently ignores binds with MySQL when prepared statements are disabled
198
+ :positional
199
+ elsif sqlserver?
200
+ proc do |statement, variables|
201
+ variables.each_with_index do |(k, _), i|
202
+ statement = statement.gsub("{#{k}}", "@#{i} ")
203
+ end
204
+ [statement, variables.values]
205
+ end
206
+ end
207
+ end
208
+
186
209
  protected
187
210
 
188
211
  def select_all(statement, params = [])
@@ -207,6 +230,10 @@ module Blazer
207
230
  ["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
208
231
  end
209
232
 
233
+ def sqlite?
234
+ ["SQLite"].include?(adapter_name)
235
+ end
236
+
210
237
  def sqlserver?
211
238
  ["SQLServer", "tinytds", "mssql"].include?(adapter_name)
212
239
  end
@@ -238,6 +265,9 @@ module Blazer
238
265
  if settings["schemas"]
239
266
  where = "table_schema IN (?)"
240
267
  schemas = settings["schemas"]
268
+ elsif mysql?
269
+ where = "table_schema IN (?)"
270
+ schemas = [default_schema]
241
271
  else
242
272
  where = "table_schema NOT IN (?)"
243
273
  schemas = ["information_schema"]
File without changes
@@ -89,7 +89,19 @@ module Blazer
89
89
  Blazer.cache.delete(run_cache_key(run_id))
90
90
  end
91
91
 
92
+ def sub_variables(statement, vars)
93
+ statement = statement.dup
94
+ vars.each do |var, value|
95
+ # use block form to disable back-references
96
+ statement.gsub!("{#{var}}") { quote(value) }
97
+ end
98
+ statement
99
+ end
100
+
92
101
  def run_statement(statement, options = {})
102
+ statement = Statement.new(statement, self) if statement.is_a?(String)
103
+ statement.bind unless statement.bind_statement
104
+
93
105
  async = options[:async]
94
106
  result = nil
95
107
  if cache_mode != "off"
@@ -118,7 +130,7 @@ module Blazer
118
130
  if options[:run_id]
119
131
  comment << ",run_id:#{options[:run_id]}"
120
132
  end
121
- result = run_statement_helper(statement, comment, async ? options[:run_id] : nil)
133
+ result = run_statement_helper(statement, comment, async ? options[:run_id] : nil, options)
122
134
  end
123
135
 
124
136
  result
@@ -133,17 +145,73 @@ module Blazer
133
145
  end
134
146
 
135
147
  def statement_cache_key(statement)
136
- cache_key(["statement", id, Digest::MD5.hexdigest(statement.to_s.gsub("\r\n", "\n"))])
148
+ cache_key(["statement", id, Digest::MD5.hexdigest(statement.bind_statement.to_s.gsub("\r\n", "\n") + statement.bind_values.sort_by { |k, _| k }.to_json)])
137
149
  end
138
150
 
139
151
  def run_cache_key(run_id)
140
152
  cache_key(["run", run_id])
141
153
  end
142
154
 
155
+ def quote(value)
156
+ if quoting == :backslash_escape || quoting == :single_quote_escape
157
+ # only need to support types generated by process_vars
158
+ if value.is_a?(Integer) || value.is_a?(Float)
159
+ value.to_s
160
+ elsif value.nil?
161
+ "NULL"
162
+ else
163
+ value = value.to_formatted_s(:db) if value.is_a?(ActiveSupport::TimeWithZone)
164
+
165
+ if quoting == :backslash_escape
166
+ "'#{value.gsub("\\") { "\\\\" }.gsub("'") { "\\'" }}'"
167
+ else
168
+ "'#{value.gsub("'", "''")}'"
169
+ end
170
+ end
171
+ elsif quoting.respond_to?(:call)
172
+ quoting.call(value)
173
+ elsif quoting.nil?
174
+ raise Blazer::Error, "Quoting not specified"
175
+ else
176
+ raise Blazer::Error, "Unknown quoting"
177
+ end
178
+ end
179
+
180
+ def bind_params(statement, variables)
181
+ if parameter_binding == :positional
182
+ locations = []
183
+ variables.each do |k, v|
184
+ i = 0
185
+ while (idx = statement.index("{#{k}}", i))
186
+ locations << [v, idx]
187
+ i = idx + 1
188
+ end
189
+ end
190
+ variables.each do |k, v|
191
+ statement = statement.gsub("{#{k}}", "?")
192
+ end
193
+ [statement, locations.sort_by(&:last).map(&:first)]
194
+ elsif parameter_binding == :numeric
195
+ variables.each_with_index do |(k, v), i|
196
+ # add trailing space if followed by digit
197
+ # try to keep minimal to avoid fixing invalid queries like SELECT{var}
198
+ statement = statement.gsub(/#{Regexp.escape("{#{k}}")}(\d)/, "$#{i + 1} \\1").gsub("{#{k}}", "$#{i + 1}")
199
+ end
200
+ [statement, variables.values]
201
+ elsif parameter_binding.respond_to?(:call)
202
+ parameter_binding.call(statement, variables)
203
+ elsif parameter_binding.nil?
204
+ [sub_variables(statement, variables), []]
205
+ else
206
+ raise Blazer::Error, "Unknown bind parameters"
207
+ end
208
+ end
209
+
143
210
  protected
144
211
 
145
212
  def adapter_instance
146
213
  @adapter_instance ||= begin
214
+ # TODO add required settings to adapters
147
215
  unless settings["url"] || Rails.env.development? || ["bigquery", "athena", "snowflake", "salesforce"].include?(settings["adapter"])
148
216
  raise Blazer::Error, "Empty url for data source: #{id}"
149
217
  end
@@ -156,9 +224,22 @@ module Blazer
156
224
  end
157
225
  end
158
226
 
159
- def run_statement_helper(statement, comment, run_id)
227
+ def quoting
228
+ @quoting ||= adapter_instance.quoting
229
+ end
230
+
231
+ def parameter_binding
232
+ @parameter_binding ||= adapter_instance.parameter_binding
233
+ end
234
+
235
+ def run_statement_helper(statement, comment, run_id, options)
160
236
  start_time = Time.now
161
- columns, rows, error = adapter_instance.run_statement(statement, comment)
237
+ columns, rows, error =
238
+ if adapter_instance.parameter_binding
239
+ adapter_instance.run_statement(statement.bind_statement, comment, statement.bind_values)
240
+ else
241
+ adapter_instance.run_statement(statement.bind_statement, comment)
242
+ end
162
243
  duration = Time.now - start_time
163
244
 
164
245
  cache_data = nil
@@ -167,7 +248,7 @@ module Blazer
167
248
  cache_data = Marshal.dump([columns, rows, error, cache ? Time.now : nil]) rescue nil
168
249
  end
169
250
 
170
- if cache && cache_data && adapter_instance.cachable?(statement)
251
+ if cache && cache_data && adapter_instance.cachable?(statement.bind_statement)
171
252
  Blazer.cache.write(statement_cache_key(statement), cache_data, expires_in: cache_expires_in.to_f * 60)
172
253
  end
173
254
 
@@ -182,11 +263,12 @@ module Blazer
182
263
  Blazer::Result.new(self, columns, rows, error, nil, cache && !cache_data.nil?)
183
264
  end
184
265
 
266
+ # TODO check for adapter with same name, default to sql
185
267
  def detect_adapter
186
- schema = settings["url"].to_s.split("://").first
187
- case schema
268
+ scheme = settings["url"].to_s.split("://").first
269
+ case scheme
188
270
  when "mongodb", "presto", "cassandra", "ignite"
189
- schema
271
+ scheme
190
272
  else
191
273
  "sql"
192
274
  end
data/lib/blazer/engine.rb CHANGED
@@ -30,12 +30,9 @@ module Blazer
30
30
  Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
31
31
  Blazer.forecasting = Blazer.settings["forecasting"] || false
32
32
  Blazer.async = Blazer.settings["async"] || false
33
- if Blazer.async
34
- require "blazer/run_statement_job"
35
- end
36
-
37
33
  Blazer.images = Blazer.settings["images"] || false
38
34
  Blazer.override_csp = Blazer.settings["override_csp"] || false
35
+ Blazer.slack_oauth_token = Blazer.settings["slack_oauth_token"] || ENV["BLAZER_SLACK_OAUTH_TOKEN"]
39
36
  Blazer.slack_webhook_url = Blazer.settings["slack_webhook_url"] || ENV["BLAZER_SLACK_WEBHOOK_URL"]
40
37
  Blazer.mapbox_access_token = Blazer.settings["mapbox_access_token"] || ENV["MAPBOX_ACCESS_TOKEN"]
41
38
  end
data/lib/blazer/result.rb CHANGED
@@ -174,9 +174,25 @@ module Blazer
174
174
  def anomaly?(series)
175
175
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
176
176
 
177
- if Blazer.anomaly_checks == "trend"
177
+ case Blazer.anomaly_checks
178
+ when "prophet"
179
+ df = Rover::DataFrame.new(series[0..-2].map { |v| {"ds" => v[0], "y" => v[1]} })
180
+ m = Prophet.new(interval_width: 0.99)
181
+ m.logger.level = ::Logger::FATAL # no logging
182
+ m.fit(df)
183
+ future = Rover::DataFrame.new(series[-1..-1].map { |v| {"ds" => v[0]} })
184
+ forecast = m.predict(future).to_a[0]
185
+ lower = forecast["yhat_lower"]
186
+ upper = forecast["yhat_upper"]
187
+ value = series.last[1]
188
+ value < lower || value > upper
189
+ when "trend"
178
190
  anomalies = Trend.anomalies(Hash[series])
179
191
  anomalies.include?(series.last[0])
192
+ when "anomaly_detection"
193
+ period = 7 # TODO determine period
194
+ anomalies = AnomalyDetection.detect(Hash[series], period: period)
195
+ anomalies.include?(series.last[0])
180
196
  else
181
197
  csv_str =
182
198
  CSV.generate do |csv|
@@ -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]
@@ -3,10 +3,12 @@ module Blazer
3
3
  self.queue_adapter = :async
4
4
 
5
5
  def perform(data_source_id, statement, options)
6
- data_source = Blazer.data_sources[data_source_id]
6
+ statement = Blazer::Statement.new(statement, data_source_id)
7
+ statement.values = options.delete(:values)
8
+ data_source = statement.data_source
7
9
  begin
8
10
  ActiveRecord::Base.connection_pool.with_connection do
9
- Blazer::RunStatement.new.perform(data_source, statement, options)
11
+ Blazer::RunStatement.new.perform(statement, options)
10
12
  end
11
13
  rescue Exception => e
12
14
  Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
@@ -23,7 +23,7 @@ module Blazer
23
23
  ]
24
24
  }
25
25
 
26
- post(Blazer.slack_webhook_url, payload)
26
+ post(payload)
27
27
  end
28
28
  end
29
29
 
@@ -44,7 +44,7 @@ module Blazer
44
44
  ]
45
45
  }
46
46
 
47
- post(Blazer.slack_webhook_url, payload)
47
+ post(payload)
48
48
  end
49
49
 
50
50
  # https://api.slack.com/docs/message-formatting#how_to_escape_characters
@@ -67,13 +67,28 @@ module Blazer
67
67
  Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
68
68
  end
69
69
 
70
- def self.post(url, payload)
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)
71
86
  uri = URI.parse(url)
72
87
  http = Net::HTTP.new(uri.host, uri.port)
73
88
  http.use_ssl = true
74
89
  http.open_timeout = 3
75
90
  http.read_timeout = 5
76
- http.post(uri.request_uri, payload.to_json)
91
+ http.post(uri.request_uri, payload.to_json, headers)
77
92
  end
78
93
  end
79
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 = "2.4.2"
2
+ VERSION = "2.6.0"
3
3
  end
data/lib/blazer.rb CHANGED
@@ -1,14 +1,18 @@
1
1
  # dependencies
2
- require "csv"
3
- require "yaml"
4
2
  require "chartkick"
5
3
  require "safely/core"
6
4
 
5
+ # stdlib
6
+ require "csv"
7
+ require "json"
8
+ require "yaml"
9
+
7
10
  # modules
8
11
  require "blazer/version"
9
12
  require "blazer/data_source"
10
13
  require "blazer/result"
11
14
  require "blazer/run_statement"
15
+ require "blazer/statement"
12
16
 
13
17
  # adapters
14
18
  require "blazer/adapters/base_adapter"
@@ -23,6 +27,7 @@ require "blazer/adapters/ignite_adapter"
23
27
  require "blazer/adapters/influxdb_adapter"
24
28
  require "blazer/adapters/mongodb_adapter"
25
29
  require "blazer/adapters/neo4j_adapter"
30
+ require "blazer/adapters/opensearch_adapter"
26
31
  require "blazer/adapters/presto_adapter"
27
32
  require "blazer/adapters/salesforce_adapter"
28
33
  require "blazer/adapters/soda_adapter"
@@ -38,6 +43,13 @@ module Blazer
38
43
  class UploadError < Error; end
39
44
  class TimeoutNotSupported < Error; end
40
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
+
41
53
  class << self
42
54
  attr_accessor :audit
43
55
  attr_reader :time_zone
@@ -57,6 +69,7 @@ module Blazer
57
69
  attr_accessor :query_viewable
58
70
  attr_accessor :query_editable
59
71
  attr_accessor :override_csp
72
+ attr_accessor :slack_oauth_token
60
73
  attr_accessor :slack_webhook_url
61
74
  attr_accessor :mapbox_access_token
62
75
  end
@@ -69,6 +82,7 @@ module Blazer
69
82
  self.images = false
70
83
  self.override_csp = false
71
84
 
85
+ VARIABLE_MESSAGE = "Variable cannot be used in this position"
72
86
  TIMEOUT_MESSAGE = "Query timed out :("
73
87
  TIMEOUT_ERRORS = [
74
88
  "canceling statement due to statement timeout", # postgres
@@ -121,6 +135,7 @@ module Blazer
121
135
  end
122
136
  end
123
137
 
138
+ # TODO move to Statement and remove in 3.0.0
124
139
  def self.extract_vars(statement)
125
140
  # strip commented out lines
126
141
  # and regex {1} or {1,2}
@@ -141,9 +156,8 @@ module Blazer
141
156
 
142
157
  ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
143
158
  # try 3 times on timeout errors
144
- data_source = data_sources[check.query.data_source]
145
- statement = check.query.statement
146
- Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
159
+ statement = check.query.statement_object
160
+ data_source = statement.data_source
147
161
 
148
162
  while tries <= 3
149
163
  result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
@@ -171,7 +185,8 @@ module Blazer
171
185
  # TODO use proper logfmt
172
186
  Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
173
187
 
174
- instrument[:statement] = statement
188
+ # should be no variables
189
+ instrument[:statement] = statement.bind_statement
175
190
  instrument[:data_source] = data_source
176
191
  instrument[:state] = check.state
177
192
  instrument[:rows] = result.rows.try(:size)
@@ -207,7 +222,7 @@ module Blazer
207
222
  end
208
223
 
209
224
  def self.slack?
210
- slack_webhook_url.present?
225
+ slack_oauth_token.present? || slack_webhook_url.present?
211
226
  end
212
227
 
213
228
  def self.uploads?
@@ -234,6 +249,14 @@ module Blazer
234
249
  def self.register_adapter(name, adapter)
235
250
  adapters[name] = adapter
236
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")
259
+ end
237
260
  end
238
261
 
239
262
  Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
@@ -245,9 +268,10 @@ Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
245
268
  Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
246
269
  Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
247
270
  Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
271
+ Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
248
272
  Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
273
+ Blazer.register_adapter "opensearch", Blazer::Adapters::OpensearchAdapter
249
274
  Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
250
- Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
251
275
  Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
252
276
  Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
253
277
  Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
@@ -63,11 +63,11 @@ check_schedules:
63
63
 
64
64
  # enable anomaly detection
65
65
  # note: with trend, time series are sent to https://trendapi.org
66
- # anomaly_checks: trend / r
66
+ # anomaly_checks: prophet / trend / anomaly_detection
67
67
 
68
68
  # enable forecasting
69
69
  # note: with trend, time series are sent to https://trendapi.org
70
- # forecasting: trend / prophet
70
+ # forecasting: prophet / trend
71
71
 
72
72
  # enable map
73
73
  # mapbox_access_token: <%%= ENV["MAPBOX_ACCESS_TOKEN"] %>
@@ -11,10 +11,10 @@ namespace :blazer do
11
11
 
12
12
  desc "archive queries"
13
13
  task archive_queries: :environment do
14
- abort "Audits must be enabled to archive" unless Blazer.audit
15
- abort "Missing status column - see https://github.com/ankane/blazer#23" unless Blazer::Query.column_names.include?("status")
16
-
17
- viewed_query_ids = Blazer::Audit.where("created_at > ?", 90.days.ago).group(:query_id).count.keys.compact
18
- Blazer::Query.active.where.not(id: viewed_query_ids).update_all(status: "archived")
14
+ begin
15
+ Blazer.archive_queries
16
+ rescue => e
17
+ abort e.message
18
+ end
19
19
  end
20
20
  end
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2011-2016 Twitter, Inc.
3
+ Copyright (c) 2011-2019 Twitter, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal