finery 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +426 -0
  3. data/CONTRIBUTING.md +49 -0
  4. data/LICENSE.txt +25 -0
  5. data/README.md +1144 -0
  6. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  7. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  11. data/app/assets/images/blazer/favicon.png +0 -0
  12. data/app/assets/javascripts/blazer/Sortable.js +3709 -0
  13. data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
  14. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
  15. data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
  16. data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
  17. data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
  18. data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
  19. data/app/assets/javascripts/blazer/ace.js +6 -0
  20. data/app/assets/javascripts/blazer/application.js +87 -0
  21. data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
  22. data/app/assets/javascripts/blazer/chart.umd.js +13 -0
  23. data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
  24. data/app/assets/javascripts/blazer/chartjs-plugin-annotation.min.js +7 -0
  25. data/app/assets/javascripts/blazer/chartkick.js +2570 -0
  26. data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
  27. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  28. data/app/assets/javascripts/blazer/highlight.min.js +466 -0
  29. data/app/assets/javascripts/blazer/jquery.js +10872 -0
  30. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  31. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  32. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
  33. data/app/assets/javascripts/blazer/moment.js +5685 -0
  34. data/app/assets/javascripts/blazer/queries.js +130 -0
  35. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  36. data/app/assets/javascripts/blazer/routes.js +26 -0
  37. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  38. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  40. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  41. data/app/assets/stylesheets/blazer/application.css +243 -0
  42. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  43. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  44. data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
  45. data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
  46. data/app/assets/stylesheets/blazer/github.css +125 -0
  47. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  48. data/app/controllers/blazer/base_controller.rb +133 -0
  49. data/app/controllers/blazer/checks_controller.rb +56 -0
  50. data/app/controllers/blazer/dashboards_controller.rb +99 -0
  51. data/app/controllers/blazer/queries_controller.rb +468 -0
  52. data/app/controllers/blazer/uploads_controller.rb +147 -0
  53. data/app/helpers/blazer/base_helper.rb +83 -0
  54. data/app/models/blazer/audit.rb +6 -0
  55. data/app/models/blazer/check.rb +104 -0
  56. data/app/models/blazer/connection.rb +5 -0
  57. data/app/models/blazer/dashboard.rb +17 -0
  58. data/app/models/blazer/dashboard_query.rb +9 -0
  59. data/app/models/blazer/query.rb +42 -0
  60. data/app/models/blazer/record.rb +5 -0
  61. data/app/models/blazer/upload.rb +11 -0
  62. data/app/models/blazer/uploads_connection.rb +7 -0
  63. data/app/views/blazer/_nav.html.erb +18 -0
  64. data/app/views/blazer/_variables.html.erb +127 -0
  65. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  66. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  67. data/app/views/blazer/checks/_form.html.erb +79 -0
  68. data/app/views/blazer/checks/edit.html.erb +3 -0
  69. data/app/views/blazer/checks/index.html.erb +72 -0
  70. data/app/views/blazer/checks/new.html.erb +3 -0
  71. data/app/views/blazer/dashboards/_form.html.erb +82 -0
  72. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  73. data/app/views/blazer/dashboards/new.html.erb +3 -0
  74. data/app/views/blazer/dashboards/show.html.erb +53 -0
  75. data/app/views/blazer/queries/_caching.html.erb +16 -0
  76. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  77. data/app/views/blazer/queries/_form.html.erb +255 -0
  78. data/app/views/blazer/queries/docs.html.erb +147 -0
  79. data/app/views/blazer/queries/edit.html.erb +2 -0
  80. data/app/views/blazer/queries/home.html.erb +169 -0
  81. data/app/views/blazer/queries/new.html.erb +2 -0
  82. data/app/views/blazer/queries/run.html.erb +188 -0
  83. data/app/views/blazer/queries/schema.html.erb +55 -0
  84. data/app/views/blazer/queries/show.html.erb +72 -0
  85. data/app/views/blazer/uploads/_form.html.erb +27 -0
  86. data/app/views/blazer/uploads/edit.html.erb +3 -0
  87. data/app/views/blazer/uploads/index.html.erb +55 -0
  88. data/app/views/blazer/uploads/new.html.erb +3 -0
  89. data/app/views/layouts/blazer/application.html.erb +25 -0
  90. data/config/routes.rb +25 -0
  91. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  92. data/lib/blazer/adapters/base_adapter.rb +76 -0
  93. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  94. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  95. data/lib/blazer/adapters/clickhouse_adapter.rb +84 -0
  96. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  97. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  98. data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
  99. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  100. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  101. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  102. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  103. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  104. data/lib/blazer/adapters/presto_adapter.rb +54 -0
  105. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  106. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  107. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  108. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  109. data/lib/blazer/adapters/sql_adapter.rb +324 -0
  110. data/lib/blazer/adapters.rb +18 -0
  111. data/lib/blazer/annotations.rb +47 -0
  112. data/lib/blazer/anomaly_detectors.rb +22 -0
  113. data/lib/blazer/check_mailer.rb +27 -0
  114. data/lib/blazer/data_source.rb +270 -0
  115. data/lib/blazer/engine.rb +42 -0
  116. data/lib/blazer/forecasters.rb +7 -0
  117. data/lib/blazer/result.rb +178 -0
  118. data/lib/blazer/result_cache.rb +71 -0
  119. data/lib/blazer/run_statement.rb +44 -0
  120. data/lib/blazer/run_statement_job.rb +20 -0
  121. data/lib/blazer/slack_notifier.rb +94 -0
  122. data/lib/blazer/statement.rb +77 -0
  123. data/lib/blazer/version.rb +3 -0
  124. data/lib/blazer.rb +286 -0
  125. data/lib/finery.rb +3 -0
  126. data/lib/generators/blazer/install_generator.rb +22 -0
  127. data/lib/generators/blazer/templates/config.yml.tt +83 -0
  128. data/lib/generators/blazer/templates/install.rb.tt +47 -0
  129. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  130. data/lib/generators/blazer/uploads_generator.rb +18 -0
  131. data/lib/tasks/blazer.rake +20 -0
  132. data/lib/tasks/finery.rake +20 -0
  133. data/licenses/LICENSE-ace.txt +24 -0
  134. data/licenses/LICENSE-bootstrap.txt +21 -0
  135. data/licenses/LICENSE-chart.js.txt +9 -0
  136. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  137. data/licenses/LICENSE-chartkick.js.txt +22 -0
  138. data/licenses/LICENSE-date-fns.txt +21 -0
  139. data/licenses/LICENSE-daterangepicker.txt +21 -0
  140. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  141. data/licenses/LICENSE-highlight.js.txt +29 -0
  142. data/licenses/LICENSE-jquery.txt +20 -0
  143. data/licenses/LICENSE-kurkle-color.txt +9 -0
  144. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  145. data/licenses/LICENSE-moment-timezone.txt +20 -0
  146. data/licenses/LICENSE-moment.txt +22 -0
  147. data/licenses/LICENSE-rails-ujs.txt +20 -0
  148. data/licenses/LICENSE-selectize.txt +202 -0
  149. data/licenses/LICENSE-sortable.txt +21 -0
  150. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  151. data/licenses/LICENSE-stupidtable.txt +19 -0
  152. data/licenses/LICENSE-vue.txt +21 -0
  153. metadata +250 -0
