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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +155 -57
- data/app/assets/javascripts/blazer/Chart.js +14000 -13979
- data/app/assets/javascripts/blazer/bootstrap.js +300 -97
- data/app/assets/javascripts/blazer/queries.js +12 -1
- data/app/assets/javascripts/blazer/vue.js +10754 -9687
- data/app/assets/stylesheets/blazer/application.css +5 -0
- data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
- data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
- data/app/assets/stylesheets/blazer/{bootstrap.css.erb → bootstrap.css} +527 -455
- data/app/controllers/blazer/base_controller.rb +45 -45
- data/app/controllers/blazer/dashboards_controller.rb +4 -11
- data/app/controllers/blazer/queries_controller.rb +31 -49
- data/app/models/blazer/query.rb +9 -3
- data/app/views/blazer/_variables.html.erb +5 -4
- data/app/views/blazer/dashboards/_form.html.erb +1 -1
- data/app/views/blazer/dashboards/show.html.erb +6 -4
- data/app/views/blazer/queries/_caching.html.erb +1 -1
- data/app/views/blazer/queries/_form.html.erb +3 -3
- data/app/views/blazer/queries/run.html.erb +5 -3
- data/app/views/blazer/queries/show.html.erb +12 -7
- data/app/views/layouts/blazer/application.html.erb +7 -2
- data/lib/blazer/adapters/athena_adapter.rb +73 -20
- data/lib/blazer/adapters/base_adapter.rb +16 -1
- data/lib/blazer/adapters/bigquery_adapter.rb +14 -3
- data/lib/blazer/adapters/cassandra_adapter.rb +15 -4
- data/lib/blazer/adapters/drill_adapter.rb +10 -0
- data/lib/blazer/adapters/druid_adapter.rb +36 -1
- data/lib/blazer/adapters/elasticsearch_adapter.rb +19 -4
- data/lib/blazer/adapters/hive_adapter.rb +10 -0
- data/lib/blazer/adapters/ignite_adapter.rb +12 -2
- data/lib/blazer/adapters/influxdb_adapter.rb +22 -10
- data/lib/blazer/adapters/mongodb_adapter.rb +4 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +17 -2
- data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
- data/lib/blazer/adapters/presto_adapter.rb +9 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +5 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +9 -0
- data/lib/blazer/adapters/soda_adapter.rb +9 -0
- data/lib/blazer/adapters/spark_adapter.rb +5 -0
- data/lib/blazer/adapters/sql_adapter.rb +41 -4
- data/{app/mailers → lib}/blazer/check_mailer.rb +0 -0
- data/lib/blazer/data_source.rb +90 -8
- data/lib/blazer/engine.rb +1 -4
- data/lib/blazer/result.rb +19 -1
- data/lib/blazer/run_statement.rb +7 -3
- data/lib/blazer/run_statement_job.rb +4 -2
- data/{app/mailers → lib}/blazer/slack_notifier.rb +19 -4
- data/lib/blazer/statement.rb +75 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/blazer.rb +32 -8
- data/lib/generators/blazer/templates/config.yml.tt +2 -2
- data/lib/tasks/blazer.rake +5 -5
- data/licenses/LICENSE-bootstrap.txt +1 -1
- metadata +10 -6
@@ -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
|
-
|
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
|
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
|
-
|
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
|
data/lib/blazer/data_source.rb
CHANGED
@@ -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
|
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 =
|
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
|
-
|
187
|
-
case
|
268
|
+
scheme = settings["url"].to_s.split("://").first
|
269
|
+
case scheme
|
188
270
|
when "mongodb", "presto", "cassandra", "ignite"
|
189
|
-
|
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
|
-
|
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|
|
data/lib/blazer/run_statement.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
module Blazer
|
2
2
|
class RunStatement
|
3
|
-
def perform(
|
3
|
+
def perform(statement, options = {})
|
4
4
|
query = options[:query]
|
5
|
-
|
5
|
+
|
6
|
+
data_source = statement.data_source
|
7
|
+
statement.bind
|
6
8
|
|
7
9
|
# audit
|
8
10
|
if Blazer.audit
|
9
|
-
|
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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
data/lib/blazer/version.rb
CHANGED
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
|
-
|
145
|
-
|
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
|
-
|
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 /
|
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:
|
70
|
+
# forecasting: prophet / trend
|
71
71
|
|
72
72
|
# enable map
|
73
73
|
# mapbox_access_token: <%%= ENV["MAPBOX_ACCESS_TOKEN"] %>
|
data/lib/tasks/blazer.rake
CHANGED
@@ -11,10 +11,10 @@ namespace :blazer do
|
|
11
11
|
|
12
12
|
desc "archive queries"
|
13
13
|
task archive_queries: :environment do
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
begin
|
15
|
+
Blazer.archive_queries
|
16
|
+
rescue => e
|
17
|
+
abort e.message
|
18
|
+
end
|
19
19
|
end
|
20
20
|
end
|