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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +426 -0
- data/CONTRIBUTING.md +49 -0
- data/LICENSE.txt +25 -0
- data/README.md +1144 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
- data/app/assets/images/blazer/favicon.png +0 -0
- data/app/assets/javascripts/blazer/Sortable.js +3709 -0
- data/app/assets/javascripts/blazer/ace/ace.js +19630 -0
- data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1981 -0
- data/app/assets/javascripts/blazer/ace/mode-sql.js +215 -0
- data/app/assets/javascripts/blazer/ace/snippets/sql.js +16 -0
- data/app/assets/javascripts/blazer/ace/snippets/text.js +9 -0
- data/app/assets/javascripts/blazer/ace/theme-twilight.js +18 -0
- data/app/assets/javascripts/blazer/ace.js +6 -0
- data/app/assets/javascripts/blazer/application.js +87 -0
- data/app/assets/javascripts/blazer/bootstrap.js +2580 -0
- data/app/assets/javascripts/blazer/chart.umd.js +13 -0
- data/app/assets/javascripts/blazer/chartjs-adapter-date-fns.bundle.js +6322 -0
- data/app/assets/javascripts/blazer/chartjs-plugin-annotation.min.js +7 -0
- data/app/assets/javascripts/blazer/chartkick.js +2570 -0
- data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
- data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
- data/app/assets/javascripts/blazer/highlight.min.js +466 -0
- data/app/assets/javascripts/blazer/jquery.js +10872 -0
- data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
- data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
- data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
- data/app/assets/javascripts/blazer/moment.js +5685 -0
- data/app/assets/javascripts/blazer/queries.js +130 -0
- data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
- data/app/assets/javascripts/blazer/routes.js +26 -0
- data/app/assets/javascripts/blazer/selectize.js +3891 -0
- data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
- data/app/assets/javascripts/blazer/stupidtable.js +281 -0
- data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
- data/app/assets/stylesheets/blazer/application.css +243 -0
- data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
- data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
- data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
- data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
- data/app/assets/stylesheets/blazer/github.css +125 -0
- data/app/assets/stylesheets/blazer/selectize.css +403 -0
- data/app/controllers/blazer/base_controller.rb +133 -0
- data/app/controllers/blazer/checks_controller.rb +56 -0
- data/app/controllers/blazer/dashboards_controller.rb +99 -0
- data/app/controllers/blazer/queries_controller.rb +468 -0
- data/app/controllers/blazer/uploads_controller.rb +147 -0
- data/app/helpers/blazer/base_helper.rb +83 -0
- data/app/models/blazer/audit.rb +6 -0
- data/app/models/blazer/check.rb +104 -0
- data/app/models/blazer/connection.rb +5 -0
- data/app/models/blazer/dashboard.rb +17 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +42 -0
- data/app/models/blazer/record.rb +5 -0
- data/app/models/blazer/upload.rb +11 -0
- data/app/models/blazer/uploads_connection.rb +7 -0
- data/app/views/blazer/_nav.html.erb +18 -0
- data/app/views/blazer/_variables.html.erb +127 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
- data/app/views/blazer/checks/_form.html.erb +79 -0
- data/app/views/blazer/checks/edit.html.erb +3 -0
- data/app/views/blazer/checks/index.html.erb +72 -0
- data/app/views/blazer/checks/new.html.erb +3 -0
- data/app/views/blazer/dashboards/_form.html.erb +82 -0
- data/app/views/blazer/dashboards/edit.html.erb +3 -0
- data/app/views/blazer/dashboards/new.html.erb +3 -0
- data/app/views/blazer/dashboards/show.html.erb +53 -0
- data/app/views/blazer/queries/_caching.html.erb +16 -0
- data/app/views/blazer/queries/_cohorts.html.erb +48 -0
- data/app/views/blazer/queries/_form.html.erb +255 -0
- data/app/views/blazer/queries/docs.html.erb +147 -0
- data/app/views/blazer/queries/edit.html.erb +2 -0
- data/app/views/blazer/queries/home.html.erb +169 -0
- data/app/views/blazer/queries/new.html.erb +2 -0
- data/app/views/blazer/queries/run.html.erb +188 -0
- data/app/views/blazer/queries/schema.html.erb +55 -0
- data/app/views/blazer/queries/show.html.erb +72 -0
- data/app/views/blazer/uploads/_form.html.erb +27 -0
- data/app/views/blazer/uploads/edit.html.erb +3 -0
- data/app/views/blazer/uploads/index.html.erb +55 -0
- data/app/views/blazer/uploads/new.html.erb +3 -0
- data/app/views/layouts/blazer/application.html.erb +25 -0
- data/config/routes.rb +25 -0
- data/lib/blazer/adapters/athena_adapter.rb +182 -0
- data/lib/blazer/adapters/base_adapter.rb +76 -0
- data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
- data/lib/blazer/adapters/clickhouse_adapter.rb +84 -0
- data/lib/blazer/adapters/drill_adapter.rb +38 -0
- data/lib/blazer/adapters/druid_adapter.rb +102 -0
- data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
- data/lib/blazer/adapters/hive_adapter.rb +55 -0
- data/lib/blazer/adapters/ignite_adapter.rb +64 -0
- data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
- data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
- data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
- data/lib/blazer/adapters/presto_adapter.rb +54 -0
- data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
- data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
- data/lib/blazer/adapters/soda_adapter.rb +105 -0
- data/lib/blazer/adapters/spark_adapter.rb +14 -0
- data/lib/blazer/adapters/sql_adapter.rb +324 -0
- data/lib/blazer/adapters.rb +18 -0
- data/lib/blazer/annotations.rb +47 -0
- data/lib/blazer/anomaly_detectors.rb +22 -0
- data/lib/blazer/check_mailer.rb +27 -0
- data/lib/blazer/data_source.rb +270 -0
- data/lib/blazer/engine.rb +42 -0
- data/lib/blazer/forecasters.rb +7 -0
- data/lib/blazer/result.rb +178 -0
- data/lib/blazer/result_cache.rb +71 -0
- data/lib/blazer/run_statement.rb +44 -0
- data/lib/blazer/run_statement_job.rb +20 -0
- data/lib/blazer/slack_notifier.rb +94 -0
- data/lib/blazer/statement.rb +77 -0
- data/lib/blazer/version.rb +3 -0
- data/lib/blazer.rb +286 -0
- data/lib/finery.rb +3 -0
- data/lib/generators/blazer/install_generator.rb +22 -0
- data/lib/generators/blazer/templates/config.yml.tt +83 -0
- data/lib/generators/blazer/templates/install.rb.tt +47 -0
- data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
- data/lib/generators/blazer/uploads_generator.rb +18 -0
- data/lib/tasks/blazer.rake +20 -0
- data/lib/tasks/finery.rake +20 -0
- data/licenses/LICENSE-ace.txt +24 -0
- data/licenses/LICENSE-bootstrap.txt +21 -0
- data/licenses/LICENSE-chart.js.txt +9 -0
- data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
- data/licenses/LICENSE-chartkick.js.txt +22 -0
- data/licenses/LICENSE-date-fns.txt +21 -0
- data/licenses/LICENSE-daterangepicker.txt +21 -0
- data/licenses/LICENSE-fuzzysearch.txt +20 -0
- data/licenses/LICENSE-highlight.js.txt +29 -0
- data/licenses/LICENSE-jquery.txt +20 -0
- data/licenses/LICENSE-kurkle-color.txt +9 -0
- data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
- data/licenses/LICENSE-moment-timezone.txt +20 -0
- data/licenses/LICENSE-moment.txt +22 -0
- data/licenses/LICENSE-rails-ujs.txt +20 -0
- data/licenses/LICENSE-selectize.txt +202 -0
- data/licenses/LICENSE-sortable.txt +21 -0
- data/licenses/LICENSE-stickytableheaders.txt +20 -0
- data/licenses/LICENSE-stupidtable.txt +19 -0
- data/licenses/LICENSE-vue.txt +21 -0
- 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 &
|
|
52
|
+
# - Replace the less-than sign, < with <
|
|
53
|
+
# - Replace the greater-than sign, > with >
|
|
54
|
+
# That's it. Don't HTML entity-encode the entire message.
|
|
55
|
+
def self.escape(str)
|
|
56
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">") 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
|
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,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,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
|