blazer 1.2.1 → 1.3.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.

@@ -87,3 +87,16 @@ input.search:focus {
87
87
  .dashboard a {
88
88
  font-weight: bold;
89
89
  }
90
+
91
+ #search-item {
92
+ display: none;
93
+ }
94
+
95
+ .creator {
96
+ text-align: right;
97
+ color: #999;
98
+ }
99
+
100
+ .vars {
101
+ color: #ccc;
102
+ }
@@ -13,6 +13,9 @@ module Blazer
13
13
 
14
14
  def create
15
15
  @check = Blazer::Check.new(check_params)
16
+ # use creator_id instead of creator
17
+ # since we setup association without checking if column exists
18
+ @check.creator = blazer_user if @check.respond_to?(:creator_id=)
16
19
 
17
20
  if @check.save
18
21
  redirect_to run_check_path(@check)
@@ -41,7 +44,7 @@ module Blazer
41
44
  private
42
45
 
43
46
  def check_params
44
- params.require(:check).permit(:query_id, :emails, :invert)
47
+ params.require(:check).permit(:query_id, :emails, :invert, :schedule)
45
48
  end
46
49
 
47
50
  def set_check
@@ -12,6 +12,9 @@ module Blazer
12
12
 
13
13
  def create
14
14
  @dashboard = Blazer::Dashboard.new
15
+ # use creator_id instead of creator
16
+ # since we setup association without checking if column exists
17
+ @dashboard.creator = blazer_user if @dashboard.respond_to?(:creator_id=)
15
18
 
16
19
  if update_dashboard(@dashboard)
17
20
  redirect_to dashboard_path(@dashboard)
@@ -4,11 +4,20 @@ module Blazer
4
4
 
5
5
  def home
6
6
  set_queries(1000)
7
+ @dashboards =
8
+ Blazer::Dashboard.order(:name).map do |d|
9
+ {
10
+ name: "<strong>#{view_context.link_to(d.name, d)}</strong>",
11
+ creator: blazer_user && d.try(:creator) == blazer_user ? "You" : d.try(:creator).try(Blazer.user_name),
12
+ hide: d.name.gsub(/\s+/, ""),
13
+ vars: nil
14
+ }
15
+ end
7
16
  end
8
17
 
9
18
  def index
10
19
  set_queries
11
- render partial: "index", layout: false
20
+ render json: @queries
12
21
  end
13
22
 
14
23
  def new
@@ -180,7 +189,7 @@ module Blazer
180
189
 
181
190
  def set_queries(limit = nil)
182
191
  @my_queries =
183
- if blazer_user
192
+ if limit && blazer_user
184
193
  favorite_query_ids = Blazer::Audit.where(user_id: blazer_user.id).where("created_at > ?", 30.days.ago).where("query_id IS NOT NULL").group(:query_id).order("count_all desc").count.keys
185
194
  queries = Blazer::Query.named.where(id: favorite_query_ids)
186
195
  queries = queries.includes(:creator) if Blazer.user_class
@@ -194,9 +203,17 @@ module Blazer
194
203
  @queries = @queries.where("id NOT IN (?)", @my_queries.map(&:id)) if @my_queries.any?
195
204
  @queries = @queries.includes(:creator) if Blazer.user_class
196
205
  @queries = @queries.limit(limit) if limit
197
- @trending_queries = Blazer::Audit.group(:query_id).where("created_at > ?", 2.days.ago).having("COUNT(DISTINCT user_id) >= 3").uniq.count(:user_id)
198
- @checks = Blazer::Check.group(:query_id).count
199
- @dashboards = Blazer::Dashboard.order(:name)
206
+
207
+ @queries =
208
+ @queries.map do |q|
209
+ {
210
+ id: q.id,
211
+ name: view_context.link_to(q.name, q),
212
+ creator: blazer_user && q.try(:creator) == blazer_user ? "You" : q.try(:creator).try(Blazer.user_name),
213
+ hide: q.name.gsub(/\s+/, ""),
214
+ vars: extract_vars(q.statement).join(", ")
215
+ }
216
+ end
200
217
  end
201
218
 
202
219
  def set_query
@@ -19,6 +19,8 @@ module Blazer
19
19
  invert ? "failing" : "passing"
20
20
  end
21
21
 
