blazer_xlsx 3.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +442 -0
- data/CONTRIBUTING.md +42 -0
- data/LICENSE.txt +22 -0
- data/README.md +1093 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
- data/app/assets/images/blazer/favicon.png +0 -0
- data/app/assets/javascripts/blazer/Sortable.js +3709 -0
- data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/application.js +84 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
- data/app/assets/javascripts/blazer/chart.umd.js +13 -0
- data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
- data/app/assets/javascripts/blazer/chartkick.js +2570 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.min.js +466 -0
- data/app/assets/javascripts/blazer/jquery.js +10872 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
- data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
- data/app/assets/javascripts/blazer/moment.js +5685 -0
- data/app/assets/javascripts/blazer/queries.js +130 -0
- data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
- data/app/assets/javascripts/blazer/routes.js +26 -0
- data/app/assets/javascripts/blazer/selectize.js +3891 -0
- data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
- data/app/assets/javascripts/blazer/stupidtable.js +281 -0
- data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
- data/app/assets/stylesheets/blazer/application.css +243 -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 +6828 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.css +403 -0
- data/app/controllers/blazer/base_controller.rb +135 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +99 -0
- data/app/controllers/blazer/queries_controller.rb +472 -0
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/helpers/blazer/base_helper.rb +39 -0
- data/app/models/blazer/audit.rb +6 -0
- data/app/models/blazer/check.rb +104 -0
- data/app/models/blazer/connection.rb +5 -0
- data/app/models/blazer/dashboard.rb +17 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +42 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +18 -0
- data/app/views/blazer/_variables.html.erb +127 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
- data/app/views/blazer/checks/_form.html.erb +79 -0
- data/app/views/blazer/checks/edit.html.erb +3 -0
- data/app/views/blazer/checks/index.html.erb +72 -0
- data/app/views/blazer/checks/new.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +82 -0
- data/app/views/blazer/dashboards/edit.html.erb +3 -0
- data/app/views/blazer/dashboards/new.html.erb +3 -0
- data/app/views/blazer/dashboards/show.html.erb +53 -0
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/_form.html.erb +255 -0
- data/app/views/blazer/queries/docs.html.erb +147 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +169 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +183 -0
- data/app/views/blazer/queries/schema.html.erb +55 -0
- data/app/views/blazer/queries/show.html.erb +72 -0
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/app/views/layouts/blazer/application.html.erb +25 -0
- data/config/routes.rb +25 -0
- data/lib/blazer/adapters/athena_adapter.rb +182 -0
- data/lib/blazer/adapters/base_adapter.rb +76 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
- data/lib/blazer/adapters/drill_adapter.rb +38 -0
- data/lib/blazer/adapters/druid_adapter.rb +102 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
- data/lib/blazer/adapters/hive_adapter.rb +55 -0
- data/lib/blazer/adapters/ignite_adapter.rb +64 -0
- data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
- data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
- data/lib/blazer/adapters/presto_adapter.rb +54 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
- data/lib/blazer/adapters/soda_adapter.rb +105 -0
- data/lib/blazer/adapters/spark_adapter.rb +14 -0
- data/lib/blazer/adapters/sql_adapter.rb +353 -0
- data/lib/blazer/adapters.rb +17 -0
- data/lib/blazer/anomaly_detectors.rb +22 -0
- data/lib/blazer/check_mailer.rb +27 -0
- data/lib/blazer/data_source.rb +266 -0
- data/lib/blazer/engine.rb +42 -0
- data/lib/blazer/forecasters.rb +7 -0
- data/lib/blazer/result.rb +178 -0
- data/lib/blazer/result_cache.rb +71 -0
- data/lib/blazer/run_statement.rb +45 -0
- data/lib/blazer/run_statement_job.rb +20 -0
- data/lib/blazer/slack_notifier.rb +94 -0
- data/lib/blazer/statement.rb +77 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/blazer.rb +282 -0
- data/lib/generators/blazer/install_generator.rb +22 -0
- data/lib/generators/blazer/templates/config.yml.tt +79 -0
- data/lib/generators/blazer/templates/install.rb.tt +47 -0
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +20 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-date-fns.txt +21 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-rails-ujs.txt +20 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- metadata +271 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace Blazer
|
4
|
+
|
5
|
+
initializer "blazer" do |app|
|
6
|
+
if defined?(Sprockets) && Sprockets::VERSION.to_i >= 4
|
7
|
+
app.config.assets.precompile += [
|
8
|
+
"blazer/application.js",
|
9
|
+
"blazer/application.css",
|
10
|
+
"blazer/glyphicons-halflings-regular.eot",
|
11
|
+
"blazer/glyphicons-halflings-regular.svg",
|
12
|
+
"blazer/glyphicons-halflings-regular.ttf",
|
13
|
+
"blazer/glyphicons-halflings-regular.woff",
|
14
|
+
"blazer/glyphicons-halflings-regular.woff2",
|
15
|
+
"blazer/favicon.png"
|
16
|
+
]
|
17
|
+
else
|
18
|
+
# use a proc instead of a string
|
19
|
+
app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
|
20
|
+
app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.(eot|svg|ttf|woff|woff2)\z/ }
|
21
|
+
app.config.assets.precompile << proc { |path| path == "blazer/favicon.png" }
|
22
|
+
end
|
23
|
+
|
24
|
+
Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
|
25
|
+
Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
|
26
|
+
Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
|
27
|
+
Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
|
28
|
+
Blazer.before_action = Blazer.settings["before_action_method"] if Blazer.settings["before_action_method"]
|
29
|
+
Blazer.check_schedules = Blazer.settings["check_schedules"] if Blazer.settings.key?("check_schedules")
|
30
|
+
Blazer.cache ||= Rails.cache
|
31
|
+
|
32
|
+
Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
|
33
|
+
Blazer.forecasting = Blazer.settings["forecasting"] || false
|
34
|
+
Blazer.async = Blazer.settings["async"] || false
|
35
|
+
Blazer.images = Blazer.settings["images"] || false
|
36
|
+
Blazer.override_csp = Blazer.settings["override_csp"] || false
|
37
|
+
Blazer.slack_oauth_token = Blazer.settings["slack_oauth_token"] || ENV["BLAZER_SLACK_OAUTH_TOKEN"]
|
38
|
+
Blazer.slack_webhook_url = Blazer.settings["slack_webhook_url"] || ENV["BLAZER_SLACK_WEBHOOK_URL"]
|
39
|
+
Blazer.mapbox_access_token = Blazer.settings["mapbox_access_token"] || ENV["MAPBOX_ACCESS_TOKEN"]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Result
|
3
|
+
attr_reader :data_source, :columns, :rows, :error, :forecast_error
|
4
|
+
attr_accessor :cached_at, :just_cached
|
5
|
+
|
6
|
+
def initialize(data_source, columns, rows, error, cached_at, just_cached)
|
7
|
+
@data_source = data_source
|
8
|
+
@columns = columns
|
9
|
+
@rows = rows
|
10
|
+
@error = error
|
11
|
+
@cached_at = cached_at
|
12
|
+
@just_cached = just_cached
|
13
|
+
end
|
14
|
+
|
15
|
+
def timed_out?
|
16
|
+
error == Blazer::TIMEOUT_MESSAGE
|
17
|
+
end
|
18
|
+
|
19
|
+
def cached?
|
20
|
+
cached_at.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def smart_values
|
24
|
+
@smart_values ||= begin
|
25
|
+
smart_values = {}
|
26
|
+
columns.each_with_index do |key, i|
|
27
|
+
smart_columns_data_source =
|
28
|
+
([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_columns[key] }
|
29
|
+
|
30
|
+
if smart_columns_data_source
|
31
|
+
query = smart_columns_data_source.smart_columns[key]
|
32
|
+
res =
|
33
|
+
if query.is_a?(Hash)
|
34
|
+
query
|
35
|
+
else
|
36
|
+
values = rows.map { |r| r[i] }.compact.uniq
|
37
|
+
result = smart_columns_data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
|
38
|
+
result.rows
|
39
|
+
end
|
40
|
+
|
41
|
+
smart_values[key] = res.to_h { |k, v| [k.nil? ? k : k.to_s, v] }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
smart_values
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def column_types
|
49
|
+
@column_types ||= begin
|
50
|
+
columns.each_with_index.map do |k, i|
|
51
|
+
v = (rows.find { |r| r[i] } || {})[i]
|
52
|
+
if smart_values[k]
|
53
|
+
"string"
|
54
|
+
elsif v.is_a?(Numeric)
|
55
|
+
"numeric"
|
56
|
+
elsif v.is_a?(Time) || v.is_a?(Date)
|
57
|
+
"time"
|
58
|
+
elsif v.nil?
|
59
|
+
nil
|
60
|
+
elsif v.is_a?(String) && v.encoding == Encoding::BINARY
|
61
|
+
"binary"
|
62
|
+
else
|
63
|
+
"string"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def chart_type
|
70
|
+
@chart_type ||= begin
|
71
|
+
if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
|
72
|
+
"line"
|
73
|
+
elsif column_types == ["time", "string", "numeric"]
|
74
|
+
"line2"
|
75
|
+
elsif column_types == ["string", "numeric"] && @columns.last == "pie"
|
76
|
+
"pie"
|
77
|
+
elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
|
78
|
+
"bar"
|
79
|
+
elsif column_types == ["string", "string", "numeric"]
|
80
|
+
"bar2"
|
81
|
+
elsif column_types == ["numeric", "numeric"]
|
82
|
+
"scatter"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def forecastable?
|
88
|
+
@forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
|
89
|
+
end
|
90
|
+
|
91
|
+
# TODO cache it?
|
92
|
+
# don't want to put result data (even hashed version)
|
93
|
+
# into cache without developer opt-in
|
94
|
+
def forecast
|
95
|
+
count = (@rows.size * 0.25).round.clamp(30, 365)
|
96
|
+
|
97
|
+
forecaster = Blazer.forecasters.fetch(Blazer.forecasting)
|
98
|
+
forecast = forecaster.call(@rows.to_h, count: count)
|
99
|
+
|
100
|
+
# round integers
|
101
|
+
if @rows[0][1].is_a?(Integer)
|
102
|
+
forecast = forecast.map { |k, v| [k, v.round] }.to_h
|
103
|
+
end
|
104
|
+
|
105
|
+
@rows.each do |row|
|
106
|
+
row[2] = nil
|
107
|
+
end
|
108
|
+
@rows.unshift(*forecast.map { |k, v| [k, nil, v] })
|
109
|
+
@columns << "forecast"
|
110
|
+
|
111
|
+
# reset cache
|
112
|
+
@column_types = nil
|
113
|
+
@chart_type = nil
|
114
|
+
|
115
|
+
forecast
|
116
|
+
rescue => e
|
117
|
+
@forecast_error = String.new("Error generating forecast")
|
118
|
+
@forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def detect_anomaly
|
123
|
+
anomaly = nil
|
124
|
+
message = nil
|
125
|
+
|
126
|
+
if rows.empty?
|
127
|
+
message = "No data"
|
128
|
+
else
|
129
|
+
if chart_type == "line" || chart_type == "line2"
|
130
|
+
series = []
|
131
|
+
|
132
|
+
if chart_type == "line"
|
133
|
+
columns[1..-1].each_with_index.each do |k, i|
|
134
|
+
series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
|
135
|
+
end
|
136
|
+
else
|
137
|
+
rows.group_by { |r| v = r[1]; (smart_values[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
|
138
|
+
series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
current_series = nil
|
143
|
+
begin
|
144
|
+
anomalies = []
|
145
|
+
series.each do |s|
|
146
|
+
current_series = s[:name]
|
147
|
+
anomalies << s[:name] if anomaly?(s[:data])
|
148
|
+
end
|
149
|
+
anomaly = anomalies.any?
|
150
|
+
if anomaly
|
151
|
+
if anomalies.size == 1
|
152
|
+
message = "Anomaly detected in #{anomalies.first}"
|
153
|
+
else
|
154
|
+
message = "Anomalies detected in #{anomalies.to_sentence}"
|
155
|
+
end
|
156
|
+
else
|
157
|
+
message = "No anomalies detected"
|
158
|
+
end
|
159
|
+
rescue => e
|
160
|
+
message = "#{current_series}: #{e.message}"
|
161
|
+
raise e if Rails.env.development?
|
162
|
+
end
|
163
|
+
else
|
164
|
+
message = "Bad format"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
[anomaly, message]
|
169
|
+
end
|
170
|
+
|
171
|
+
def anomaly?(series)
|
172
|
+
series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
|
173
|
+
|
174
|
+
anomaly_detector = Blazer.anomaly_detectors.fetch(Blazer.anomaly_checks)
|
175
|
+
anomaly_detector.call(series)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Blazer
|
2
|
+
class ResultCache
|
3
|
+
def initialize(data_source)
|
4
|
+
@data_source = data_source
|
5
|
+
end
|
6
|
+
|
7
|
+
def write_run(run_id, result)
|
8
|
+
write(run_cache_key(run_id), result, expires_in: 30.seconds)
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_run(run_id)
|
12
|
+
read(run_cache_key(run_id))
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete_run(run_id)
|
16
|
+
delete(run_cache_key(run_id))
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_statement(statement, result, expires_in:)
|
20
|
+
write(statement_cache_key(statement), result, expires_in: expires_in) if caching?
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_statement(statement)
|
24
|
+
read(statement_cache_key(statement)) if caching?
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_statement(statement)
|
28
|
+
delete(statement_cache_key(statement)) if caching?
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def write(key, result, expires_in:)
|
34
|
+
raise ArgumentError, "expected Blazer::Result" unless result.is_a?(Blazer::Result)
|
35
|
+
value = [result.columns, result.rows, result.error, result.cached_at, result.just_cached]
|
36
|
+
cache.write(key, value, expires_in: expires_in)
|
37
|
+
end
|
38
|
+
|
39
|
+
def read(key)
|
40
|
+
value = cache.read(key)
|
41
|
+
if value
|
42
|
+
columns, rows, error, cached_at, just_cached = value
|
43
|
+
Blazer::Result.new(@data_source, columns, rows, error, cached_at, just_cached)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key)
|
48
|
+
cache.delete(key)
|
49
|
+
end
|
50
|
+
|
51
|
+
def caching?
|
52
|
+
@data_source.cache_mode != "off"
|
53
|
+
end
|
54
|
+
|
55
|
+
def cache_key(key)
|
56
|
+
(["blazer", "v5", @data_source.id] + key).join("/")
|
57
|
+
end
|
58
|
+
|
59
|
+
def statement_cache_key(statement)
|
60
|
+
cache_key(["statement", Digest::SHA256.hexdigest(statement.bind_statement.to_s.gsub("\r\n", "\n") + statement.bind_values.to_json)])
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_cache_key(run_id)
|
64
|
+
cache_key(["run", run_id])
|
65
|
+
end
|
66
|
+
|
67
|
+
def cache
|
68
|
+
Blazer.cache
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Blazer
|
2
|
+
class RunStatement
|
3
|
+
def perform(statement, options = {})
|
4
|
+
query = options[:query]
|
5
|
+
|
6
|
+
data_source = statement.data_source
|
7
|
+
statement.bind
|
8
|
+
|
9
|
+
# audit
|
10
|
+
if Blazer.audit
|
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)
|
14
|
+
audit.query = query
|
15
|
+
audit.data_source = data_source.id
|
16
|
+
# only set user if present to avoid error with Rails 7.1 when no user model
|
17
|
+
audit.user = options[:user] unless options[:user].nil?
|
18
|
+
audit.save!
|
19
|
+
end
|
20
|
+
|
21
|
+
start_time = Blazer.monotonic_time
|
22
|
+
result = data_source.run_statement(statement, options)
|
23
|
+
duration = Blazer.monotonic_time - start_time
|
24
|
+
|
25
|
+
if Blazer.audit
|
26
|
+
audit.duration = duration if audit.respond_to?(:duration=)
|
27
|
+
audit.error = result.error if audit.respond_to?(:error=)
|
28
|
+
audit.timed_out = result.timed_out? if audit.respond_to?(:timed_out=)
|
29
|
+
audit.cached = result.cached? if audit.respond_to?(:cached=)
|
30
|
+
if !result.cached? && duration >= 10
|
31
|
+
audit.cost = data_source.cost(statement) if audit.respond_to?(:cost=)
|
32
|
+
end
|
33
|
+
audit.save! if audit.changed?
|
34
|
+
end
|
35
|
+
|
36
|
+
if query && !result.timed_out? && !result.cached? && !query.variables.any?
|
37
|
+
query.checks.each do |check|
|
38
|
+
check.update_state(result)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Blazer
|
2
|
+
class RunStatementJob < ActiveJob::Base
|
3
|
+
self.queue_adapter = :async
|
4
|
+
|
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
|
+
begin
|
10
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
11
|
+
Blazer::RunStatement.new.perform(statement, options)
|
12
|
+
end
|
13
|
+
rescue Exception => e
|
14
|
+
result = Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
|
15
|
+
data_source.result_cache.write_run(options[:run_id], result)
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
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 &
|
52
|
+
# - Replace the less-than sign, < with <
|
53
|
+
# - Replace the greater-than sign, > with >
|
54
|
+
# That's it. Don't HTML entity-encode the entire message.
|
55
|
+
def self.escape(str)
|
56
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">") 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,77 @@
|
|
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
|
+
# strip commented out lines
|
14
|
+
# and regex {1} or {1,2}
|
15
|
+
@variables ||= 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
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_values(var_params)
|
19
|
+
variables.each do |var|
|
20
|
+
value = var_params[var].presence
|
21
|
+
value = nil unless value.is_a?(String) # ignore arrays and hashes
|
22
|
+
if value
|
23
|
+
if ["start_time", "end_time"].include?(var)
|
24
|
+
value = value.to_s.gsub(" ", "+") # fix for Quip bug
|
25
|
+
end
|
26
|
+
|
27
|
+
if var.end_with?("_at")
|
28
|
+
begin
|
29
|
+
value = Blazer.time_zone.parse(value)
|
30
|
+
rescue
|
31
|
+
# do nothing
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
unless value.is_a?(ActiveSupport::TimeWithZone)
|
36
|
+
if value =~ /\A\d+\z/
|
37
|
+
value = value.to_i
|
38
|
+
elsif value =~ /\A\d+\.\d+\z/
|
39
|
+
value = value.to_f
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable
|
44
|
+
@values[var] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cohort_analysis?
|
49
|
+
/\/\*\s*cohort analysis\s*\*\//i.match?(statement)
|
50
|
+
end
|
51
|
+
|
52
|
+
def apply_cohort_analysis(period:, days:)
|
53
|
+
@statement = data_source.cohort_analysis_statement(statement, period: period, days: days).sub("{placeholder}") { statement }
|
54
|
+
end
|
55
|
+
|
56
|
+
# should probably transform before cohort analysis
|
57
|
+
# but keep previous order for now
|
58
|
+
def transformed_statement
|
59
|
+
statement = self.statement.dup
|
60
|
+
Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
|
61
|
+
statement
|
62
|
+
end
|
63
|
+
|
64
|
+
def bind
|
65
|
+
@bind_statement, @bind_values = data_source.bind_params(transformed_statement, values)
|
66
|
+
end
|
67
|
+
|
68
|
+
def display_statement
|
69
|
+
data_source.sub_variables(transformed_statement, values)
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear_cache
|
73
|
+
bind if bind_statement.nil?
|
74
|
+
data_source.clear_cache(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|