finery 3.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 (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