blazer 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/CONTRIBUTING.md +9 -7
  4. data/README.md +59 -19
  5. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  10. data/app/assets/javascripts/blazer/Chart.js +4195 -3884
  11. data/app/assets/javascripts/blazer/Sortable.js +1493 -1097
  12. data/app/assets/javascripts/blazer/ace/ace.js +21294 -4
  13. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1991 -3
  14. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -1
  15. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -1
  16. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -1
  17. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -1
  18. data/app/assets/javascripts/blazer/application.js +4 -3
  19. data/app/assets/javascripts/blazer/bootstrap.js +623 -612
  20. data/app/assets/javascripts/blazer/chartkick.js +1769 -1248
  21. data/app/assets/javascripts/blazer/daterangepicker.js +263 -115
  22. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  23. data/app/assets/javascripts/blazer/{jquery_ujs.js → jquery-ujs.js} +161 -75
  24. data/app/assets/javascripts/blazer/jquery.js +9506 -9450
  25. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +321 -259
  26. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1212 -0
  27. data/app/assets/javascripts/blazer/queries.js +1 -1
  28. data/app/assets/javascripts/blazer/routes.js +1 -1
  29. data/app/assets/javascripts/blazer/selectize.js +3828 -3604
  30. data/app/assets/javascripts/blazer/stupidtable.js +255 -88
  31. data/app/assets/javascripts/blazer/vue.js +8015 -4583
  32. data/app/assets/stylesheets/blazer/application.css +13 -1
  33. data/app/assets/stylesheets/blazer/bootstrap.css.erb +879 -325
  34. data/app/assets/stylesheets/blazer/daterangepicker.css +269 -0
  35. data/app/assets/stylesheets/blazer/selectize.default.css +26 -10
  36. data/app/controllers/blazer/base_controller.rb +7 -1
  37. data/app/controllers/blazer/checks_controller.rb +1 -1
  38. data/app/controllers/blazer/queries_controller.rb +7 -8
  39. data/app/mailers/blazer/slack_notifier.rb +76 -0
  40. data/app/models/blazer/check.rb +9 -0
  41. data/app/views/blazer/_variables.html.erb +38 -18
  42. data/app/views/blazer/checks/_form.html.erb +9 -1
  43. data/app/views/blazer/checks/index.html.erb +4 -1
  44. data/app/views/blazer/queries/_form.html.erb +2 -2
  45. data/app/views/blazer/queries/docs.html.erb +138 -0
  46. data/app/views/blazer/queries/show.html.erb +1 -1
  47. data/app/views/layouts/blazer/application.html.erb +2 -2
  48. data/config/routes.rb +1 -1
  49. data/lib/blazer.rb +22 -15
  50. data/lib/blazer/adapters/bigquery_adapter.rb +5 -4
  51. data/lib/blazer/adapters/elasticsearch_adapter.rb +14 -17
  52. data/lib/blazer/adapters/mongodb_adapter.rb +1 -1
  53. data/lib/blazer/adapters/sql_adapter.rb +7 -1
  54. data/lib/blazer/data_source.rb +0 -1
  55. data/lib/blazer/engine.rb +2 -0
  56. data/lib/blazer/run_statement_job.rb +6 -9
  57. data/lib/blazer/version.rb +1 -1
  58. data/lib/generators/blazer/templates/{config.yml → config.yml.tt} +3 -0
  59. data/lib/generators/blazer/templates/{install.rb → install.rb.tt} +1 -0
  60. data/lib/tasks/blazer.rake +2 -1
  61. metadata +24 -30
  62. data/.gitattributes +0 -1
  63. data/.github/ISSUE_TEMPLATE.md +0 -7
  64. data/.gitignore +0 -14
  65. data/Gemfile +0 -4
  66. data/Rakefile +0 -1
  67. data/app/assets/javascripts/blazer/highlight.pack.js +0 -1
  68. data/app/assets/javascripts/blazer/moment-timezone.js +0 -1007
  69. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +0 -375
  70. data/app/views/blazer/queries/schema.html.erb +0 -20
  71. data/blazer.gemspec +0 -27