@@ -0,0 +1,94 @@
1
+ require "net/http"
2
+
3
+ module Blazer
4
+ class SlackNotifier
5
+ def self.state_change(check, state, state_was, rows_count, error, check_type)
6
+ check.split_slack_channels.each do |channel|
7
+ text =
8
+ if error
9
+ error
10
+ elsif rows_count > 0 && check_type == "bad_data"
11
+ pluralize(rows_count, "row")
12
+ end
13
+
14
+ payload = {
15
+ channel: channel,
16
+ attachments: [
17
+ {
18
+ title: escape("Check #{state.titleize}: #{check.query.name}"),
19
+ title_link: query_url(check.query_id),
20
+ text: escape(text),
21
+ color: state == "passing" ? "good" : "danger"
22
+ }
23
+ ]
24
+ }
25
+
26
+ post(payload)
27
+ end
28
+ end
29
+
30
+ def self.failing_checks(channel, checks)
31
+ text =
32
+ checks.map do |check|
33
+ "<#{query_url(check.query_id)}|#{escape(check.query.name)}> #{escape(check.state)}"
34
+ end
35
+
36
+ payload = {
37
+ channel: channel,
38
+ attachments: [
39
+ {
40
+ title: escape("#{pluralize(checks.size, "Check")} Failing"),
41
+ text: text.join("\n"),
42
+ color: "warning"
43
+ }
44
+ ]
45
+ }
46
+
47
+ post(payload)
48
+ end
49
+
50
+ # https://api.slack.com/docs/message-formatting#how_to_escape_characters
51
+ # - Replace the ampersand, &, with &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
+ # checks shouldn't have variables, but in any case,
64
+ # avoid passing variable params to url helpers
65
+ # (known unsafe parameters are removed, but still not ideal)
66
+ def self.query_url(id)
67
+ Blazer::Engine.routes.url_helpers.query_url(id, ActionMailer::Base.default_url_options)
68
+ end
69
+
70
+ # TODO use return value
71
+ def self.post(payload)
72
+ if Blazer.slack_webhook_url.present?
73
+ response = post_api(Blazer.slack_webhook_url, payload, {})
74
+ response.is_a?(Net::HTTPSuccess) && response.body == "ok"
75
+ else
76
+ headers = {
77
+ "Authorization" => "Bearer #{Blazer.slack_oauth_token}",
78
+ "Content-type" => "application/json"
79
+ }
80
+ response = post_api("https://slack.com/api/chat.postMessage", payload, headers)
81
+ response.is_a?(Net::HTTPSuccess) && (JSON.parse(response.body)["ok"] rescue false)
82
+ end
83
+ end
84
+
85
+ def self.post_api(url, payload, headers)
86
+ uri = URI.parse(url)
87
+ http = Net::HTTP.new(uri.host, uri.port)
88
+ http.use_ssl = true
89
+ http.open_timeout = 3
90
+ http.read_timeout = 5
91
+ http.post(uri.request_uri, payload.to_json, headers)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,77 @@
1
+ module Blazer
2
+ class Statement
3
+ attr_reader :statement, :data_source, :bind_statement, :bind_values
4
+ attr_accessor :values
5
+
6
+ def initialize(statement, data_source = nil)
7
+ @statement = statement
8
+ @data_source = data_source.is_a?(String) ? Blazer.data_sources[data_source] : data_source
9
+ @values = {}
10
+ end
11
+
12
+ def variables
13
+ # strip commented out lines
14
+ # and regex {1} or {1,2}
15
+ @variables ||= statement.to_s.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
16
+ end
17
+
18
+ def add_values(var_params)
19
+ variables.each do |var|
20
+ value = var_params[var].presence
21
+ value = nil unless value.is_a?(String) # ignore arrays and hashes
22
+ if value
23
+ if ["start_time", "end_time"].include?(var)
24
+ value = value.to_s.gsub(" ", "+") # fix for Quip bug
25
+ end
26
+
27
+ if var.end_with?("_at")
28
+ begin
29
+ value = Blazer.time_zone.parse(value)
30
+ rescue
31
+ # do nothing
32
+ end
33
+ end
34
+
35
+ unless value.is_a?(ActiveSupport::TimeWithZone)
36
+ if value =~ /\A\d+\z/
37
+ value = value.to_i
38
+ elsif value =~ /\A\d+\.\d+\z/
39
+ value = value.to_f
40
+ end
41
+ end
42
+ end
43
+ value = Blazer.transform_variable.call(var, value) if Blazer.transform_variable
44
+ @values[var] = value
45
+ end
46
+ end
47
+
48
+ def cohort_analysis?
49
+ /\/\*\s*cohort analysis\s*\*\//i.match?(statement)
50
+ end
51
+
52
+ def apply_cohort_analysis(period:, days:)
53
+ @statement = data_source.cohort_analysis_statement(statement, period: period, days: days).sub("{placeholder}") { statement }
54
+ end
55
+
56
+ # should probably transform before cohort analysis
57
+ # but keep previous order for now
58
+ def transformed_statement
59
+ statement = self.statement.dup
60
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
61
+ statement
62
+ end
63
+
64
+ def bind
65
+ @bind_statement, @bind_values = data_source.bind_params(transformed_statement, values)
66
+ end
67
+
68
+ def display_statement
69
+ data_source.sub_variables(transformed_statement, values)
70
+ end
71
+
72
+ def clear_cache
73
+ bind if bind_statement.nil?
74
+ data_source.clear_cache(self)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module Blazer
2
+ VERSION = "3.0.0"
3
+ end
data/lib/blazer.rb ADDED
@@ -0,0 +1,286 @@
1
+ # dependencies
2
+ require "chartkick"
3
+ require "safely/core"
4
+
5
+ # stdlib
6
+ require "csv"
7
+ require "digest/sha2"
8
+ require "json"
9
+ require "yaml"
10
+
11
+ # modules
12
+ require_relative "blazer/version"
13
+ require_relative "blazer/data_source"
14
+ require_relative "blazer/result"
15
+ require_relative "blazer/result_cache"
16
+ require_relative "blazer/run_statement"
17
+ require_relative "blazer/statement"
18
+ require_relative "blazer/annotations"
19
+
20
+ # adapters
21
+ require_relative "blazer/adapters/base_adapter"
22
+ require_relative "blazer/adapters/athena_adapter"
23
+ require_relative "blazer/adapters/bigquery_adapter"
24
+ require_relative "blazer/adapters/cassandra_adapter"
25
+ require_relative "blazer/adapters/clickhouse_adapter"
26
+ require_relative "blazer/adapters/drill_adapter"
27
+ require_relative "blazer/adapters/druid_adapter"
28
+ require_relative "blazer/adapters/elasticsearch_adapter"
29
+ require_relative "blazer/adapters/hive_adapter"
30
+ require_relative "blazer/adapters/ignite_adapter"
31
+ require_relative "blazer/adapters/influxdb_adapter"
32
+ require_relative "blazer/adapters/neo4j_adapter"
33
+ require_relative "blazer/adapters/opensearch_adapter"
34
+ require_relative "blazer/adapters/presto_adapter"
35
+ require_relative "blazer/adapters/salesforce_adapter"
36
+ require_relative "blazer/adapters/soda_adapter"
37
+ require_relative "blazer/adapters/spark_adapter"
38
+ require_relative "blazer/adapters/sql_adapter"
39
+ require_relative "blazer/adapters/snowflake_adapter"
40
+
41
+ # engine
42
+ require_relative "blazer/engine"
43
+
44
+ module Blazer
45
+ class Error < StandardError; end
46
+ class UploadError < Error; end
47
+ class TimeoutNotSupported < Error; end
48
+
49
+ # actionmailer optional
50
+ autoload :CheckMailer, "blazer/check_mailer"
51
+ # net/http optional
52
+ autoload :SlackNotifier, "blazer/slack_notifier"
53
+ # activejob optional
54
+ autoload :RunStatementJob, "blazer/run_statement_job"
55
+
56
+ class << self
57
+ attr_accessor :audit
58
+ attr_reader :time_zone
59
+ attr_accessor :user_name
60
+ attr_writer :user_class
61
+ attr_writer :user_method
62
+ attr_accessor :before_action
63
+ attr_accessor :from_email
64
+ attr_accessor :cache
65
+ attr_accessor :transform_statement
66
+ attr_accessor :transform_variable
67
+ attr_accessor :check_schedules
68
+ attr_accessor :anomaly_checks
69
+ attr_accessor :forecasting
70
+ attr_accessor :async
71
+ attr_accessor :images
72
+ attr_accessor :annotations
73
+ attr_accessor :override_csp
74
+ attr_accessor :slack_oauth_token
75
+ attr_accessor :slack_webhook_url
76
+ attr_accessor :mapbox_access_token
77
+ end
78
+ self.audit = true
79
+ self.user_name = :name
80
+ self.check_schedules = ["5 minutes", "1 hour", "1 day"]
81
+ self.anomaly_checks = false
82
+ self.forecasting = false
83
+ self.async = false
84
+ self.images = false
85
+ self.override_csp = false
86
+ self.annotations = Blazer::Annotations
87
+
88
+ VARIABLE_MESSAGE = "Variable cannot be used in this position"
89
+ TIMEOUT_MESSAGE = "Query timed out :("
90
+ TIMEOUT_ERRORS = [
91
+ "canceling statement due to statement timeout", # postgres
92
+ "canceling statement due to conflict with recovery", # postgres
93
+ "cancelled on user's request", # redshift
94
+ "canceled on user's request", # redshift
95
+ "system requested abort", # redshift
96
+ "maximum statement execution time exceeded" # mysql
97
+ ]
98
+
99
+ def self.time_zone=(time_zone)
100
+ @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
101
+ end
102
+
103
+ def self.user_class
104
+ if !defined?(@user_class)
105
+ @user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
106
+ end
107
+ @user_class
108
+ end
109
+
110
+ def self.user_method
111
+ if !defined?(@user_method)
112
+ @user_method = settings["user_method"]
113
+ if user_class
114
+ @user_method ||= "current_#{user_class.to_s.downcase.singularize}"
115
+ end
116
+ end
117
+ @user_method
118
+ end
119
+
120
+ def self.settings
121
+ @settings ||= begin
122
+ path = Rails.root.join("config", "blazer.yml").to_s
123
+ if File.exist?(path)
124
+ YAML.safe_load(ERB.new(File.read(path)).result, aliases: true)
125
+ else
126
+ {}
127
+ end
128
+ end
129
+ end
130
+
131
+ def self.data_sources
132
+ @data_sources ||= begin
133
+ ds = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
134
+ settings["data_sources"].each do |id, s|
135
+ ds[id] = Blazer::DataSource.new(id, s)
136
+ end
137
+ ds
138
+ end
139
+ end
140
+
141
+ def self.run_checks(schedule: nil)
142
+ checks = Blazer::Check.includes(:query)
143
+ checks = checks.where(schedule: schedule) if schedule
144
+ checks.find_each do |check|
145
+ next if check.state == "disabled"
146
+ Safely.safely { run_check(check) }
147
+ end
148
+ end
149
+
150
+ def self.run_check(check)
151
+ tries = 1
152
+
153
+ ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
154
+ # try 3 times on timeout errors
155
+ statement = check.query.statement_object
156
+ data_source = statement.data_source
157
+
158
+ while tries <= 3
159
+ result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
160
+ if result.timed_out?
161
+ Rails.logger.info "[blazer timeout] query=#{check.query.name}"
162
+ tries += 1
163
+ sleep(10)
164
+ elsif result.error.to_s.start_with?("PG::ConnectionBad")
165
+ data_source.reconnect
166
+ Rails.logger.info "[blazer reconnect] query=#{check.query.name}"
167
+ tries += 1
168
+ sleep(10)
169
+ else
170
+ break
171
+ end
172
+ end
173
+
174
+ begin
175
+ check.reload # in case state has changed since job started
176
+ check.update_state(result)
177
+ rescue ActiveRecord::RecordNotFound
178
+ # check deleted
179
+ end
180
+
181
+ # TODO use proper logfmt
182
+ Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
183
+
184
+ # should be no variables
185
+ instrument[:statement] = statement.bind_statement
186
+ instrument[:data_source] = data_source
187
+ instrument[:state] = check.state
188
+ instrument[:rows] = result.rows.try(:size)
189
+ instrument[:error] = result.error
190
+ instrument[:tries] = tries
191
+ end
192
+ end
193
+
194
+ def self.send_failing_checks
195
+ emails = {}
196
+ slack_channels = {}
197
+
198
+ Blazer::Check.includes(:query).where(state: ["failing", "error", "timed out", "disabled"]).find_each do |check|
199
+ check.split_emails.each do |email|
200
+ (emails[email] ||= []) << check
201
+ end
202
+ check.split_slack_channels.each do |channel|
203
+ (slack_channels[channel] ||= []) << check
204
+ end
205
+ end
206
+
207
+ emails.each do |email, checks|
208
+ Safely.safely do
209
+ Blazer::CheckMailer.failing_checks(email, checks).deliver_now
210
+ end
211
+ end
212
+
213
+ slack_channels.each do |channel, checks|
214
+ Safely.safely do
215
+ Blazer::SlackNotifier.failing_checks(channel, checks)
216
+ end
217
+ end
218
+ end
219
+
220
+ def self.slack?
221
+ slack_oauth_token.present? || slack_webhook_url.present?
222
+ end
223
+
224
+ # TODO show warning on invalid access token
225
+ def self.maps?
226
+ mapbox_access_token.present? && mapbox_access_token.start_with?("pk.")
227
+ end
228
+
229
+ def self.uploads?
230
+ settings.key?("uploads")
231
+ end
232
+
233
+ def self.uploads_connection
234
+ raise "Empty url for uploads" unless settings.dig("uploads", "url")
235
+ Blazer::UploadsConnection.connection
236
+ end
237
+
238
+ def self.uploads_schema
239
+ settings.dig("uploads", "schema") || "uploads"
240
+ end
241
+
242
+ def self.uploads_table_name(name)
243
+ uploads_connection.quote_table_name("#{uploads_schema}.#{name}")
244
+ end
245
+
246
+ def self.adapters
247
+ @adapters ||= {}
248
+ end
249
+
250
+ def self.register_adapter(name, adapter)
251
+ adapters[name] = adapter
252
+ end
253
+
254
+ def self.anomaly_detectors
255
+ @anomaly_detectors ||= {}
256
+ end
257
+
258
+ def self.register_anomaly_detector(name, &anomaly_detector)
259
+ anomaly_detectors[name] = anomaly_detector
260
+ end
261
+
262
+ def self.forecasters
263
+ @forecasters ||= {}
264
+ end
265
+
266
+ def self.register_forecaster(name, &forecaster)
267
+ forecasters[name] = forecaster
268
+ end
269
+
270
+ def self.archive_queries
271
+ raise "Audits must be enabled to archive" unless Blazer.audit
272
+ raise "Missing status column - see https://github.com/ankane/blazer#23" unless Blazer::Query.column_names.include?("status")
273
+
274
+ viewed_query_ids = Blazer::Audit.where("created_at > ?", 90.days.ago).group(:query_id).count.keys.compact
275
+ Blazer::Query.active.where.not(id: viewed_query_ids).update_all(status: "archived")
276
+ end
277
+
278
+ # private
279
+ def self.monotonic_time
280
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
281
+ end
282
+ end
283
+
284
+ require_relative "blazer/adapters"
285
+ require_relative "blazer/anomaly_detectors"
286
+ require_relative "blazer/forecasters"
data/lib/finery.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "blazer"
2
+
3
+ Finery = Blazer
@@ -0,0 +1,22 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Blazer
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_migration
10
+ migration_template "install.rb", "db/migrate/install_blazer.rb", migration_version: migration_version
11
+ end
12
+
13
+ def copy_config
14
+ template "config.yml", "config/finery.yml"
15
+ end
16
+
17
+ def migration_version
18
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,83 @@
1
+ # see https://github.com/finery-bi/finery for more info
2
+
3
+ data_sources:
4
+ main:
5
+ url: <%%= ENV["BLAZER_DATABASE_URL"] %>
6
+
7
+ # statement timeout, in seconds
8
+ # none by default
9
+ # timeout: 15
10
+
11
+ # caching settings
12
+ # can greatly improve speed
13
+ # off by default
14
+ # cache:
15
+ # mode: slow # or all
16
+ # expires_in: 60 # min
17
+ # slow_threshold: 15 # sec, only used in slow mode
18
+
19
+ # wrap queries in a transaction for safety
20
+ # not necessary if you use a read-only user
21
+ # true by default
22
+ # use_transaction: false
23
+
24
+ smart_variables:
25
+ # zone_id: "SELECT id, name FROM zones ORDER BY name ASC"
26
+ # period: ["day", "week", "month"]
27
+ # status: {0: "Active", 1: "Archived"}
28
+
29
+ linked_columns:
30
+ # user_id: "/admin/users/{value}"
31
+
32
+ smart_columns:
33
+ # user_id: "SELECT id, name FROM users WHERE id IN {value}"
34
+
35
+ annotations:
36
+ # holiday_boxes: SELECT min_date, max_date, label FROM holidays WHERE (min_date, max_date) OVERLAPS ({min_date}, {max_date})
37
+ # deployment_lines: SELECT date, label FROM deployments WHERE date BETWEEN {min_date} and {max_date}
38
+
39
+ # create audits
40
+ audit: true
41
+
42
+ # change the time zone
43
+ # time_zone: "Pacific Time (US & Canada)"
44
+
45
+ # class name of the user model
46
+ # user_class: User
47
+
48
+ # method name for the current user
49
+ # user_method: current_user
50
+
51
+ # method name for the display name
52
+ # user_name: name
53
+
54
+ # custom before_action to use for auth
55
+ # before_action_method: require_admin
56
+
57
+ # email to send checks from
58
+ # from_email: finery@example.org
59
+
60
+ # webhook for Slack
61
+ # slack_webhook_url: <%%= ENV["BLAZER_SLACK_WEBHOOK_URL"] %>
62
+
63
+ check_schedules:
64
+ - "1 day"
65
+ - "1 hour"
66
+ - "5 minutes"
67
+
68
+ # enable anomaly detection
69
+ # note: with trend, time series are sent to https://trendapi.org
70
+ # anomaly_checks: prophet / trend / anomaly_detection
71
+
72
+ # enable forecasting
73
+ # note: with trend, time series are sent to https://trendapi.org
74
+ # forecasting: prophet / trend
75
+
76
+ # enable map
77
+ # mapbox_access_token: <%%= ENV["MAPBOX_ACCESS_TOKEN"] %>
78
+
79
+ # enable uploads
80
+ # uploads:
81
+ # url: <%%= ENV["BLAZER_UPLOADS_URL"] %>
82
+ # schema: uploads
83
+ # data_source: main
@@ -0,0 +1,47 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :blazer_queries do |t|
4
+ t.references :creator
5
+ t.string :name
6
+ t.text :description
7
+ t.text :statement
8
+ t.string :data_source
9
+ t.string :status
10
+ t.timestamps null: false
11
+ end
12
+
13
+ create_table :blazer_audits do |t|
14
+ t.references :user
15
+ t.references :query
16
+ t.text :statement
17
+ t.string :data_source
18
+ t.datetime :created_at
19
+ end
20
+
21
+ create_table :blazer_dashboards do |t|
22
+ t.references :creator
23
+ t.string :name
24
+ t.timestamps null: false
25
+ end
26
+
27
+ create_table :blazer_dashboard_queries do |t|
28
+ t.references :dashboard
29
+ t.references :query
30
+ t.integer :position
31
+ t.timestamps null: false
32
+ end
33
+
34
+ create_table :blazer_checks do |t|
35
+ t.references :creator
36
+ t.references :query
37
+ t.string :state
38
+ t.string :schedule
39
+ t.text :emails
40
+ t.text :slack_channels
41
+ t.string :check_type
42
+ t.text :message
43
+ t.datetime :last_run_at
44
+ t.timestamps null: false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :blazer_uploads do |t|
4
+ t.references :creator
5
+ t.string :table
6
+ t.text :description
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Blazer
4
+ module Generators
5
+ class UploadsGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_migration
10
+ migration_template "uploads.rb", "db/migrate/create_blazer_uploads.rb", migration_version: migration_version
11
+ end
12
+
13
+ def migration_version
14
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ namespace :blazer do
2
+ desc "run checks"
3
+ task :run_checks, [:schedule] => :environment do |_, args|
4
+ Blazer.run_checks(schedule: args[:schedule] || ENV["SCHEDULE"])
5
+ end
6
+
7
+ desc "send failing checks"
8
+ task send_failing_checks: :environment do
9
+ Blazer.send_failing_checks
10
+ end
11
+
12
+ desc "archive queries"
13
+ task archive_queries: :environment do
14
+ begin
15
+ Blazer.archive_queries
16
+ rescue => e
17
+ abort e.message
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ namespace :finery do
2
+ desc "run checks"
3
+ task :run_checks, [:schedule] => :environment do |_, args|
4
+ Finery.run_checks(schedule: args[:schedule] || ENV["SCHEDULE"])
5
+ end
6
+
7
+ desc "send failing checks"
8
+ task send_failing_checks: :environment do
9
+ Finery.send_failing_checks
10
+ end
11
+
12
+ desc "archive queries"
13
+ task archive_queries: :environment do
14
+ begin
15
+ Finery.archive_queries
16
+ rescue => e
17
+ abort e.message
18
+ end
19
+ end
20
+ end