blazer 2.4.2 → 2.6.4

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +155 -57
  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/queries.js +12 -1
  7. data/app/assets/javascripts/blazer/vue.js +10754 -9687
  8. data/app/assets/stylesheets/blazer/application.css +5 -0
  9. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  10. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  11. data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +527 -455
  12. data/app/controllers/blazer/base_controller.rb +45 -45
  13. data/app/controllers/blazer/dashboards_controller.rb +4 -11
  14. data/app/controllers/blazer/queries_controller.rb +31 -49
  15. data/app/models/blazer/query.rb +9 -3
  16. data/app/views/blazer/_variables.html.erb +5 -4
  17. data/app/views/blazer/dashboards/_form.html.erb +1 -1
  18. data/app/views/blazer/dashboards/show.html.erb +6 -4
  19. data/app/views/blazer/queries/_caching.html.erb +1 -1
  20. data/app/views/blazer/queries/_form.html.erb +3 -3
  21. data/app/views/blazer/queries/run.html.erb +5 -3
  22. data/app/views/blazer/queries/show.html.erb +12 -7
  23. data/app/views/layouts/blazer/application.html.erb +7 -2
  24. data/lib/blazer/adapters/athena_adapter.rb +73 -20
  25. data/lib/blazer/adapters/base_adapter.rb +16 -1
  26. data/lib/blazer/adapters/bigquery_adapter.rb +14 -3
  27. data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
  28. data/lib/blazer/adapters/drill_adapter.rb +10 -0
  29. data/lib/blazer/adapters/druid_adapter.rb +36 -1
  30. data/lib/blazer/adapters/elasticsearch_adapter.rb +19 -4
  31. data/lib/blazer/adapters/hive_adapter.rb +10 -0
  32. data/lib/blazer/adapters/ignite_adapter.rb +12 -2
  33. data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
  34. data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
  35. data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
  36. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  37. data/lib/blazer/adapters/presto_adapter.rb +9 -0
  38. data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
  39. data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
  40. data/lib/blazer/adapters/soda_adapter.rb +9 -0
  41. data/lib/blazer/adapters/spark_adapter.rb +5 -0
  42. data/lib/blazer/adapters/sql_adapter.rb +41 -4
  43. data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
  44. data/lib/blazer/data_source.rb +90 -8
  45. data/lib/blazer/engine.rb +1 -4
  46. data/lib/blazer/result.rb +19 -1
  47. data/lib/blazer/run_statement.rb +7 -3
  48. data/lib/blazer/run_statement_job.rb +4 -2
  49. data/{app/mailers → lib}/blazer/slack_notifier.rb +19 -4
  50. data/lib/blazer/statement.rb +75 -0
  51. data/lib/blazer/version.rb +1 -1
  52. data/lib/blazer.rb +32 -8
  53. data/lib/generators/blazer/templates/config.yml.tt +2 -2
  54. data/lib/tasks/blazer.rake +5 -5
  55. data/licenses/LICENSE-bootstrap.txt +1 -1
  56. 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,30 @@ 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? && (ActiveRecord::VERSION::STRING.to_f >= 6.1 || prepared_statements?)
195
+ # Active Record < 6.1 silently ignores binds with Postgres when prepared statements are disabled
196
+ :numeric
197
+ elsif sqlite?
198
+ :numeric
199
+ elsif mysql? && prepared_statements?
200
+ # Active Record silently ignores binds with MySQL when prepared statements are disabled
201
+ :positional
202
+ elsif sqlserver?
203
+ proc do |statement, variables|
204
+ variables.each_with_index do |(k, _), i|
205
+ statement = statement.gsub("{#{k}}", "@#{i} ")
206
+ end
207
+ [statement, variables.values]
208
+ end
209
+ end
210
+ end
211
+
186
212
  protected
187
213
 
188
214
  def select_all(statement, params = [])
@@ -207,6 +233,10 @@ module Blazer
207
233
  ["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
208
234
  end
209
235
 
236
+ def sqlite?
237
+ ["SQLite"].include?(adapter_name)
238
+ end
239
+
210
240
  def sqlserver?
211
241
  ["SQLServer", "tinytds", "mssql"].include?(adapter_name)
212
242
  end
@@ -238,6 +268,9 @@ module Blazer
238
268
  if settings["schemas"]
239
269
  where = "table_schema IN (?)"
240
270
  schemas = settings["schemas"]
271
+ elsif mysql?
272
+ where = "table_schema IN (?)"
273
+ schemas = [default_schema]
241
274
  else
242
275
  where = "table_schema NOT IN (?)"
243
276
  schemas = ["information_schema"]
@@ -279,6 +312,10 @@ module Blazer
279
312
  end
280
313
  end
281
314
  end
315
+
316
+ def prepared_statements?
317
+ connection_model.connection.prepared_statements
318
+ end
282
319
  end
283
320
  end
284
321
  end
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.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
@@ -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
@@ -174,9 +176,25 @@ module Blazer
174
176
  def anomaly?(series)
175
177
  series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
176
178
 
177
- if Blazer.anomaly_checks == "trend"
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"
178
192
  anomalies = Trend.anomalies(Hash[series])
179
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])
180
198
  else
181
199
  csv_str =
182
200
  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.4"
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