@@ -0,0 +1,76 @@
1
+ require "net/http"
2
+
3
+ module Blazer
4
+ class SlackNotifier
5
+ def self.state_change(check, state, state_was, rows_count, error, check_type)
6
+ check.split_slack_channels.each do |channel|
7
+ text =
8
+ if error
9
+ error
10
+ elsif rows_count > 0 && check_type == "bad_data"
11
+ pluralize(rows_count, "row")
12
+ end
13
+
14
+ payload = {
15
+ channel: channel,
16
+ attachments: [
17
+ {
18
+ title: escape("Check #{state.titleize}: #{check.query.name}"),
19
+ title_link: query_url(check.query_id),
20
+ text: escape(text),
21
+ color: state == "passing" ? "good" : "danger"
22
+ }
23
+ ]
24
+ }
25
+
26
+ post(Blazer.slack_webhook_url, payload)
27
+ end
28
+ end
29
+
30
+ def self.failing_checks(channel, checks)
31
+ text =
32
+ checks.map do |check|
33
+ "<#{query_url(check.query_id)}|#{escape(check.query.name)}> #{escape(check.state)}"
34
+ end
35
+
36
+ payload = {
37
+ channel: channel,
38
+ attachments: [
39
+ {
40
+ title: escape("#{pluralize(checks.size, "Check")} Failing"),
41
+ text: text.join("\n"),
42
+ color: "warning"
43
+ }
44
+ ]
45
+ }
46
+
47
+ post(Blazer.slack_webhook_url, payload)
48
+ end
49
+
50
+ # https://api.slack.com/docs/message-formatting#how_to_escape_characters
51
+ # - Replace the ampersand, &, with &amp;
52
+ # - Replace the less-than sign, < with &lt;
53
+ # - Replace the greater-than sign, > with &gt;
54
+ # That's it. Don't HTML entity-encode the entire message.
55
+ def self.escape(str)
56
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") if str
57
+ end
58
+
59
+ def self.pluralize(*args)
60
+ ActionController::Base.helpers.pluralize(*args)
61
+ end
62
+
63
+ def self.query_url(id)
64
+ Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
65
+ end
66
+
67
+ def self.post(url, payload)
68
+ uri = URI.parse(url)
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ http.use_ssl = true
71
+ http.open_timeout = 3
72
+ http.read_timeout = 5
73
+ http.post(uri.request_uri, payload.to_json)
74
+ end
75
+ end
76
+ end
@@ -14,6 +14,14 @@ module Blazer
14
14
  emails.to_s.downcase.split(",").map(&:strip)
15
15
  end
16
16
 
17
+ def split_slack_channels
18
+ if Blazer.slack?
19
+ slack_channels.to_s.downcase.split(",").map(&:strip)
20
+ else
21
+ []
22
+ end
23
+ end
24
+
17
25
  def update_state(result)
18
26
  check_type =
19
27
  if respond_to?(:check_type)
@@ -61,6 +69,7 @@ module Blazer
61
69
  # do not notify on creation, except when not passing
62
70
  if (state_was != "new" || state != "passing") && state != state_was && emails.present?
63
71
  Blazer::CheckMailer.state_change(self, state, state_was, result.rows.size, message, result.columns, result.rows.first(10).as_json, result.column_types, check_type).deliver_now
72
+ Blazer::SlackNotifier.state_change(self, state, state_was, result.rows.size, message, check_type)
64
73
  end
65
74
  save! if changed?
66
75
  end
@@ -1,4 +1,13 @@
1
1
  <% if @bind_vars.any? %>
2
+ <script>
3
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
4
+ var now = moment.tz(timeZone)
5
+ var format = "YYYY-MM-DD"
6
+
7
+ function toDate(time) {
8
+ return moment.tz(time.format(format), timeZone)
9
+ }
10
+ </script>
2
11
  <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 10px;">
3
12
  <% date_vars = ["start_time", "end_time"] %>
4
13
  <% if (date_vars - @bind_vars).empty? %>
@@ -16,18 +25,37 @@
16
25
  create: true
17
26
  });
18
27
  </script>