22
+ self.last_run_at = Time.now if self.respond_to?(:last_run_at=)
23
+
22
24
  # do not notify on creation, except when not passing
23
25
  if (state_was || state != "passing") && state != state_was && emails.present?
24
26
  Blazer::CheckMailer.state_change(self, state, state_was, rows.size, error).deliver_later
@@ -17,17 +17,29 @@
17
17
  </script>
18
18
  </div>
19
19
 
20
- <% if @check.respond_to?(:invert) %>
21
- <div class="form-group">
22
- <%= f.label "Fails if" %>
23
- <div class="hide">
24
- <%= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]], {}, {id: "select-invert"} %>
20
+ <% if @check.respond_to?(:invert) %>
21
+ <div class="form-group">
22
+ <%= f.label :invert, "Fails if" %>
23
+ <div class="hide">
24
+ <%= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]] %>
25
+ </div>
26
+ <script>
27
+ $("#check_invert").selectize({}).parent().removeClass("hide");
28
+ </script>
25
29
  </div>
26
- <script>
27
- $("#select-invert").selectize({}).parent().removeClass("hide");
28
- </script>
29
- </div>
30
- <% end %>
30
+ <% end %>
31
+
32
+ <% if @check.respond_to?(:schedule) && Blazer.check_schedules %>
33
+ <div class="form-group">
34
+ <%= f.label :schedule, "Run every" %>
35
+ <div class="hide">
36
+ <%= f.select :schedule, Blazer.check_schedules.map { |v| [v, v] } %>
37
+ </div>
38
+ <script>
39
+ $("#check_schedule").selectize({}).parent().removeClass("hide");
40
+ </script>
41
+ </div>
42
+ <% end %>
31
43
 
32
44
  <div class="form-group">
33
45
  <%= f.label :emails %>
@@ -8,7 +8,8 @@
8
8
  <thead>
9
9
  <tr>
10
10
  <th>Query</th>
11
- <th style="width: 15%;">State</th>
11
+ <th style="width: 10%;">State</th>
12
+ <th style="width: 10%;">Run</th>
12
13
  <th style="width: 20%;">Emails</th>
13
14
  <th style="width: 15%;"></th>
14
15
  </tr>
@@ -22,6 +23,7 @@
22
23
  <small style="font-weight: bold; color: <%= colors[check.state.to_sym] %>;"><%= check.state.upcase %></small>
23
24
  <% end %>
24
25
  </td>
26
+ <td><%= check.schedule if check.respond_to?(:schedule) %></td>
25
27
  <td>
26
28
  <ul class="list-unstyled" style="margin-bottom: 0;">
27
29
  <% check.split_emails.each do |email| %>
@@ -19,7 +19,7 @@
19
19
  <div class="row" style="padding-top: 13px;">
20
20
  <div class="col-sm-9">
21
21
  <%= render partial: "blazer/nav" %>
22
- <h3 style="margin: 0; line-height: 34px; display: inline-block;">
22
+ <h3 style="margin: 0; line-height: 34px; display: inline;">
23
23
  <%= @dashboard.name %>
24
24
  </h3>
25
25
  </div>
@@ -48,8 +48,17 @@
48
48
  <% end %>
49
49
  <%= f.submit @query.persisted? ? "Update" : "Create", class: "btn btn-success" %>
50
50
  </div>
51
- <% if @query.persisted? && (dashboards_count = @query.dashboards.count) > 0 %>
52
- <div class="alert alert-info" style="margin-top: 10px; padding: 8px 12px;">Part of <%= pluralize(dashboards_count, "dashboard") %>. Be careful when editing.</div>
51
+ <% if @query.persisted? %>
52
+ <% dashboards_count = @query.dashboards.count %>
53
+ <% checks_count = @query.checks.count %>
54
+ <% words = [] %>
55
+ <% words << pluralize(dashboards_count, "dashboard") if dashboards_count > 0 %>
56
+ <% words << pluralize(checks_count, "check") if checks_count > 0 %>
57
+ <% if words.any? %>
58
+ <div class="alert alert-info" style="margin-top: 10px; padding: 8px 12px;">
59
+ Part of <%= words.to_sentence %>. Be careful when editing.
60
+ </div>
61
+ <% end %>
53
62
  <% end %>
