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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ebec2b00f0b97033272541d144c26ad98379be5
4
- data.tar.gz: db8f009f5cb2c12d4a960c2ce5cab96f33e287b0
3
+ metadata.gz: 63faea5b7fa9ad8d4365931c80851e33d80553e8
4
+ data.tar.gz: 30b6a8d2dd631512a5cd82a6fabe119e7e9d5963
5
5
  SHA512:
6
- metadata.gz: e267ad7981a3ed4d8774e1ef255ebecea5d43edfe97addbe07f2a95a43c1af8344e69834a9978563400294f6ff5012061e0a832cf0bb99d50acf67aa64a1d331
7
- data.tar.gz: cdea706a25e45acea0cfb660d31bfb6f3d2163319c781ce91bca213b1df0efa69124f339b374d4b7703ba275f486b8c757043889237e3eb6982767538b8ec140
6
+ metadata.gz: 69a08a57180385ed46ae3bd776fa3e67bf885ed9845dc7508f70d1d7d883d940120c4b03167800726fe6d0dcb728518e4cfbdc32b6fc2647813d381eea0d1670
7
+ data.tar.gz: 681fdbe806cfc7675335802166199ed11124e6c443c4d8da31e279c47b48ed847a5d2061d24d657d5c343c47a284b929dc93f4188ca7fa7fcae4d47bcd225d67
@@ -1,3 +1,9 @@
1
+ ## 1.5.0
2
+
3
+ - Added new bar chart format
4
+ - Added anomaly detection checks
5
+ - Added `async` option for polling
6
+
1
7
  ## 1.4.0
2
8
 
3
9
  - Added `slow` cache mode
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(success).fail( function(jqXHR, textStatus, errorThrown) {
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 ((!key || this.options.hasOwnProperty(key)) && !this.settings.allowEmptyOption) return false;
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
- columns, rows, error, cached_at = data_source.run_statement(query)
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
- Blazer::Dashboard.order(:name).map do |d|
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
- columns, rows, error, cached_at = data_source.run_statement(query)
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
- if @success
73
- @query = Query.find_by(id: params[:query_id]) if params[:query_id]
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
- render_run
81
- end
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
- respond_to do |format|
84
- format.html do
85
- render layout: false
86
- end
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)
@@ -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(rows, error)
12
- invert = respond_to?(:invert) && self.invert
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 error
15
- if error == Blazer::TIMEOUT_MESSAGE
16
- "timed out"
17
- else
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
- invert ? "passing" : "failing"
44
+ elsif result.rows.any?
45
+ check_type == "missing_data" ? "passing" : "failing"
22
46
  else
23
- invert ? "failing" : "passing"
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 state == "timed out"
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, error).deliver_later
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
@@ -1,5 +1,6 @@
1
1
  module Blazer
2
2
  class Dashboard < ActiveRecord::Base
3
+ belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
3
4
  has_many :dashboard_queries, dependent: :destroy
4
5
  has_many :queries, through: :dashboard_queries
5
6
 
@@ -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
- <li><%= link_to "New Check", new_check_path %></li>
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, Blazer::Query.named.order(:name).map { |q| [q.name, q.id] }, {include_blank: true} %>
13
+ <%= f.select :query_id, [], {include_blank: true} %>
14
14
  </div>
15
15
  <script>
16
- $("#check_query_id").selectize().parents(".hide").removeClass("hide");
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?(:invert) %>
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 style="font-weight: bold; color: <%= colors[check.state.parameterize("_").to_sym] %>;"><%= check.state.upcase %></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, options_for_select(Blazer::Query.named.order(:name).map { |q| [q.name, q.id] }), {include_blank: true, placeholder: "Select chart"} %>
41
+ <%= select_tag :query_id, nil, {include_blank: true, placeholder: "Select chart"} %>
42
42
  </div>
43
43
  <script>
44
- $("#query_id").selectize().parents(".hide").removeClass("hide");
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="<%= url_for(params) %>" class="form-inline" style="margin-bottom: 10px;">
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: editor.getValue().replace(/\n/g, "\r\n"), data_source: $("#query_data_source").val()});
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"><%= pluralize(@rows.size, "row") %></p>
27
+ <p class="text-muted">
28
+ <%= pluralize(@rows.size, "row") %>
29
+
30
+ <% @checks.select(&:state).each do |check| %>
31
+ &middot; <small class="check-state <%= check.state.parameterize("_") %>"><%= link_to check.state.upcase, edit_check_path(check) %></small>
32
+ <% if check.try(:message) %>
33
+ &middot; <%= 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 = blazer_column_types(@columns, @rows, @boom) %>
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 values.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" } %>
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 values.size == 3 && column_types == ["time", "string", "numeric"] %>
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 values.size >= 2 && column_types == ["string"] + (values.size - 1).times.map { "numeric" } %>
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?(key) ? 180 : 60 %>px;">
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="<%= url_for(params) %>" class="form-inline" style="margin-bottom: 10px;">
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 %>
@@ -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
- columns, rows, error, cached_at = data_source.run_statement(statement, refresh_cache: true)
88
- if error == Blazer::TIMEOUT_MESSAGE
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
- data_sources[check.query.data_source].reconnect
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(rows, error)
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
@@ -110,42 +110,52 @@ module Blazer
110
110
  end
111
111
 
112
112
  start_time = Time.now
113
- columns, rows, error, cached_at, just_cached = run_statement(statement, options.merge(with_just_cached: true))
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 = error == Blazer::TIMEOUT_MESSAGE if audit.respond_to?(:timed_out=)
120
- audit.cached = cached_at.present? if audit.respond_to?(:cached=)
121
- if !cached_at && duration >= 10
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 && error != Blazer::TIMEOUT_MESSAGE
127
+ if query && !result.timed_out?
128
128
  query.checks.each do |check|
129
- check.update_state(rows, error)
129
+ check.update_state(result)
130
130
  end
131
131
  end
132
132
 
133
- [columns, rows, error, cached_at, just_cached]
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
- columns = nil
138
- rows = nil
139
- error = nil
140
- cached_at = nil
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 rows
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
- columns, rows, error, just_cached = run_statement_helper(statement, comment)
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
- output = [columns, rows, error, cached_at]
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(cache_key(statement))
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 cache_key(statement)
173
- ["blazer", "v3", id, Digest::MD5.hexdigest(statement)].join("/")
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
- columns, rows, error, cached_at = run_statement(connection_model.send(:sanitize_sql_array, ["SELECT table_name FROM information_schema.tables WHERE table_schema IN (?) ORDER BY table_name", schemas]))
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
- if !error && (cache_mode == "all" || (cache_mode == "slow" && duration >= cache_slow_threshold))
242
- cache_data = Marshal.dump([columns, rows, Time.now]) rescue nil
243
- if cache_data
244
- Blazer.cache.write(cache_key(statement), cache_data, expires_in: cache_expires_in.to_f * 60)
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
- [columns, rows, error, !cache_data.nil?]
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
+ })
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Blazer
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -36,7 +36,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration
36
36
  t.string :state
37
37
  t.string :schedule
38
38
  t.text :emails
39
- t.boolean :invert
39
+ t.string :check_type
40
+ t.text :message
40
41
  t.timestamp :last_run_at
41
42
  t.timestamps
42
43
  end
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.0
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-10 00:00:00.000000000 Z
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.6.1
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>