19
- <% else %>
20
- <%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
21
- <% if var.end_with?("_at") %>
22
- <script>
23
- $("#<%= var %>").daterangepicker({singleDatePicker: true, locale: {format: "YYYY-MM-DD"}, autoUpdateInput: false});
28
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
29
+ <%= hidden_field_tag var, params[var] %>
30
+
31
+ <div class="selectize-control single" style="width: 200px;">
32
+ <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
33
+ <span>Select a date</span>
34
+ </div>
35
+ </div>
36
+
37
+ <script>
38
+ (function() {
39
+ var input = $("#<%= var %>")
40
+ var datePicker = $("#<%= var %>-select")
41
+ datePicker.daterangepicker({
42
+ singleDatePicker: true,
43
+ locale: {format: format},
44
+ autoUpdateInput: false,
45
+ startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
46
+ })
24
47
  // hack to start with empty date
25
- $("#<%= var %>").on("apply.daterangepicker", function(ev, picker) {
26
- $(this).val(picker.startDate.format("YYYY-MM-DD"));
27
- $(this).change();
48
+ datePicker.on("apply.daterangepicker", function(ev, picker) {
49
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
50
+ input.val(toDate(picker.startDate).utc().format())
51
+ submitIfCompleted($("#<%= var %>").closest("form"))
28
52
  });
29
- </script>
30
- <% end %>
53
+ var picker = datePicker.data("daterangepicker")
54
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
55
+ })()
56
+ </script>
57
+ <% else %>
58
+ <%= text_field_tag var, params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !params[var], class: "form-control" %>
31
59
  <% end %>
32
60
  <% end %>
33
61
 
@@ -44,18 +72,10 @@
44
72
  </div>
45
73
 
46
74
  <script>
47
- <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
48
- var format = "YYYY-MM-DD"
49
- var now = moment.tz(timeZone)
50
-
51
75
  function dateStr(daysAgo) {
52
76
  return now.clone().subtract(daysAgo || 0, "days").format(format)
53
77
  }
54
78
 
55
- function toDate(time) {
56
- return moment.tz(time.format(format), timeZone)
57
- }
58
-
59
79
  function setTimeInputs(start, end) {
60
80
  $("#start_time").val(toDate(start).utc().format())
61
81
  $("#end_time").val(toDate(end).endOf("day").utc().format())
@@ -60,7 +60,15 @@
60
60
  <%= f.label :emails %>
61
61
  <%= f.text_field :emails, placeholder: "Optional, comma separated", class: "form-control" %>
62
62
  </div>
63
- <p class="text-muted">Emails are sent when a check starts failing, and when it starts passing again.
63
+
64
+ <% if Blazer.slack? %>
65
+ <div class="form-group">
66
+ <%= f.label :slack_channels %>
67
+ <%= f.text_field :slack_channels, placeholder: "Optional, comma separated", class: "form-control" %>
68
+ </div>
69
+ <% end %>
70
+
71
+ <p class="text-muted">Emails <%= Blazer.slack? ? "and Slack notifications " : nil %>are sent when a check starts failing, and when it starts passing again.
64
72
  <p>
65
73
  <% if @check.persisted? %>
66
74
  <%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
@@ -9,7 +9,7 @@
9
9
  <th>Query</th>
10
10
  <th style="width: 10%;">State</th>
11
11
  <th style="width: 10%;">Run</th>
12
- <th style="width: 20%;">Emails</th>
12
+ <th style="width: 20%;">Notify</th>
13
13
  <th style="width: 15%;"></th>
14
14
  </tr>
15
15
  </thead>
@@ -28,6 +28,9 @@
28
28
  <% check.split_emails.each do |email| %>
29
29
  <li><%= email %></li>
30
30
  <% end %>
31
+ <% check.split_slack_channels.each do |channel| %>
32
+ <li><%= channel %></li>
33
+ <% end %>
31
34
  </ul>
32
35
  </td>
33
36
  <td style="text-align: right; padding: 1px;">
@@ -16,7 +16,7 @@
16
16
  <div class="pull-left" style="margin-top: 9px;">
17
17
  <%= link_to "Back", :back %>
18
18
  </div>
19
- <a :href="dataSourcePath" target="_blank" style="margin-right: 10px;">Schema</a>
19
+ <a :href="dataSourcePath" target="_blank" style="margin-right: 10px;">Docs</a>
20
20
  <%= f.select :data_source, Blazer.data_sources.values.select { |ds| q = @query.dup; q.data_source = ds.id; q.editable?(blazer_user) }.map { |ds| [ds.name, ds.id] }, {}, class: ("hide" if Blazer.data_sources.size <= 1), style: "width: 140px;" %>
21
21
  <div id="tables" style="display: inline-block; width: 250px; margin-right: 10px;">
22
22
  <select id="table_names" style="width: 240px;" placeholder="Preview table"></select>
@@ -173,7 +173,7 @@
173
173
  editor.focus()
174
174
  },
175
175
  adjustHeight: function() {
176
- // http://stackoverflow.com/questions/11584061/
176
+ // https://stackoverflow.com/questions/11584061/
177
177
  var editor = this.editor
178
178
  var lines = editor.getSession().getScreenLength()
179
179
  if (lines < 9) {
@@ -0,0 +1,138 @@
1
+ <% blazer_title @data_source.name %>
2
+
3
+ <h1><%= @data_source.name %></h1>
4
+
5
+ <h2>Smart Variables</h2>
6
+
7
+ <p>Use these variable names to get a dropdown.</p>
8
+
9
+ <table class="table" style="max-width: 500px;">
10
+ <thead>
11
+ <tr>
12
+ <th>Variable</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% @data_source.smart_variables.each do |k, _| %>
17
+ <tr>
18
+ <td><code>{<%= k %>}</code></td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+
24
+ <p>Use <code>{start_time}</code> and <code>{end_time}</code> for a date range selector. End a variable name with <code>_at</code> for a date selector.</p>
25
+
26
+ <h2>Linked Columns</h2>
27
+
28
+ <p>Use these column names to link results to other pages.</p>
29
+
30
+ <table class="table" style="max-width: 500px;">
31
+ <thead>
32
+ <tr>
33
+ <th style="width: 20%;">Name</th>
34
+ <th>URL</th>
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ <% @data_source.linked_columns.each do |k, v| %>
39
+ <tr>
40
+ <td><%= k %></td>
41
+ <td><%= v %></td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+
47
+ <p>Values that match the format of a URL will be linked automatically.</p>
48
+
49
+ <h2>Smart Columns</h2>
50
+
51
+ <p>Use these column names to show additional data.</p>
52
+
53
+ <table class="table" style="max-width: 500px;">
54
+ <thead>
55
+ <tr>
56
+ <th>Name</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ <% @data_source.smart_columns.each do |k, _| %>
61
+ <tr>
62
+ <td><%= k %></td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+
68
+ <h2>Charts</h2>
69
+
70
+ <p>Use specific combinations of column types to generate charts.</p>
71
+
72
+ <table class="table" style="max-width: 500px;">
73
+ <thead>
74
+ <tr>
75
+ <th style="width: 20%;">Chart</th>
76
+ <th>Column Types</th>
77
+ </tr>
78
+ </thead>
79
+ <tbody>
80
+ <tr>
81
+ <td>Line</td>
82
+ <td>2+ columns - timestamp, numeric(s)</td>
83
+ </tr>
84
+ <tr>
85
+ <td>Line</td>
86
+ <td>3 columns - timestamp, string, numeric</td>
87
+ </tr>
88
+ <tr>
89
+ <td>Column</td>
90
+ <td>2+ columns - string, numeric(s)</td>
91
+ </tr>
92
+ <tr>
93
+ <td>Column</td>
94
+ <td>3 columns - string, string, numeric</td>
95
+ </tr>
96
+ <tr>
97
+ <td>Scatter</td>
98
+ <td>2 columns - both numeric</td>
99
+ </tr>
100
+ <tr>
101
+ <td>Map</td>
102
+ <td>
103
+ Named <code>latitude</code> and <code>longitude</code>, or <code>lat</code> and <code>lon</code>, or <code>lat</code> and <code>lng</code>
104
+ <% if !blazer_maps? %>
105
+ <br />
106
+ <strong>Needs configured</strong>
107
+ <% end %>
108
+ </td>
109
+ </tr>
110
+ </tbody>
111
+ </table>
112
+
113
+ <p>Use the column name <code>target</code> to draw a line for goals.</p>
114
+
115
+ <h2>Schema</h2>
116
+
117
+ <% @schema.each do |table| %>
118
+ <table class="table" style="max-width: 500px;">
119
+ <thead>
120
+ <tr>
121
+ <th colspan="2">
122
+ <%= table[:table] %>
123
+ <% if table[:schema] != "public" %>
124
+ <span class="text-muted" style="font-weight: normal;"><%= table[:schema] %></span>
125
+ <% end %>
126
+ </th>
127
+ </tr>
128
+ </thead>
129
+ <tbody>
130
+ <% table[:columns].each do |column| %>
131
+ <tr>
132
+ <td style="width: 60%;"><%= column[:name] %></td>
133
+ <td class="text-muted"><%= column[:data_type] %></td>
134
+ </tr>
135
+ <% end %>
136
+ </tbody>
137
+ </table>
138
+ <% end %>
@@ -62,7 +62,7 @@
62
62
  </script>
63
63
  <% end %>
64
64
 
65
- <% unless %w(mongodb elasticsearch).include?(Blazer.data_sources[@query.data_source].adapter) %>
65
+ <% unless %w(mongodb).include?(Blazer.data_sources[@query.data_source].adapter) %>
66
66
  <script>
67
67
  // do not highlight really long queries
68
68
  // this can lead to performance issues
@@ -11,8 +11,8 @@
11
11
  <%= blazer_js_var "rootPath", root_path %>
12
12
  </script>
13
13
  <% if blazer_maps? %>
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" %>
14
+ <%= stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.css", integrity: "sha384-o8tecBIfqi9yU5yYK2Ne/9A9hlOGFV9MBvCpmemvJH1XxqOe6h8Bl4mLxMi6PgjG", crossorigin: "anonymous" %>
15
+ <%= javascript_include_tag "https://api.mapbox.com/mapbox.js/v3.1.1/mapbox.js", integrity: "sha384-j81LqvtvYigFzGSUgAoFijpvoq4yGoCJSOXI9DFaUEpenR029MBE3E/X5Gr+WdO0", crossorigin: "anonymous" %>
16
16
  <% end %>
17
17
  <%= csrf_meta_tags %>
18
18
  </head>
data/config/routes.rb CHANGED
@@ -4,7 +4,7 @@ Blazer::Engine.routes.draw do
4
4
  post :cancel, on: :collection
5
5
  post :refresh, on: :member
6
6
  get :tables, on: :collection
7
- get :schema, on: :collection
7
+ get :docs, on: :collection
8
8
  end
9
9
  resources :checks, except: [:show] do
10
10
  get :run, on: :member
data/lib/blazer.rb CHANGED
@@ -39,6 +39,8 @@ module Blazer
39
39
  attr_accessor :images
40
40
  attr_accessor :query_viewable
41
41
  attr_accessor :query_editable
42
+ attr_accessor :override_csp
43
+ attr_accessor :slack_webhook_url
42
44
  end
43
45
  self.audit = true
44
46
  self.user_name = :name
@@ -46,6 +48,7 @@ module Blazer
46
48
  self.anomaly_checks = false
47
49
  self.async = false
48
50
  self.images = false
51
+ self.override_csp = false
49
52
 
50
53
  TIMEOUT_MESSAGE = "Query timed out :("
51
54
  TIMEOUT_ERRORS = [
@@ -93,20 +96,11 @@ module Blazer
93
96
 
94
97
  def self.data_sources
95
98
  @data_sources ||= begin
96
- ds = Hash[
97
- settings["data_sources"].map do |id, s|
98
- [id, Blazer::DataSource.new(id, s)]
99
- end
100
- ]
101
- ds.default = ds.values.first
99
+ ds = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
100
+ settings["data_sources"].each do |id, s|
101
+ ds[id] = Blazer::DataSource.new(id, s)
102
+ end
102
103
  ds
103
-
104
- # TODO Blazer 2.0
105
- # ds2 = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
106
- # ds.each do |k, v|
107
- # ds2[k] = v
108
- # end
109
- # ds2
110
104
  end
111
105
  end
112
106
 
@@ -126,8 +120,6 @@ module Blazer
126
120
  end
127
121
 
128
122
  def self.run_check(check)
129
- rows = nil
130
- error = nil
131
123
  tries = 1
132
124
 
133
125
  ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
@@ -173,10 +165,15 @@ module Blazer
173
165
 
174
166
  def self.send_failing_checks
175
167
  emails = {}
168
+ slack_channels = {}
169
+
176
170
  Blazer::Check.includes(:query).where(state: ["failing", "error", "timed out", "disabled"]).find_each do |check|
177
171
  check.split_emails.each do |email|
178
172
  (emails[email] ||= []) << check
179
173
  end
174
+ check.split_slack_channels.each do |channel|
175
+ (slack_channels[channel] ||= []) << check
176
+ end
180
177
  end
181
178
 
182
179
  emails.each do |email, checks|
@@ -184,6 +181,16 @@ module Blazer
184
181
  Blazer::CheckMailer.failing_checks(email, checks).deliver_now
185
182
  end
186
183
  end
184
+
185
+ slack_channels.each do |channel, checks|
186
+ Safely.safely do
187
+ Blazer::SlackNotifier.failing_checks(channel, checks)
188
+ end
189
+ end
190
+ end
191
+
192
+ def self.slack?
193
+ slack_webhook_url.present?
187
194
  end
188
195
 
189
196
  def self.adapters