54
63
  </div>
55
64
  </div>
@@ -25,30 +25,55 @@
25
25
  </tr>
26
26
  </thead>
27
27
  <tbody class="list">
28
- <%= render partial: "index" %>
28
+ <tr id="search-item">
29
+ <td>
30
+ <span class="name"></span>
31
+ <span class="vars"></span>
32
+ <span class="hide"></span>
33
+ </td>
34
+ <td class="creator"></td>
35
+ </tr>
29
36
  </tbody>
30
37
  </table>
31
38
  </div>
32
39
 
33
40
  <script>
34
41
  var options = {
35
- valueNames: ['query', 'creator'],
36
- page: 200
42
+ valueNames: ["name", "vars", "hide", "creator"],
43
+ item: "search-item",
44
+ page: 200,
45
+ indexAsync: true
37
46
  };
47
+ var dashboardValues = <%= blazer_json_escape(@dashboards.to_json).html_safe %>;
48
+ var queryValues = <%= blazer_json_escape(@queries.to_json).html_safe %>;
49
+ var queryList = new List("queries", options, dashboardValues);
50
+ queryList.add(queryValues);
38
51
 
39
- function updateList() {
40
- var userList = new List('queries', options);
41
- userList.search($(".search").val());
52
+ var queryIds = {};
53
+ for (var i = 0; i < queryValues.length; i++) {
54
+ queryIds[queryValues[i].id] = true;
42
55
  }
43
- updateList();
44
56
  </script>
45
57
 
46
- <% if @queries.size == 1000 %>
58
+ <% if @queries.size >= 200 %>
47
59
  <p id="loading" class="text-muted">Loading...</p>
48
60
  <script>
49
- $(".list").load("<%= queries_path %>", function () {
50
- updateList();
61
+ $.getJSON("<%= queries_path %>", function (data) {
62
+ var i, j, newValues, val, size = 500;
51
63
  $("#loading").remove();
64
+ for (i = 0; i < data.length / size; i++) {
65
+ newValues = [];
66
+ for (j = 0; j < size; j++) {
67
+ val = data[i * size + j];
68
+ if (!val) {
69
+ break;
70
+ }
71
+ if (!queryIds[val.id]) {
72
+ newValues.push(val);
73
+ }
74
+ }
75
+ queryList.add(newValues);
76
+ }
52
77
  });
53
78
  </script>
54
79
  <% end %>
@@ -28,8 +28,9 @@
28
28
  <% chart_id = SecureRandom.hex %>
29
29
  <% column_types = blazer_column_types(@columns, @rows, @boom) %>
30
30
  <% chart_options = {id: chart_id, min: nil} %>
31
- <% if (target_index = @columns.keys.map(&:downcase).index("target")) %>
32
- <% chart_options[:library] = {series: {(target_index - 1) => {pointSize: 0, lineWidth: 3, color: "#109618"}}} %>
31
+ <% series_library = {} %>
32
+ <% @columns.keys.select { |k| k.downcase == "target" }.each do |key| %>
33
+ <% series_library[key] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"} %>
33
34
  <% end %>
34
35
  <% if blazer_maps? && @markers.any? %>
35
36
  <div id="map" style="height: <%= @only_chart ? 300 : 500 %>px;"></div>
@@ -62,10 +63,10 @@
62
63
  </script>
63
64
  <% elsif values.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" } %>
64
65
  <% time_k = @columns.keys.first %>
65
- <%= line_chart @columns.keys[1..-1].map{|k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }} }, chart_options %>
66
+ <%= line_chart @columns.keys[1..-1].map{ |k| {name: k, data: @rows.map{|r| [r[time_k], r[k]] }, library: series_library[k]} }, chart_options %>
66
67
  <% elsif values.size == 3 && column_types == ["time", "string", "numeric"] %>
67
68
  <% keys = @columns.keys %>
68
- <%= line_chart @rows.group_by { |r| k = keys[1]; v = r[k]; (@boom[k] || {})[v.to_s] || v }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] } } }, chart_options %>
69
+ <%= line_chart @rows.group_by { |r| k = keys[1]; v = r[k]; (@boom[k] || {})[v.to_s] || v }.map { |name, v| {name: name, data: v.map { |v2| [v2[keys[0]], v2[keys[2]]] }, library: series_library[name]} }, chart_options %>
69
70
  <% elsif values.size >= 2 && column_types == ["string"] + (values.size - 1).times.map { "numeric" } %>
