blazer 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +56 -0
- data/app/assets/javascripts/blazer/application.js +11 -1
- data/app/assets/javascripts/blazer/selectize.js +1 -1
- data/app/assets/stylesheets/blazer/application.css +28 -0
- data/app/controllers/blazer/base_controller.rb +1 -1
- data/app/controllers/blazer/checks_controller.rb +3 -2
- data/app/controllers/blazer/dashboards_controller.rb +3 -3
- data/app/controllers/blazer/queries_controller.rb +86 -29
- data/app/helpers/blazer/base_helper.rb +0 -17
- data/app/mailers/blazer/check_mailer.rb +1 -1
- data/app/models/blazer/check.rb +36 -11
- data/app/models/blazer/dashboard.rb +1 -0
- data/app/views/blazer/_nav.html.erb +2 -1
- data/app/views/blazer/checks/_form.html.erb +18 -3
- data/app/views/blazer/checks/index.html.erb +2 -3
- data/app/views/blazer/dashboards/_form.html.erb +3 -2
- data/app/views/blazer/dashboards/show.html.erb +1 -1
- data/app/views/blazer/queries/_form.html.erb +7 -4
- data/app/views/blazer/queries/run.html.erb +26 -6
- data/app/views/blazer/queries/show.html.erb +1 -1
- data/lib/blazer.rb +16 -9
- data/lib/blazer/data_source.rb +62 -33
- data/lib/blazer/detect_anomalies.R +14 -0
- data/lib/blazer/engine.rb +6 -0
- data/lib/blazer/result.rb +147 -0
- data/lib/blazer/run_statement_job.rb +16 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/install.rb +2 -1
- metadata +6 -5
- data/app/views/blazer/checks/run.html.erb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63faea5b7fa9ad8d4365931c80851e33d80553e8
|
4
|
+
data.tar.gz: 30b6a8d2dd631512a5cd82a6fabe119e7e9d5963
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69a08a57180385ed46ae3bd776fa3e67bf885ed9845dc7508f70d1d7d883d940120c4b03167800726fe6d0dcb728518e4cfbdc32b6fc2647813d381eea0d1670
|
7
|
+
data.tar.gz: 681fdbe806cfc7675335802166199ed11124e6c443c4d8da31e279c47b48ed847a5d2061d24d657d5c343c47a284b929dc93f4188ca7fa7fcae4d47bcd225d67
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -254,12 +254,20 @@ SELECT date_trunc('week', created_at), gender, COUNT(*) FROM users GROUP BY 1, 2
|
|
254
254
|
|
255
255
|
### Column Chart
|
256
256
|
|
257
|
+
There are also two ways to generate column charts.
|
258
|
+
|
257
259
|
2+ columns - string, numeric(s) - [Example](https://blazerme.herokuapp.com/queries/2-top-genres)
|
258
260
|
|
259
261
|
```sql
|
260
262
|
SELECT gender, COUNT(*) FROM users GROUP BY 1
|
261
263
|
```
|
262
264
|
|
265
|
+
3 columns - string, string, numeric
|
266
|
+
|
267
|
+
```sql
|
268
|
+
SELECT gender, zip_code, COUNT(*) FROM users GROUP BY 1, 2
|
269
|
+
```
|
270
|
+
|
263
271
|
### Maps
|
264
272
|
|
265
273
|
Columns named `latitude` and `longitude` or `lat` and `lon` - [Example](https://blazerme.herokuapp.com/queries/11-airports-in-pacific-time-zone)
|
@@ -270,6 +278,14 @@ SELECT name, latitude, longitude FROM cities
|
|
270
278
|
|
271
279
|
To enable, get an access token from [Mapbox](https://www.mapbox.com/) and set `ENV["MAPBOX_ACCESS_TOKEN"]`.
|
272
280
|
|
281
|
+
### Targets
|
282
|
+
|
283
|
+
Use the column name `target` to draw a line for goals.
|
284
|
+
|
285
|
+
```sql
|
286
|
+
SELECT date_trunc('week', created_at), COUNT(*) AS new_users, 100000 AS target FROM users GROUP BY 1
|
287
|
+
```
|
288
|
+
|
273
289
|
## Dashboards
|
274
290
|
|
275
291
|
Create a dashboard with multiple queries. [Example](https://blazerme.herokuapp.com/dashboards/1-movielens)
|
@@ -290,6 +306,25 @@ SELECT * FROM ratings WHERE user_id IS NULL /* all ratings should have a user */
|
|
290
306
|
|
291
307
|
Then create check with optional emails if you want to be notified. Emails are sent when a check starts failing, and when it starts passing again.
|
292
308
|
|
309
|
+
## Anomaly Detection
|
310
|
+
|
311
|
+
Anomaly detection is supported thanks to Twitter’s [AnomalyDetection](https://github.com/twitter/AnomalyDetection) library.
|
312
|
+
|
313
|
+
First, [install R](https://cloud.r-project.org/). Then, run:
|
314
|
+
|
315
|
+
```R
|
316
|
+
install.packages("devtools")
|
317
|
+
devtools::install_github("twitter/AnomalyDetection")
|
318
|
+
```
|
319
|
+
|
320
|
+
And add to `config/blazer.yml`:
|
321
|
+
|
322
|
+
```yml
|
323
|
+
anomaly_checks: true
|
324
|
+
```
|
325
|
+
|
326
|
+
If upgrading from version 1.4 or below, also follow the [upgrade instructions](#15).
|
327
|
+
|
293
328
|
## Data Sources
|
294
329
|
|
295
330
|
Blazer supports multiple data sources :tada:
|
@@ -330,6 +365,27 @@ For an easy way to group by day, week, month, and more with correct time zones,
|
|
330
365
|
|
331
366
|
## Upgrading
|
332
367
|
|
368
|
+
### 1.5
|
369
|
+
|
370
|
+
To take advantage of the anomaly detection, create a migration
|
371
|
+
|
372
|
+
```sh
|
373
|
+
rails g migration upgrade_blazer_to_1_5
|
374
|
+
```
|
375
|
+
|
376
|
+
with:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
add_column(:blazer_checks, :check_type, :string)
|
380
|
+
add_column(:blazer_checks, :message, :text)
|
381
|
+
commit_db_transaction
|
382
|
+
|
383
|
+
Blazer::Check.reset_column_information
|
384
|
+
|
385
|
+
Blazer::Check.where(invert: true).update_all(check_type: "missing_data")
|
386
|
+
Blazer::Check.where(check_type: nil).update_all(check_type: "bad_data")
|
387
|
+
```
|
388
|
+
|
333
389
|
### 1.3
|
334
390
|
|
335
391
|
To take advantage of the latest features, create a migration
|
@@ -31,7 +31,17 @@ function runQuery(data, success, error) {
|
|
31
31
|
method: "POST",
|
32
32
|
data: data,
|
33
33
|
dataType: "html"
|
34
|
-
}).done(
|
34
|
+
}).done( function (d) {
|
35
|
+
if (d[0] == "{") {
|
36
|
+
var response = $.parseJSON(d);
|
37
|
+
data.blazer = response;
|
38
|
+
setTimeout( function () {
|
39
|
+
runQuery(data, success, error);
|
40
|
+
}, 1000);
|
41
|
+
} else {
|
42
|
+
success(d);
|
43
|
+
}
|
44
|
+
}).fail( function(jqXHR, textStatus, errorThrown) {
|
35
45
|
var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message;
|
36
46
|
error(message);
|
37
47
|
});
|
@@ -2236,7 +2236,7 @@
|
|
2236
2236
|
*/
|
2237
2237
|
registerOption: function(data) {
|
2238
2238
|
var key = hash_key(data[this.settings.valueField]);
|
2239
|
-
if (
|
2239
|
+
if (!key || this.options.hasOwnProperty(key)) return false;
|
2240
2240
|
data.$order = data.$order || ++this.order;
|
2241
2241
|
this.options[key] = data;
|
2242
2242
|
return key;
|
@@ -100,3 +100,31 @@ input.search:focus {
|
|
100
100
|
.vars {
|
101
101
|
color: #ccc;
|
102
102
|
}
|
103
|
+
|
104
|
+
.check-state {
|
105
|
+
font-weight: bold;
|
106
|
+
}
|
107
|
+
|
108
|
+
.check-state a {
|
109
|
+
color: inherit;
|
110
|
+
}
|
111
|
+
|
112
|
+
.check-state.failing {
|
113
|
+
color: red;
|
114
|
+
}
|
115
|
+
|
116
|
+
.check-state.passing {
|
117
|
+
color: #5cb85c;
|
118
|
+
}
|
119
|
+
|
120
|
+
.check-state.error {
|
121
|
+
color: #666;
|
122
|
+
}
|
123
|
+
|
124
|
+
.check-state.timed_out {
|
125
|
+
color: orange;
|
126
|
+
}
|
127
|
+
|
128
|
+
.check-state.disabled {
|
129
|
+
color: #000;
|
130
|
+
}
|
@@ -51,7 +51,7 @@ module Blazer
|
|
51
51
|
helper_method :extract_vars
|
52
52
|
|
53
53
|
def variable_params
|
54
|
-
params.except(:controller, :action, :id, :host, :query, :dashboard, :query_id, :query_ids, :table_names, :authenticity_token, :utf8, :_method, :commit, :statement, :data_source, :name, :fork_query_id).permit!
|
54
|
+
params.except(:controller, :action, :id, :host, :query, :dashboard, :query_id, :query_ids, :table_names, :authenticity_token, :utf8, :_method, :commit, :statement, :data_source, :name, :fork_query_id, :blazer).permit!
|
55
55
|
end
|
56
56
|
helper_method :variable_params
|
57
57
|
|
@@ -9,7 +9,7 @@ module Blazer
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def new
|
12
|
-
@check = Blazer::Check.new
|
12
|
+
@check = Blazer::Check.new(query_id: params[:query_id])
|
13
13
|
end
|
14
14
|
|
15
15
|
def create
|
@@ -40,12 +40,13 @@ module Blazer
|
|
40
40
|
|
41
41
|
def run
|
42
42
|
@query = @check.query
|
43
|
+
redirect_to query_path(@query)
|
43
44
|
end
|
44
45
|
|
45
46
|
private
|
46
47
|
|
47
48
|
def check_params
|
48
|
-
params.require(:check).permit(:query_id, :emails, :invert, :schedule)
|
49
|
+
params.require(:check).permit(:query_id, :emails, :invert, :check_type, :schedule)
|
49
50
|
end
|
50
51
|
|
51
52
|
def set_check
|
@@ -37,9 +37,9 @@ module Blazer
|
|
37
37
|
@data_sources.each do |data_source|
|
38
38
|
query = data_source.smart_variables[var]
|
39
39
|
if query
|
40
|
-
|
41
|
-
((@smart_vars[var] ||= []).concat(rows.map { |v| v.reverse })).uniq!
|
42
|
-
@sql_errors << error if error
|
40
|
+
result = data_source.run_statement(query)
|
41
|
+
((@smart_vars[var] ||= []).concat(result.rows.map { |v| v.reverse })).uniq!
|
42
|
+
@sql_errors << result.error if result.error
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -4,8 +4,11 @@ module Blazer
|
|
4
4
|
|
5
5
|
def home
|
6
6
|
set_queries(1000)
|
7
|
+
|
8
|
+
@dashboards = Blazer::Dashboard.order(:name)
|
9
|
+
@dashboards = @dashboards.includes(:creator) if Blazer.user_class
|
7
10
|
@dashboards =
|
8
|
-
|
11
|
+
@dashboards.map do |d|
|
9
12
|
{
|
10
13
|
name: "<strong>#{view_context.link_to(d.name, d)}</strong>",
|
11
14
|
creator: blazer_user && d.try(:creator) == blazer_user ? "You" : d.try(:creator).try(Blazer.user_name),
|
@@ -51,9 +54,9 @@ module Blazer
|
|
51
54
|
@bind_vars.each do |var|
|
52
55
|
query = data_source.smart_variables[var]
|
53
56
|
if query
|
54
|
-
|
55
|
-
@smart_vars[var] = rows.map { |v| v.reverse }
|
56
|
-
@sql_errors << error if error
|
57
|
+
result = data_source.run_statement(query)
|
58
|
+
@smart_vars[var] = result.rows.map { |v| v.reverse }
|
59
|
+
@sql_errors << result.error if result.error
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
@@ -68,25 +71,68 @@ module Blazer
|
|
68
71
|
data_source = params[:data_source]
|
69
72
|
process_vars(@statement, data_source)
|
70
73
|
@only_chart = params[:only_chart]
|
74
|
+
@run_id = blazer_params[:run_id]
|
75
|
+
@query = Query.find_by(id: params[:query_id]) if params[:query_id]
|
76
|
+
data_source = @query.data_source if @query && @query.data_source
|
77
|
+
@data_source = Blazer.data_sources[data_source]
|
78
|
+
|
79
|
+
if @run_id
|
80
|
+
@timestamp = blazer_params[:timestamp].to_i
|
81
|
+
|
82
|
+
@result = @data_source.run_results(@run_id)
|
83
|
+
@success = !@result.nil?
|
84
|
+
|
85
|
+
if @success
|
86
|
+
@data_source.delete_results(@run_id)
|
87
|
+
@columns = @result.columns
|
88
|
+
@rows = @result.rows
|
89
|
+
@error = @result.error
|
90
|
+
@just_cached = !@result.error && @result.cached_at.present?
|
91
|
+
@cached_at = nil
|
92
|
+
params[:data_source] = nil
|
93
|
+
render_run
|
94
|
+
elsif Time.now > Time.at(@timestamp + (@data_source.timeout || 120).to_i)
|
95
|
+
# timed out
|
96
|
+
@error = Blazer::TIMEOUT_MESSAGE
|
97
|
+
@rows = []
|
98
|
+
@columns = []
|
99
|
+
render_run
|
100
|
+
else
|
101
|
+
continue_run
|
102
|
+
end
|
103
|
+
elsif @success
|
104
|
+
@run_id = Blazer.async ? SecureRandom.uuid : nil
|
105
|
+
|
106
|
+
options = {user: blazer_user, query: @query, refresh_cache: params[:check], run_id: @run_id}
|
107
|
+
if Blazer.async && request.format.symbol != :csv
|
108
|
+
result = []
|
109
|
+
Blazer::RunStatementJob.perform_async(result, @data_source, @statement, options)
|
110
|
+
wait_start = Time.now
|
111
|
+
loop do
|
112
|
+
sleep(0.02)
|
113
|
+
break if result.any? || Time.now - wait_start > 3
|
114
|
+
end
|
115
|
+
@result = result.first
|
116
|
+
else
|
117
|
+
@result = @data_source.run_main_statement(@statement, options)
|
118
|
+
end
|
71
119
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
data_source = @query.data_source if @query && @query.data_source
|
76
|
-
@data_source = Blazer.data_sources[data_source]
|
77
|
-
|
78
|
-
@columns, @rows, @error, @cached_at, @just_cached = @data_source.run_main_statement(@statement, user: blazer_user, query: @query, refresh_cache: params[:check])
|
120
|
+
if @result
|
121
|
+
@data_source.delete_results(@run_id) if @run_id
|
79
122
|
|
80
|
-
|
81
|
-
|
123
|
+
@columns = @result.columns
|
124
|
+
@rows = @result.rows
|
125
|
+
@error = @result.error
|
126
|
+
@cached_at = @result.cached_at
|
127
|
+
@just_cached = @result.just_cached
|
82
128
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
format.csv do
|
88
|
-
send_data csv_data(@columns, @rows, @data_source), type: "text/csv; charset=utf-8; header=present", disposition: "attachment; filename=\"#{@query.try(:name).try(:parameterize).presence || 'query'}.csv\""
|
129
|
+
render_run
|
130
|
+
else
|
131
|
+
@timestamp = Time.now.to_i
|
132
|
+
continue_run
|
89
133
|
end
|
134
|
+
else
|
135
|
+
render layout: false
|
90
136
|
end
|
91
137
|
end
|
92
138
|
|
@@ -126,7 +172,13 @@ module Blazer
|
|
126
172
|
|
127
173
|
private
|
128
174
|
|
175
|
+
def continue_run
|
176
|
+
render json: {run_id: @run_id, timestamp: @timestamp}, status: :accepted
|
177
|
+
end
|
178
|
+
|
129
179
|
def render_run
|
180
|
+
@checks = @query ? @query.checks : []
|
181
|
+
|
130
182
|
@first_row = @rows.first || []
|
131
183
|
@column_types = []
|
132
184
|
if @rows.any?
|
@@ -145,17 +197,9 @@ module Blazer
|
|
145
197
|
end
|
146
198
|
|
147
199
|
@filename = @query.name.parameterize if @query
|
148
|
-
@min_width_types = @columns.each_with_index.select { |c, i| @first_row[i].is_a?(Time) || @first_row[i].is_a?(String) || @data_source.smart_columns[c] }
|
200
|
+
@min_width_types = @columns.each_with_index.select { |c, i| @first_row[i].is_a?(Time) || @first_row[i].is_a?(String) || @data_source.smart_columns[c] }.map(&:last)
|
149
201
|
|
150
|
-
@boom =
|
151
|
-
@columns.each_with_index do |key, i|
|
152
|
-
query = @data_source.smart_columns[key]
|
153
|
-
if query
|
154
|
-
values = @rows.map { |r| r[i] }.compact.uniq
|
155
|
-
columns, rows, error, cached_at = @data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
|
156
|
-
@boom[key] = Hash[rows.map { |k, v| [k.to_s, v] }]
|
157
|
-
end
|
158
|
-
end
|
202
|
+
@boom = @result.boom
|
159
203
|
|
160
204
|
@linked_columns = @data_source.linked_columns
|
161
205
|
|
@@ -176,6 +220,15 @@ module Blazer
|
|
176
220
|
end
|
177
221
|
end
|
178
222
|
end
|
223
|
+
|
224
|
+
respond_to do |format|
|
225
|
+
format.html do
|
226
|
+
render layout: false
|
227
|
+
end
|
228
|
+
format.csv do
|
229
|
+
send_data csv_data(@columns, @rows, @data_source), type: "text/csv; charset=utf-8; header=present", disposition: "attachment; filename=\"#{@query.try(:name).try(:parameterize).presence || 'query'}.csv\""
|
230
|
+
end
|
231
|
+
end
|
179
232
|
end
|
180
233
|
|
181
234
|
def set_queries(limit = nil)
|
@@ -215,6 +268,10 @@ module Blazer
|
|
215
268
|
params.require(:query).permit(:name, :description, :statement, :data_source)
|
216
269
|
end
|
217
270
|
|
271
|
+
def blazer_params
|
272
|
+
params[:blazer] || {}
|
273
|
+
end
|
274
|
+
|
218
275
|
def csv_data(columns, rows, data_source)
|
219
276
|
CSV.generate do |csv|
|
220
277
|
csv << columns
|
@@ -16,23 +16,6 @@ module Blazer
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
def blazer_column_types(columns, rows, boom)
|
20
|
-
columns.each_with_index.map do |k, i|
|
21
|
-
v = (rows.find { |r| r[i] } || {})[i]
|
22
|
-
if boom[k]
|
23
|
-
"string"
|
24
|
-
elsif v.is_a?(Numeric)
|
25
|
-
"numeric"
|
26
|
-
elsif v.is_a?(Time) || v.is_a?(Date)
|
27
|
-
"time"
|
28
|
-
elsif v.nil?
|
29
|
-
nil
|
30
|
-
else
|
31
|
-
"string"
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
19
|
def blazer_maps?
|
37
20
|
ENV["MAPBOX_ACCESS_TOKEN"].present?
|
38
21
|
end
|
@@ -10,7 +10,7 @@ module Blazer
|
|
10
10
|
@state_was = state_was
|
11
11
|
@rows_count = rows_count
|
12
12
|
@error = error
|
13
|
-
mail to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
|
13
|
+
mail to: check.emails, reply_to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
|
14
14
|
end
|
15
15
|
|
16
16
|
def failing_checks(email, checks)
|
data/app/models/blazer/check.rb
CHANGED
@@ -1,32 +1,57 @@
|
|
1
1
|
module Blazer
|
2
2
|
class Check < ActiveRecord::Base
|
3
|
+
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
|
3
4
|
belongs_to :query
|
4
5
|
|
5
6
|
validates :query_id, presence: true
|
6
7
|
|
8
|
+
before_validation :set_state
|
9
|
+
|
10
|
+
def set_state
|
11
|
+
self.state ||= "new"
|
12
|
+
end
|
13
|
+
|
7
14
|
def split_emails
|
8
15
|
emails.to_s.downcase.split(",").map(&:strip)
|
9
16
|
end
|
10
17
|
|
11
|
-
def update_state(
|
12
|
-
|
18
|
+
def update_state(result)
|
19
|
+
check_type =
|
20
|
+
if respond_to?(:check_type)
|
21
|
+
self.check_type
|
22
|
+
elsif respond_to?(:invert)
|
23
|
+
invert ? "missing_data" : "bad_data"
|
24
|
+
else
|
25
|
+
"bad_data"
|
26
|
+
end
|
27
|
+
|
28
|
+
message = result.error
|
29
|
+
|
13
30
|
self.state =
|
14
|
-
if
|
15
|
-
|
16
|
-
|
17
|
-
|
31
|
+
if result.timed_out?
|
32
|
+
"timed out"
|
33
|
+
elsif result.error
|
34
|
+
"error"
|
35
|
+
elsif check_type == "anomaly"
|
36
|
+
anomaly, message = result.detect_anomaly
|
37
|
+
if anomaly.nil?
|
18
38
|
"error"
|
39
|
+
elsif anomaly
|
40
|
+
"failing"
|
41
|
+
else
|
42
|
+
"passing"
|
19
43
|
end
|
20
|
-
elsif rows.any?
|
21
|
-
|
44
|
+
elsif result.rows.any?
|
45
|
+
check_type == "missing_data" ? "passing" : "failing"
|
22
46
|
else
|
23
|
-
|
47
|
+
check_type == "missing_data" ? "failing" : "passing"
|
24
48
|
end
|
25
49
|
|
26
50
|
self.last_run_at = Time.now if respond_to?(:last_run_at=)
|
51
|
+
self.message = message if respond_to?(:message=)
|
27
52
|
|
28
53
|
if respond_to?(:timeouts=)
|
29
|
-
if
|
54
|
+
if result.timed_out?
|
30
55
|
self.timeouts += 1
|
31
56
|
self.state = "disabled" if timeouts >= 3
|
32
57
|
else
|
@@ -36,7 +61,7 @@ module Blazer
|
|
36
61
|
|
37
62
|
# do not notify on creation, except when not passing
|
38
63
|
if (state_was || state != "passing") && state != state_was && emails.present?
|
39
|
-
Blazer::CheckMailer.state_change(self, state, state_was, rows.size,
|
64
|
+
Blazer::CheckMailer.state_change(self, state, state_was, result.rows.size, message).deliver_later
|
40
65
|
end
|
41
66
|
save! if changed?
|
42
67
|
end
|
@@ -10,6 +10,7 @@
|
|
10
10
|
<li role="separator" class="divider"></li>
|
11
11
|
<li><%= link_to "New Query", new_query_path %></li>
|
12
12
|
<li><%= link_to "New Dashboard", new_dashboard_path %></li>
|
13
|
-
|
13
|
+
<% check_params = @query ? {query_id: @query.id} : {} %>
|
14
|
+
<li><%= link_to "New Check", new_check_path(check_params) %></li>
|
14
15
|
</ul>
|
15
16
|
</div>
|
@@ -10,14 +10,29 @@
|
|
10
10
|
<div class="form-group">
|
11
11
|
<%= f.label :query_id, "Query" %>
|
12
12
|
<div class="hide">
|
13
|
-
<%= f.select :query_id,
|
13
|
+
<%= f.select :query_id, [], {include_blank: true} %>
|
14
14
|
</div>
|
15
15
|
<script>
|
16
|
-
|
16
|
+
var queries = <%= blazer_json_escape(Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }.to_json).html_safe %>;
|
17
|
+
var items = <%= blazer_json_escape([@check.query_id].compact.to_json).html_safe %>;
|
18
|
+
|
19
|
+
$("#check_query_id").selectize({options: queries, items: items, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
|
17
20
|
</script>
|
18
21
|
</div>
|
19
22
|
|
20
|
-
<% if @check.respond_to?(:
|
23
|
+
<% if @check.respond_to?(:check_type) %>
|
24
|
+
<div class="form-group">
|
25
|
+
<%= f.label :check_type, "Alert if" %>
|
26
|
+
<div class="hide">
|
27
|
+
<% check_options = [["Any results (bad data)", "bad_data"], ["No results (missing data)", "missing_data"]] %>
|
28
|
+
<% check_options << ["Anomaly (most recent data point)", "anomaly"] if Blazer.anomaly_checks %>
|
29
|
+
<%= f.select :check_type, check_options %>
|
30
|
+
</div>
|
31
|
+
<script>
|
32
|
+
$("#check_check_type").selectize({}).parent().removeClass("hide");
|
33
|
+
</script>
|
34
|
+
</div>
|
35
|
+
<% elsif @check.respond_to?(:invert) %>
|
21
36
|
<div class="form-group">
|
22
37
|
<%= f.label :invert, "Fails if" %>
|
23
38
|
<div class="hide">
|
@@ -3,7 +3,6 @@
|
|
3
3
|
<p style="float: right;"><%= link_to "New Check", new_check_path, class: "btn btn-info" %></p>
|
4
4
|
<%= render partial: "blazer/nav" %>
|
5
5
|
|
6
|
-
<% colors = {failing: "red", passing: "#5cb85c", error: "#666", timed_out: "orange", disabled: "#000"} %>
|
7
6
|
<table class="table">
|
8
7
|
<thead>
|
9
8
|
<tr>
|
@@ -20,7 +19,7 @@
|
|
20
19
|
<td><%= link_to check.query.name, check.query %></td>
|
21
20
|
<td>
|
22
21
|
<% if check.state %>
|
23
|
-
<small
|
22
|
+
<small class="check-state <%= check.state.parameterize("_") %>"><%= check.state.upcase %></small>
|
24
23
|
<% end %>
|
25
24
|
</td>
|
26
25
|
<td><%= check.schedule if check.respond_to?(:schedule) %></td>
|
@@ -33,7 +32,7 @@
|
|
33
32
|
</td>
|
34
33
|
<td style="text-align: right; padding: 1px;">
|
35
34
|
<%= link_to "Edit", edit_check_path(check), class: "btn btn-info" %>
|
36
|
-
<%= link_to "Run Now", run_check_path(check), class: "btn btn-primary" %>
|
35
|
+
<%= link_to "Run Now", run_check_path(check), target: "_blank", class: "btn btn-primary" %>
|
37
36
|
</td>
|
38
37
|
</tr>
|
39
38
|
<% end %>
|
@@ -38,10 +38,11 @@ li:hover .glyphicon-remove {
|
|
38
38
|
<div class="form-group">
|
39
39
|
<%= f.label :query_id, "Add Chart" %>
|
40
40
|
<div class="hide">
|
41
|
-
<%= select_tag :query_id,
|
41
|
+
<%= select_tag :query_id, nil, {include_blank: true, placeholder: "Select chart"} %>
|
42
42
|
</div>
|
43
43
|
<script>
|
44
|
-
|
44
|
+
var queries = <%= blazer_json_escape(Blazer::Query.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }.to_json).html_safe %>;
|
45
|
+
$("#query_id").selectize({options: queries, highlight: false, maxOptions: 100}).parents(".hide").removeClass("hide");
|
45
46
|
$("#query_id").change( function () {
|
46
47
|
var $option = $(this).find("option:selected");
|
47
48
|
if ($option.val() !== "") {
|
@@ -40,7 +40,7 @@
|
|
40
40
|
<% end %>
|
41
41
|
|
42
42
|
<% if @bind_vars.any? %>
|
43
|
-
<form id="bind" method="get" action="<%=
|
43
|
+
<form id="bind" method="get" action="<%= dashboard_path(@dashboard, variable_params) %>" class="form-inline" style="margin-bottom: 10px;">
|
44
44
|
<% date_vars = ["start_time", "end_time"] %>
|
45
45
|
<% if (date_vars - @bind_vars).empty? %>
|
46
46
|
<% @bind_vars = @bind_vars - date_vars %>
|
@@ -14,9 +14,6 @@
|
|
14
14
|
<div class="form-group text-right">
|
15
15
|
<div class="pull-left" style="margin-top: 6px;">
|
16
16
|
<%= link_to "Back", :back %>
|
17
|
-
<% if Blazer.data_sources.size == 1 %>
|
18
|
-
<span class="text-muted" style="margin-left: 20px;"> Use {start_time} and {end_time} for time ranges</span>
|
19
|
-
<% end %>
|
20
17
|
</div>
|
21
18
|
<%= f.select :data_source, Blazer.data_sources.values.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size == 1), style: "width: 140px;" %>
|
22
19
|
<div id="tables" style="display: inline-block; width: 260px; margin-right: 10px;" class="hide">
|
@@ -103,6 +100,12 @@
|
|
103
100
|
editor.resize();
|
104
101
|
};
|
105
102
|
|
103
|
+
function getSQL() {
|
104
|
+
var selectedText = editor.getSelectedText();
|
105
|
+
var text = selectedText.length == 0 ? editor.getValue() : selectedText;
|
106
|
+
return text.replace(/\n/g, "\r\n");
|
107
|
+
}
|
108
|
+
|
106
109
|
editor.getSession().on("change", adjustHeight);
|
107
110
|
adjustHeight();
|
108
111
|
$("#editor").show();
|
@@ -125,7 +128,7 @@
|
|
125
128
|
xhr.abort();
|
126
129
|
}
|
127
130
|
|
128
|
-
var data = $.extend({}, params, {statement:
|
131
|
+
var data = $.extend({}, params, {statement: getSQL(), data_source: $("#query_data_source").val()});
|
129
132
|
|
130
133
|
xhr = runQuery(data, function (data) {
|
131
134
|
$("#results").html(data);
|
@@ -24,12 +24,22 @@
|
|
24
24
|
<% end %>
|
25
25
|
</p>
|
26
26
|
<% end %>
|
27
|
-
<p class="text-muted"
|
27
|
+
<p class="text-muted">
|
28
|
+
<%= pluralize(@rows.size, "row") %>
|
29
|
+
|
30
|
+
<% @checks.select(&:state).each do |check| %>
|
31
|
+
· <small class="check-state <%= check.state.parameterize("_") %>"><%= link_to check.state.upcase, edit_check_path(check) %></small>
|
32
|
+
<% if check.try(:message) %>
|
33
|
+
· <%= check.message %>
|
34
|
+
<% end %>
|
35
|
+
<% end %>
|
36
|
+
</p>
|
28
37
|
<% end %>
|
29
38
|
<% if @rows.any? %>
|
30
39
|
<% values = @rows.first %>
|
31
40
|
<% chart_id = SecureRandom.hex %>
|
32
|
-
<% column_types =
|
41
|
+
<% column_types = @result.column_types %>
|
42
|
+
<% chart_type = @result.chart_type %>
|
33
43
|
<% chart_options = {id: chart_id, min: nil} %>
|
34
44
|
<% series_library = {} %>
|
35
45
|
<% target_index = @columns.index { |k| k.downcase == "target" } %>
|
@@ -65,12 +75,22 @@
|
|
65
75
|
featureLayer.setGeoJSON(geojson);
|
66
76
|
map.fitBounds(featureLayer.getBounds());
|
67
77
|
</script>
|
68
|
-
<% elsif
|
78
|
+
<% elsif chart_type == "line" %>
|
69
79
|
<%= line_chart @columns[1..-1].each_with_index.map{ |k, i| {name: k, data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} }, chart_options %>
|
70
|
-
<% elsif
|
80
|
+
<% elsif chart_type == "line2" %>
|
71
81
|
<%= line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: name, data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options %>
|
72
|
-
<% elsif
|
82
|
+
<% elsif chart_type == "bar" %>
|
73
83
|
<%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: name, data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, id: chart_id %>
|
84
|
+
<% elsif chart_type == "bar2" %>
|
85
|
+
<% first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1) %>
|
86
|
+
<% labels = first_20.map { |r| r[0] }.uniq %>
|
87
|
+
<% series = first_20.map { |r| r[1] }.uniq %>
|
88
|
+
<% labels.each do |l| %>
|
89
|
+
<% series.each do |s| %>
|
90
|
+
<% first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s } %>
|
91
|
+
<% end %>
|
92
|
+
<% end %>
|
93
|
+
<%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: name, data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, id: chart_id %>
|
74
94
|
<% elsif @only_chart %>
|
75
95
|
<% if @rows.size == 1 && @rows.first.size == 1 %>
|
76
96
|
<% v = @rows.first.first %>
|
@@ -96,7 +116,7 @@
|
|
96
116
|
<% @columns.each_with_index do |key, i| %>
|
97
117
|
<% type = @column_types[i] %>
|
98
118
|
<th style="width: <%= header_width %>%;" data-sort="<%= type %>">
|
99
|
-
<div style="min-width: <%= @min_width_types.include?(
|
119
|
+
<div style="min-width: <%= @min_width_types.include?(i) ? 180 : 60 %>px;">
|
100
120
|
<%= key %>
|
101
121
|
</div>
|
102
122
|
</th>
|
@@ -52,7 +52,7 @@
|
|
52
52
|
<% end %>
|
53
53
|
|
54
54
|
<% if @bind_vars.any? %>
|
55
|
-
<form id="bind" method="get" action="<%=
|
55
|
+
<form id="bind" method="get" action="<%= query_path(@query, variable_params) %>" class="form-inline" style="margin-bottom: 10px;">
|
56
56
|
<% date_vars = ["start_time", "end_time"] %>
|
57
57
|
<% if (date_vars - @bind_vars).empty? %>
|
58
58
|
<% @bind_vars = @bind_vars - date_vars %>
|
data/lib/blazer.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require "csv"
|
2
2
|
require "yaml"
|
3
3
|
require "chartkick"
|
4
|
+
require "safely/core"
|
4
5
|
require "blazer/version"
|
5
6
|
require "blazer/data_source"
|
7
|
+
require "blazer/result"
|
6
8
|
require "blazer/engine"
|
7
|
-
require "safely/core"
|
8
9
|
|
9
10
|
module Blazer
|
10
11
|
class Error < StandardError; end
|
@@ -21,14 +22,19 @@ module Blazer
|
|
21
22
|
attr_accessor :cache
|
22
23
|
attr_accessor :transform_statement
|
23
24
|
attr_accessor :check_schedules
|
25
|
+
attr_accessor :anomaly_checks
|
26
|
+
attr_accessor :async
|
24
27
|
end
|
25
28
|
self.audit = true
|
26
29
|
self.user_name = :name
|
27
30
|
self.check_schedules = ["5 minutes", "1 hour", "1 day"]
|
31
|
+
self.anomaly_checks = false
|
32
|
+
self.async = false
|
28
33
|
|
29
34
|
TIMEOUT_MESSAGE = "Query timed out :("
|
30
35
|
TIMEOUT_ERRORS = [
|
31
36
|
"canceling statement due to statement timeout", # postgres
|
37
|
+
"canceling statement due to conflict with recovery", # postgres
|
32
38
|
"cancelled on user's request", # redshift
|
33
39
|
"canceled on user's request", # redshift
|
34
40
|
"system requested abort", # redshift
|
@@ -68,6 +74,7 @@ module Blazer
|
|
68
74
|
checks = Blazer::Check.includes(:query)
|
69
75
|
checks = checks.where(schedule: schedule) if schedule
|
70
76
|
checks.find_each do |check|
|
77
|
+
next if check.state == "disabled"
|
71
78
|
Safely.safely { run_check(check) }
|
72
79
|
end
|
73
80
|
end
|
@@ -84,13 +91,13 @@ module Blazer
|
|
84
91
|
Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
|
85
92
|
|
86
93
|
while tries <= 3
|
87
|
-
|
88
|
-
if
|
94
|
+
result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
|
95
|
+
if result.timed_out?
|
89
96
|
Rails.logger.info "[blazer timeout] query=#{check.query.name}"
|
90
97
|
tries += 1
|
91
98
|
sleep(10)
|
92
|
-
elsif error.to_s.start_with?("PG::ConnectionBad")
|
93
|
-
|
99
|
+
elsif result.error.to_s.start_with?("PG::ConnectionBad")
|
100
|
+
data_source.reconnect
|
94
101
|
Rails.logger.info "[blazer reconnect] query=#{check.query.name}"
|
95
102
|
tries += 1
|
96
103
|
sleep(10)
|
@@ -98,15 +105,15 @@ module Blazer
|
|
98
105
|
break
|
99
106
|
end
|
100
107
|
end
|
101
|
-
check.update_state(
|
108
|
+
check.update_state(result)
|
102
109
|
# TODO use proper logfmt
|
103
|
-
Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{rows.try(:size)} error=#{error}"
|
110
|
+
Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
|
104
111
|
|
105
112
|
instrument[:statement] = statement
|
106
113
|
instrument[:data_source] = data_source
|
107
114
|
instrument[:state] = check.state
|
108
|
-
instrument[:rows] = rows.try(:size)
|
109
|
-
instrument[:error] = error
|
115
|
+
instrument[:rows] = result.rows.try(:size)
|
116
|
+
instrument[:error] = result.error
|
110
117
|
instrument[:tries] = tries
|
111
118
|
end
|
112
119
|
end
|
data/lib/blazer/data_source.rb
CHANGED
@@ -110,42 +110,52 @@ module Blazer
|
|
110
110
|
end
|
111
111
|
|
112
112
|
start_time = Time.now
|
113
|
-
|
113
|
+
result = run_statement(statement, options)
|
114
114
|
duration = Time.now - start_time
|
115
115
|
|
116
116
|
if Blazer.audit
|
117
117
|
audit.duration = duration if audit.respond_to?(:duration=)
|
118
|
-
audit.error = error if audit.respond_to?(:error=)
|
119
|
-
audit.timed_out =
|
120
|
-
audit.cached =
|
121
|
-
if !
|
118
|
+
audit.error = result.error if audit.respond_to?(:error=)
|
119
|
+
audit.timed_out = result.timed_out? if audit.respond_to?(:timed_out=)
|
120
|
+
audit.cached = result.cached? if audit.respond_to?(:cached=)
|
121
|
+
if !result.cached? && duration >= 10
|
122
122
|
audit.cost = cost(statement) if audit.respond_to?(:cost=)
|
123
123
|
end
|
124
124
|
audit.save! if audit.changed?
|
125
125
|
end
|
126
126
|
|
127
|
-
if query &&
|
127
|
+
if query && !result.timed_out?
|
128
128
|
query.checks.each do |check|
|
129
|
-
check.update_state(
|
129
|
+
check.update_state(result)
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
133
|
-
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def read_cache(cache_key)
|
137
|
+
value = Blazer.cache.read(cache_key)
|
138
|
+
if value
|
139
|
+
Blazer::Result.new(self, *Marshal.load(value), nil)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def run_results(run_id)
|
144
|
+
read_cache(run_cache_key(run_id))
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete_results(run_id)
|
148
|
+
Blazer.cache.delete(run_cache_key(run_id))
|
134
149
|
end
|
135
150
|
|
136
151
|
def run_statement(statement, options = {})
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
just_cached = false
|
142
|
-
cache_key = self.cache_key(statement) if cache
|
143
|
-
if cache && !options[:refresh_cache]
|
144
|
-
value = Blazer.cache.read(cache_key)
|
145
|
-
columns, rows, cached_at = Marshal.load(value) if value
|
152
|
+
run_id = options[:run_id]
|
153
|
+
result = nil
|
154
|
+
if cache_mode != "off" && !options[:refresh_cache]
|
155
|
+
result = read_cache(statement_cache_key(statement))
|
146
156
|
end
|
147
157
|
|
148
|
-
unless
|
158
|
+
unless result
|
149
159
|
comment = "blazer"
|
150
160
|
if options[:user].respond_to?(:id)
|
151
161
|
comment << ",user_id:#{options[:user].id}"
|
@@ -157,20 +167,29 @@ module Blazer
|
|
157
167
|
if options[:query].respond_to?(:id)
|
158
168
|
comment << ",query_id:#{options[:query].id}"
|
159
169
|
end
|
160
|
-
|
170
|
+
if options[:check]
|
171
|
+
comment << ",check_id:#{options[:check].id},check_emails:#{options[:check].emails}"
|
172
|
+
end
|
173
|
+
result = run_statement_helper(statement, comment, options[:run_id])
|
161
174
|
end
|
162
175
|
|
163
|
-
|
164
|
-
output << just_cached if options[:with_just_cached]
|
165
|
-
output
|
176
|
+
result
|
166
177
|
end
|
167
178
|
|
168
179
|
def clear_cache(statement)
|
169
|
-
Blazer.cache.delete(
|
180
|
+
Blazer.cache.delete(statement_cache_key(statement))
|
181
|
+
end
|
182
|
+
|
183
|
+
def cache_key(key)
|
184
|
+
(["blazer", "v4"] + key).join("/")
|
185
|
+
end
|
186
|
+
|
187
|
+
def statement_cache_key(statement)
|
188
|
+
cache_key(["statement", id, Digest::MD5.hexdigest(statement)])
|
170
189
|
end
|
171
190
|
|
172
|
-
def
|
173
|
-
["
|
191
|
+
def run_cache_key(run_id)
|
192
|
+
cache_key(["run", run_id])
|
174
193
|
end
|
175
194
|
|
176
195
|
def schemas
|
@@ -179,8 +198,8 @@ module Blazer
|
|
179
198
|
end
|
180
199
|
|
181
200
|
def tables
|
182
|
-
|
183
|
-
rows.map(&:first)
|
201
|
+
result = run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name FROM information_schema.tables WHERE table_schema IN (?) ORDER BY table_name", schemas]))
|
202
|
+
result.rows.map(&:first)
|
184
203
|
end
|
185
204
|
|
186
205
|
def postgresql?
|
@@ -201,7 +220,7 @@ module Blazer
|
|
201
220
|
|
202
221
|
protected
|
203
222
|
|
204
|
-
def run_statement_helper(statement, comment)
|
223
|
+
def run_statement_helper(statement, comment, run_id)
|
205
224
|
columns = []
|
206
225
|
rows = []
|
207
226
|
error = nil
|
@@ -238,14 +257,24 @@ module Blazer
|
|
238
257
|
end
|
239
258
|
|
240
259
|
cache_data = nil
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
260
|
+
cache = !error && (cache_mode == "all" || (cache_mode == "slow" && duration >= cache_slow_threshold))
|
261
|
+
if cache || run_id
|
262
|
+
cache_data = Marshal.dump([columns, rows, error, cache ? Time.now : nil]) rescue nil
|
263
|
+
end
|
264
|
+
|
265
|
+
if cache && cache_data
|
266
|
+
Blazer.cache.write(statement_cache_key(statement), cache_data, expires_in: cache_expires_in.to_f * 60)
|
267
|
+
end
|
268
|
+
|
269
|
+
if run_id
|
270
|
+
unless cache_data
|
271
|
+
error = "Error storing the results of this query :("
|
272
|
+
cache_data = Marshal.dump([[], [], error, nil])
|
245
273
|
end
|
274
|
+
Blazer.cache.write(run_cache_key(run_id), cache_data, expires_in: 30.seconds)
|
246
275
|
end
|
247
276
|
|
248
|
-
|
277
|
+
Blazer::Result.new(self, columns, rows, error, nil, cache && !cache_data.nil?)
|
249
278
|
end
|
250
279
|
|
251
280
|
def adapter_name
|
@@ -0,0 +1,14 @@
|
|
1
|
+
tryCatch({
|
2
|
+
library(AnomalyDetection)
|
3
|
+
|
4
|
+
args <- commandArgs(trailingOnly = TRUE)
|
5
|
+
|
6
|
+
con <- textConnection(args[1])
|
7
|
+
data <- read.csv(con, stringsAsFactors = FALSE)
|
8
|
+
data$timestamp <- as.POSIXct(data$timestamp)
|
9
|
+
|
10
|
+
res = AnomalyDetectionTs(data, direction = "both", alpha = 0.05)
|
11
|
+
write.csv(res$anoms)
|
12
|
+
}, error = function (e) {
|
13
|
+
write.csv(geterrmessage())
|
14
|
+
})
|
data/lib/blazer/engine.rb
CHANGED
@@ -29,6 +29,12 @@ module Blazer
|
|
29
29
|
end
|
30
30
|
|
31
31
|
Blazer.cache ||= Rails.cache
|
32
|
+
|
33
|
+
Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
|
34
|
+
Blazer.async = Blazer.settings["async"] || false
|
35
|
+
if Blazer.async
|
36
|
+
require "blazer/run_statement_job"
|
37
|
+
end
|
32
38
|
end
|
33
39
|
end
|
34
40
|
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Blazer
|
2
|
+
class Result
|
3
|
+
attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached
|
4
|
+
|
5
|
+
def initialize(data_source, columns, rows, error, cached_at, just_cached)
|
6
|
+
@data_source = data_source
|
7
|
+
@columns = columns
|
8
|
+
@rows = rows
|
9
|
+
@error = error
|
10
|
+
@cached_at = cached_at
|
11
|
+
@just_cached = just_cached
|
12
|
+
end
|
13
|
+
|
14
|
+
def timed_out?
|
15
|
+
error == Blazer::TIMEOUT_MESSAGE
|
16
|
+
end
|
17
|
+
|
18
|
+
def cached?
|
19
|
+
cached_at.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def boom
|
23
|
+
@boom ||= begin
|
24
|
+
boom = {}
|
25
|
+
columns.each_with_index do |key, i|
|
26
|
+
query = data_source.smart_columns[key]
|
27
|
+
if query
|
28
|
+
values = rows.map { |r| r[i] }.compact.uniq
|
29
|
+
result = data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
|
30
|
+
boom[key] = Hash[result.rows.map { |k, v| [k.to_s, v] }]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
boom
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def column_types
|
38
|
+
@column_types ||= begin
|
39
|
+
columns.each_with_index.map do |k, i|
|
40
|
+
v = (rows.find { |r| r[i] } || {})[i]
|
41
|
+
if boom[k]
|
42
|
+
"string"
|
43
|
+
elsif v.is_a?(Numeric)
|
44
|
+
"numeric"
|
45
|
+
elsif v.is_a?(Time) || v.is_a?(Date)
|
46
|
+
"time"
|
47
|
+
elsif v.nil?
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
"string"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def chart_type
|
57
|
+
@chart_type ||= begin
|
58
|
+
if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
|
59
|
+
"line"
|
60
|
+
elsif column_types == ["time", "string", "numeric"]
|
61
|
+
"line2"
|
62
|
+
elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
|
63
|
+
"bar"
|
64
|
+
elsif column_types == ["string", "string", "numeric"]
|
65
|
+
"bar2"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def detect_anomaly
|
71
|
+
anomaly = nil
|
72
|
+
message = nil
|
73
|
+
|
74
|
+
if rows.empty?
|
75
|
+
message = "No data"
|
76
|
+
else
|
77
|
+
if chart_type == "line" || chart_type == "line2"
|
78
|
+
series = []
|
79
|
+
|
80
|
+
if chart_type == "line"
|
81
|
+
columns[1..-1].each_with_index.each do |k, i|
|
82
|
+
series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
|
83
|
+
end
|
84
|
+
else
|
85
|
+
rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
|
86
|
+
series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
current_series = nil
|
91
|
+
begin
|
92
|
+
anomalies = []
|
93
|
+
series.each do |s|
|
94
|
+
current_series = s[:name]
|
95
|
+
anomalies << s[:name] if anomaly?(s[:data])
|
96
|
+
end
|
97
|
+
anomaly = anomalies.any?
|
98
|
+
if anomaly
|
99
|
+
if anomalies.size == 1
|
100
|
+
message = "Anomaly detected in #{anomalies.first}"
|
101
|
+
else
|
102
|
+
message = "Anomalies detected in #{anomalies.to_sentence}"
|
103
|
+
end
|
104
|
+
else
|
105
|
+
message = "No anomalies detected"
|
106
|
+
end
|
107
|
+
rescue => e
|
108
|
+
message = "#{current_series}: #{e.message}"
|
109
|
+
end
|
110
|
+
else
|
111
|
+
message = "Bad format"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
[anomaly, message]
|
116
|
+
end
|
117
|
+
|
118
|
+
def anomaly?(series)
|
119
|
+
series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
|
120
|
+
|
121
|
+
csv_str =
|
122
|
+
CSV.generate do |csv|
|
123
|
+
csv << ["timestamp", "count"]
|
124
|
+
series.each do |row|
|
125
|
+
csv << row
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
timestamps = []
|
130
|
+
r_script = %x[which Rscript].chomp
|
131
|
+
raise "R not found" if r_script.empty?
|
132
|
+
output = %x[#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{Shellwords.escape(csv_str)}]
|
133
|
+
if output.empty?
|
134
|
+
raise "Unknown R error"
|
135
|
+
end
|
136
|
+
|
137
|
+
rows = CSV.parse(output, headers: true)
|
138
|
+
error = rows.first && rows.first["x"]
|
139
|
+
raise error if error
|
140
|
+
|
141
|
+
rows.each do |row|
|
142
|
+
timestamps << Time.parse(row["timestamp"])
|
143
|
+
end
|
144
|
+
timestamps.include?(series.last[0].to_time)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "sucker_punch"
|
2
|
+
|
3
|
+
module Blazer
|
4
|
+
class RunStatementJob
|
5
|
+
include SuckerPunch::Job
|
6
|
+
workers 4
|
7
|
+
|
8
|
+
def perform(result, data_source, statement, options)
|
9
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
10
|
+
data_source.connection_model.connection_pool.with_connection do
|
11
|
+
result << data_source.run_main_statement(statement, options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/blazer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blazer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06-
|
11
|
+
date: 2016-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -143,7 +143,6 @@ files:
|
|
143
143
|
- app/views/blazer/checks/edit.html.erb
|
144
144
|
- app/views/blazer/checks/index.html.erb
|
145
145
|
- app/views/blazer/checks/new.html.erb
|
146
|
-
- app/views/blazer/checks/run.html.erb
|
147
146
|
- app/views/blazer/dashboards/_form.html.erb
|
148
147
|
- app/views/blazer/dashboards/edit.html.erb
|
149
148
|
- app/views/blazer/dashboards/index.html.erb
|
@@ -161,7 +160,10 @@ files:
|
|
161
160
|
- config/routes.rb
|
162
161
|
- lib/blazer.rb
|
163
162
|
- lib/blazer/data_source.rb
|
163
|
+
- lib/blazer/detect_anomalies.R
|
164
164
|
- lib/blazer/engine.rb
|
165
|
+
- lib/blazer/result.rb
|
166
|
+
- lib/blazer/run_statement_job.rb
|
165
167
|
- lib/blazer/version.rb
|
166
168
|
- lib/generators/blazer/install_generator.rb
|
167
169
|
- lib/generators/blazer/templates/config.yml
|
@@ -187,9 +189,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
189
|
version: '0'
|
188
190
|
requirements: []
|
189
191
|
rubyforge_project:
|
190
|
-
rubygems_version: 2.
|
192
|
+
rubygems_version: 2.4.5.1
|
191
193
|
signing_key:
|
192
194
|
specification_version: 4
|
193
195
|
summary: Share data effortlessly with your team
|
194
196
|
test_files: []
|
195
|
-
has_rdoc:
|
@@ -1,11 +0,0 @@
|
|
1
|
-
<p style="text-muted">Running check...</p>
|
2
|
-
|
3
|
-
<script>
|
4
|
-
var data = <%= blazer_json_escape({statement: @query.statement, query_id: @query.id, check: true}.to_json).html_safe %>;
|
5
|
-
|
6
|
-
runQuery(data, function (data) {
|
7
|
-
setTimeout( function () {
|
8
|
-
window.location.href = "<%= checks_path %>";
|
9
|
-
}, 200);
|
10
|
-
});
|
11
|
-
</script>
|