70
71
  <% keys = @columns.keys %>
71
72
  <%= column_chart (values.size - 1).times.map { |i| name = @columns.keys[i + 1]; {name: name, data: @rows.first(20).map { |r| [(@boom[keys[0]] || {})[r[keys[0]].to_s] || r[keys[0]], r[keys[i + 1]]] } } }, id: chart_id %>
@@ -80,46 +81,50 @@
80
81
  <% unless @only_chart && !@no_chart %>
81
82
  <% header_width = 100 / @rows.first.keys.size.to_f %>
82
83
  <div class="results-container">
83
- <table class="table results-table" style="margin-bottom: 0;">
84
- <thead>
85
- <tr>
86
- <% @columns.each do |key, type| %>
87
- <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
88
- <div style="min-width: <%= @min_width_types.include?(key) ? 180 : 60 %>px;">
89
- <%= key %>
90
- </div>
91
- </th>
92
- <% end %>
93
- </tr>
94
- </thead>
95
- <tbody>
96
- <% @rows.each do |row| %>
84
+ <% if @columns.keys == ["QUERY PLAN"] %>
85
+ <pre><code><%= @rows.map { |r| r["QUERY PLAN"] }.join("\n") %></code></pre>
86
+ <% else %>
87
+ <table class="table results-table" style="margin-bottom: 0;">
88
+ <thead>
97
89
  <tr>
98
- <% row.each do |k, v| %>
99
- <td>
100
- <% if v.is_a?(Time) %>
101
- <% v = blazer_time_value(k, v) %>
102
- <% end %>
103
-
104
- <% unless v.nil? %>
105
- <% if v == "" %>
106
- <div class="text-muted">empty string</div>
107
- <% elsif @linked_columns[k] %>
108
- <%= link_to blazer_format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
109
- <% else %>
110
- <%= blazer_format_value(k, v) %>
90
+ <% @columns.each do |key, type| %>
91
+ <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
92
+ <div style="min-width: <%= @min_width_types.include?(key) ? 180 : 60 %>px;">
93
+ <%= key %>
94
+ </div>
95
+ </th>
96
+ <% end %>
97
+ </tr>
98
+ </thead>
99
+ <tbody>
100
+ <% @rows.each do |row| %>
101
+ <tr>
102
+ <% row.each do |k, v| %>
103
+ <td>
104
+ <% if v.is_a?(Time) %>
105
+ <% v = blazer_time_value(k, v) %>
111
106
  <% end %>
112
107
 
113
- <% if v2 = (@boom[k] || {})[v.to_s] %>
114
- <div class="text-muted"><%= v2 %></div>
108
+ <% unless v.nil? %>
109
+ <% if v.is_a?(String) && v == "" %>
110
+ <div class="text-muted">empty string</div>
111
+ <% elsif @linked_columns[k] %>
112
+ <%= link_to blazer_format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
113
+ <% else %>
114
+ <%= blazer_format_value(k, v) %>
115
+ <% end %>
116
+
117
+ <% if v2 = (@boom[k] || {})[v.to_s] %>
118
+ <div class="text-muted"><%= v2 %></div>
119
+ <% end %>
115
120
  <% end %>
116
- <% end %>
117
- </td>
118
- <% end %>
119
- </tr>
120
- <% end %>
121
- </tbody>
122
- </table>
121
+ </td>
122
+ <% end %>
123
+ </tr>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ <% end %>
123
128
  </div>
124
129
  <% end %>
125
130
  <% elsif @only_chart %>
@@ -19,7 +19,7 @@
19
19
  <div class="row" style="padding-top: 13px;">
20
20
  <div class="col-sm-9">
21
21
  <%= render partial: "blazer/nav" %>
22
- <h3 style="margin: 0; line-height: 34px; display: inline-block;">
22
+ <h3 style="margin: 0; line-height: 34px; display: inline;">
23
23
  <%= @query.name %>
24
24
  </h3>
25
25
  </div>
@@ -149,7 +149,7 @@
149
149
  </form>
150
150
  <% end %>
151
151
 
152
- <pre><code><%= @statement %></code></pre>
152
+ <pre style="max-height: 236px; overflow: hidden;" onclick="this.style.maxHeight = 'none';"><code><%= @statement %></code></pre>
153
153
 
154
154
  <% if @success %>
155
155
  <div id="results">
@@ -6,10 +6,13 @@
6
6
  <meta charset="utf-8" />
7
7
 
8
8
  <%= stylesheet_link_tag "blazer/application" %>
9
- <%= javascript_include_tag "https://www.google.com/jsapi", "blazer/application" %>
9
+ <script>
10
+ var Chartkick = {smarterDates: true, smarterDiscrete: true};
11
+ </script>
12
+ <%= javascript_include_tag "blazer/application" %>
10
13
  <% if blazer_maps? %>
11
- <%= stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v2.2.2/mapbox.css" %>
12
- <%= javascript_include_tag "https://api.mapbox.com/mapbox.js/v2.2.2/mapbox.js" %>
14
+ <%= stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css" %>
15
+ <%= javascript_include_tag "https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js" %>
13
16
  <% end %>
14
17
  <%= csrf_meta_tags %>
15
18
  </head>
@@ -4,9 +4,10 @@ require "chartkick"
4
4
  require "blazer/version"
5
5
  require "blazer/data_source"
6
6
  require "blazer/engine"
7
- require "blazer/tasks"
8
7
 
9
8
  module Blazer
9
+ class TimeoutNotSupported < StandardError; end
10
+
10
11
  class << self
11
12
  attr_accessor :audit
12
13
  attr_reader :time_zone
@@ -17,11 +18,18 @@ module Blazer
17
18
  attr_accessor :from_email
18
19
  attr_accessor :cache
19
20
  attr_accessor :transform_statement
21
+ attr_accessor :check_schedules
20
22
  end
21
23
  self.audit = true
22
24
  self.user_name = :name
25
+ self.check_schedules = ["5 minutes", "1 hour", "1 day"]
23
26
 
24
27
  TIMEOUT_MESSAGE = "Query timed out :("
28
+ TIMEOUT_ERRORS = [
29
+ "canceling statement due to statement timeout", # postgres
30
+ "cancelled on user's request", # redshift
31
+ "system requested abort" # redshift
32
+ ]
25
33
 
26
34
  def self.time_zone=(time_zone)
27
35
  @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
@@ -50,25 +58,36 @@ module Blazer
50
58
  end
51
59
  end
52
60
 
53
- def self.run_checks
54
- Blazer::Check.includes(:query).find_each do |check|
61
+ def self.run_checks(schedule: nil)
62
+ checks = Blazer::Check.includes(:query)
63
+ checks = checks.where(schedule: schedule) if schedule
64
+ checks.find_each do |check|
55
65
  rows = nil
56
66
  error = nil
57
- tries = 0
58
- # try 3 times on timeout errors
59
- while tries < 3
60
- rows, error, cached_at = data_sources[check.query.data_source].run_statement(check.query.statement, refresh_cache: true)
61
- if error == Blazer::TIMEOUT_MESSAGE
62
- Rails.logger.info "[blazer timeout] query=#{check.query.name}"
63
- tries += 1
64
- sleep(10)
65
- else
66
- break
67
+ tries = 1
68
+
69
+ ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
70
+ # try 3 times on timeout errors
71
+ while tries <= 3
72
+ rows, error, cached_at = data_sources[check.query.data_source].run_statement(check.query.statement, refresh_cache: true)
73
+ if error == Blazer::TIMEOUT_MESSAGE
74
+ Rails.logger.info "[blazer timeout] query=#{check.query.name}"
75
+ tries += 1
76
+ sleep(10)
77
+ else
78
+ break
79
+ end
67
80
  end
81
+ check.update_state(rows, error)
82
+ # TODO use proper logfmt
83
+ Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{rows.try(:size)} error=#{error}"
84
+
85
+ instrument[:state] = check.state
86
+ instrument[:rows] = rows.try(:size)
87
+ instrument[:error] = error
88
+ instrument[:tries] = tries
68
89
  end
69
- check.update_state(rows, error)
70
- # TODO use proper logfmt
71
- Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{rows.try(:size)} error=#{error}"
90
+
72
91
  end
73
92
  end
74
93