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,324 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
module Adapters
|
|
3
|
+
class SqlAdapter < BaseAdapter
|
|
4
|
+
attr_reader :connection_model
|
|
5
|
+
|
|
6
|
+
def initialize(data_source)
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
@connection_model =
|
|
10
|
+
Class.new(Blazer::Connection) do
|
|
11
|
+
def self.name
|
|
12
|
+
"Blazer::Connection::Adapter#{object_id}"
|
|
13
|
+
end
|
|
14
|
+
establish_connection(data_source.settings["url"]) if data_source.settings["url"]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run_statement(statement, comment, bind_params = [])
|
|
19
|
+
columns = []
|
|
20
|
+
rows = []
|
|
21
|
+
error = nil
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
in_transaction do
|
|
25
|
+
set_timeout(data_source.timeout) if data_source.timeout
|
|
26
|
+
|
|
27
|
+
binds = bind_params.map { |v| ActiveRecord::Relation::QueryAttribute.new(nil, v, ActiveRecord::Type::Value.new) }
|
|
28
|
+
result = connection_model.connection.select_all("#{statement} /*#{comment}*/", nil, binds)
|
|
29
|
+
columns = result.columns
|
|
30
|
+
result.rows.each do |untyped_row|
|
|
31
|
+
rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] && result.column_types[c] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
rescue => e
|
|
35
|
+
error = e.message.sub(/.+ERROR: /, "")
|
|
36
|
+
error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
|
|
37
|
+
error = Blazer::VARIABLE_MESSAGE if error.include?("syntax error at or near \"$") || error.include?("Incorrect syntax near '@") || error.include?("your MySQL server version for the right syntax to use near '?")
|
|
38
|
+
if error.include?("could not determine data type of parameter")
|
|
39
|
+
error += " - try adding casting to variables and make sure none are inside a string literal"
|
|
40
|
+
end
|
|
41
|
+
reconnect if error.include?("PG::ConnectionBad")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
[columns, rows, error]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tables
|
|
48
|
+
sql = add_schemas("SELECT table_schema, table_name FROM information_schema.tables")
|
|
49
|
+
result = data_source.run_statement(sql, refresh_cache: true)
|
|
50
|
+
if postgresql? || redshift? || snowflake?
|
|
51
|
+
result.rows.sort_by { |r| [r[0] == default_schema ? "" : r[0], r[1]] }.map do |row|
|
|
52
|
+
table =
|
|
53
|
+
if row[0] == default_schema
|
|
54
|
+
row[1]
|
|
55
|
+
else
|
|
56
|
+
"#{row[0]}.#{row[1]}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
table = table.downcase if snowflake?
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
table: table,
|
|
63
|
+
value: connection_model.connection.quote_table_name(table)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
result.rows.map(&:second).sort
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def schema
|
|
72
|
+
sql = add_schemas("SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns")
|
|
73
|
+
result = data_source.run_statement(sql)
|
|
74
|
+
result.rows.group_by { |r| [r[0], r[1]] }.map { |k, vs| {schema: k[0], table: k[1], columns: vs.sort_by { |v| v[2] }.map { |v| {name: v[2], data_type: v[3]} }} }.sort_by { |t| [t[:schema] == default_schema ? "" : t[:schema], t[:table]] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def preview_statement
|
|
78
|
+
if sqlserver?
|
|
79
|
+
"SELECT TOP (10) * FROM {table}"
|
|
80
|
+
else
|
|
81
|
+
"SELECT * FROM {table} LIMIT 10"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reconnect
|
|
86
|
+
connection_model.establish_connection(settings["url"])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def cost(statement)
|
|
90
|
+
result = explain(statement)
|
|
91
|
+
if sqlserver?
|
|
92
|
+
result["TotalSubtreeCost"]
|
|
93
|
+
else
|
|
94
|
+
match = /cost=\d+\.\d+..(\d+\.\d+) /.match(result)
|
|
95
|
+
match[1] if match
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def explain(statement)
|
|
100
|
+
if postgresql? || redshift?
|
|
101
|
+
select_all("EXPLAIN #{statement}").rows.first.first
|
|
102
|
+
elsif sqlserver?
|
|
103
|
+
begin
|
|
104
|
+
execute("SET SHOWPLAN_ALL ON")
|
|
105
|
+
result = select_all(statement).each.first
|
|
106
|
+
ensure
|
|
107
|
+
execute("SET SHOWPLAN_ALL OFF")
|
|
108
|
+
end
|
|
109
|
+
result
|
|
110
|
+
end
|
|
111
|
+
rescue
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def cancel(run_id)
|
|
116
|
+
if postgresql?
|
|
117
|
+
select_all("SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND query LIKE ?", ["%,run_id:#{run_id}%"])
|
|
118
|
+
elsif redshift?
|
|
119
|
+
first_row = select_all("SELECT pid FROM stv_recents WHERE status = 'Running' AND query LIKE ?", ["%,run_id:#{run_id}%"]).first
|
|
120
|
+
if first_row
|
|
121
|
+
select_all("CANCEL #{first_row["pid"].to_i}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cachable?(statement)
|
|
127
|
+
!%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def supports_cohort_analysis?
|
|
131
|
+
postgresql? || mysql?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# TODO treat date columns as already in time zone
|
|
135
|
+
def cohort_analysis_statement(statement, period:, days:)
|
|
136
|
+
raise "Cohort analysis not supported" unless supports_cohort_analysis?
|
|
137
|
+
|
|
138
|
+
cohort_column = statement =~ /\bcohort_time\b/ ? "cohort_time" : "conversion_time"
|
|
139
|
+
tzname = Blazer.time_zone.tzinfo.name
|
|
140
|
+
|
|
141
|
+
if mysql?
|
|
142
|
+
time_sql = "CONVERT_TZ(cohorts.cohort_time, '+00:00', ?)"
|
|
143
|
+
case period
|
|
144
|
+
when "day"
|
|
145
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-%d') AS DATE)"
|
|
146
|
+
date_params = [tzname]
|
|
147
|
+
when "week"
|
|
148
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql} - INTERVAL ((5 + DAYOFWEEK(#{time_sql})) % 7) DAY, '%Y-%m-%d') AS DATE)"
|
|
149
|
+
date_params = [tzname, tzname]
|
|
150
|
+
else
|
|
151
|
+
date_sql = "CAST(DATE_FORMAT(#{time_sql}, '%Y-%m-01') AS DATE)"
|
|
152
|
+
date_params = [tzname]
|
|
153
|
+
end
|
|
154
|
+
bucket_sql = "CAST(CEIL(TIMESTAMPDIFF(SECOND, cohorts.cohort_time, query.conversion_time) / ?) AS SIGNED)"
|
|
155
|
+
else
|
|
156
|
+
date_sql = "date_trunc(?, cohorts.cohort_time::timestamptz AT TIME ZONE ?)::date"
|
|
157
|
+
date_params = [period, tzname]
|
|
158
|
+
bucket_sql = "CEIL(EXTRACT(EPOCH FROM query.conversion_time - cohorts.cohort_time) / ?)::int"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# WITH not an optimization fence in Postgres 12+
|
|
162
|
+
statement = <<~SQL
|
|
163
|
+
WITH query AS (
|
|
164
|
+
{placeholder}
|
|
165
|
+
),
|
|
166
|
+
cohorts AS (
|
|
167
|
+
SELECT user_id, MIN(#{cohort_column}) AS cohort_time FROM query
|
|
168
|
+
WHERE user_id IS NOT NULL AND #{cohort_column} IS NOT NULL
|
|
169
|
+
GROUP BY 1
|
|
170
|
+
)
|
|
171
|
+
SELECT
|
|
172
|
+
#{date_sql} AS period,
|
|
173
|
+
0 AS bucket,
|
|
174
|
+
COUNT(DISTINCT cohorts.user_id)
|
|
175
|
+
FROM cohorts GROUP BY 1
|
|
176
|
+
UNION ALL
|
|
177
|
+
SELECT
|
|
178
|
+
#{date_sql} AS period,
|
|
179
|
+
#{bucket_sql} AS bucket,
|
|
180
|
+
COUNT(DISTINCT query.user_id)
|
|
181
|
+
FROM cohorts INNER JOIN query ON query.user_id = cohorts.user_id
|
|
182
|
+
WHERE query.conversion_time IS NOT NULL
|
|
183
|
+
AND query.conversion_time >= cohorts.cohort_time
|
|
184
|
+
#{cohort_column == "conversion_time" ? "AND query.conversion_time != cohorts.cohort_time" : ""}
|
|
185
|
+
GROUP BY 1, 2
|
|
186
|
+
SQL
|
|
187
|
+
params = [statement] + date_params + date_params + [days.to_i * 86400]
|
|
188
|
+
connection_model.send(:sanitize_sql_array, params)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def quoting
|
|
192
|
+
->(value) { connection_model.connection.quote(value) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Redshift adapter silently ignores binds
|
|
196
|
+
def parameter_binding
|
|
197
|
+
if postgresql? && (ActiveRecord::VERSION::STRING.to_f >= 6.1 || prepared_statements?)
|
|
198
|
+
# Active Record < 6.1 silently ignores binds with Postgres when prepared statements are disabled
|
|
199
|
+
:numeric
|
|
200
|
+
elsif sqlite?
|
|
201
|
+
:numeric
|
|
202
|
+
elsif mysql? && prepared_statements?
|
|
203
|
+
# Active Record silently ignores binds with MySQL when prepared statements are disabled
|
|
204
|
+
:positional
|
|
205
|
+
elsif sqlserver?
|
|
206
|
+
proc do |statement, variables|
|
|
207
|
+
variables.each_with_index do |(k, _), i|
|
|
208
|
+
statement = statement.gsub("{#{k}}", "@#{i} ")
|
|
209
|
+
end
|
|
210
|
+
[statement, variables.values]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
protected
|
|
216
|
+
|
|
217
|
+
def select_all(statement, params = [])
|
|
218
|
+
statement = connection_model.send(:sanitize_sql_array, [statement] + params) if params.any?
|
|
219
|
+
connection_model.connection.select_all(statement)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# seperate from select_all to prevent mysql error
|
|
223
|
+
def execute(statement)
|
|
224
|
+
connection_model.connection.execute(statement)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def postgresql?
|
|
228
|
+
["PostgreSQL", "PostGIS"].include?(adapter_name)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def redshift?
|
|
232
|
+
["Redshift"].include?(adapter_name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def mysql?
|
|
236
|
+
["MySQL", "Mysql2", "Mysql2Spatial", "Trilogy"].include?(adapter_name)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def sqlite?
|
|
240
|
+
["SQLite"].include?(adapter_name)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def sqlserver?
|
|
244
|
+
["SQLServer", "tinytds", "mssql"].include?(adapter_name)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def snowflake?
|
|
248
|
+
data_source.adapter == "snowflake"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def adapter_name
|
|
252
|
+
# prevent bad data source from taking down queries/new
|
|
253
|
+
connection_model.connection.adapter_name rescue nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def default_schema
|
|
257
|
+
@default_schema ||= begin
|
|
258
|
+
if postgresql? || redshift?
|
|
259
|
+
"public"
|
|
260
|
+
elsif sqlserver?
|
|
261
|
+
"dbo"
|
|
262
|
+
elsif connection_model.respond_to?(:connection_db_config)
|
|
263
|
+
connection_model.connection_db_config.database
|
|
264
|
+
else
|
|
265
|
+
connection_model.connection_config[:database]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def add_schemas(query)
|
|
271
|
+
if settings["schemas"]
|
|
272
|
+
where = "table_schema IN (?)"
|
|
273
|
+
schemas = settings["schemas"]
|
|
274
|
+
elsif mysql?
|
|
275
|
+
where = "table_schema IN (?)"
|
|
276
|
+
schemas = [default_schema]
|
|
277
|
+
else
|
|
278
|
+
where = "table_schema NOT IN (?)"
|
|
279
|
+
schemas = ["information_schema"]
|
|
280
|
+
schemas.map!(&:upcase) if snowflake?
|
|
281
|
+
schemas << "pg_catalog" if postgresql? || redshift?
|
|
282
|
+
end
|
|
283
|
+
connection_model.send(:sanitize_sql_array, ["#{query} WHERE #{where}", schemas])
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def set_timeout(timeout)
|
|
287
|
+
if postgresql? || redshift?
|
|
288
|
+
execute("SET #{use_transaction? ? "LOCAL " : ""}statement_timeout = #{timeout.to_i * 1000}")
|
|
289
|
+
elsif mysql?
|
|
290
|
+
# use send as this method is private in Rails 4.2
|
|
291
|
+
mariadb = connection_model.connection.send(:mariadb?) rescue false
|
|
292
|
+
if mariadb
|
|
293
|
+
execute("SET max_statement_time = #{timeout.to_i * 1000}")
|
|
294
|
+
else
|
|
295
|
+
execute("SET max_execution_time = #{timeout.to_i * 1000}")
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
raise Blazer::TimeoutNotSupported, "Timeout not supported for #{adapter_name} adapter"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def use_transaction?
|
|
303
|
+
settings.key?("use_transaction") ? settings["use_transaction"] : true
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def in_transaction
|
|
307
|
+
connection_model.connection_pool.with_connection do
|
|
308
|
+
if use_transaction?
|
|
309
|
+
connection_model.transaction do
|
|
310
|
+
yield
|
|
311
|
+
raise ActiveRecord::Rollback
|
|
312
|
+
end
|
|
313
|
+
else
|
|
314
|
+
yield
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def prepared_statements?
|
|
320
|
+
connection_model.connection.prepared_statements
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
|
|
2
|
+
Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
|
|
3
|
+
Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
|
|
4
|
+
Blazer.register_adapter "clickhouse", Blazer::Adapters::ClickhouseAdapter
|
|
5
|
+
Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
|
|
6
|
+
Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
|
|
7
|
+
Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
|
|
8
|
+
Blazer.register_adapter "hive", Blazer::Adapters::HiveAdapter
|
|
9
|
+
Blazer.register_adapter "ignite", Blazer::Adapters::IgniteAdapter
|
|
10
|
+
Blazer.register_adapter "influxdb", Blazer::Adapters::InfluxdbAdapter
|
|
11
|
+
Blazer.register_adapter "neo4j", Blazer::Adapters::Neo4jAdapter
|
|
12
|
+
Blazer.register_adapter "opensearch", Blazer::Adapters::OpensearchAdapter
|
|
13
|
+
Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
|
|
14
|
+
Blazer.register_adapter "salesforce", Blazer::Adapters::SalesforceAdapter
|
|
15
|
+
Blazer.register_adapter "soda", Blazer::Adapters::SodaAdapter
|
|
16
|
+
Blazer.register_adapter "spark", Blazer::Adapters::SparkAdapter
|
|
17
|
+
Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
|
|
18
|
+
Blazer.register_adapter "snowflake", Blazer::Adapters::SnowflakeAdapter
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
class Annotations
|
|
3
|
+
attr_reader :annotations
|
|
4
|
+
|
|
5
|
+
def initialize(annotations)
|
|
6
|
+
@annotations = annotations.values
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(result)
|
|
10
|
+
return [] unless result.chart_type.in?(["line", "line2"])
|
|
11
|
+
min, max = result.rows.map(&:first).minmax
|
|
12
|
+
annotations.map { |annotation| fetch_annotation(annotation, result, min, max) }.flatten
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def fetch_annotation(annotation, result, min_date, max_date)
|
|
18
|
+
query = build_query(annotation, max_date, min_date)
|
|
19
|
+
results = result.data_source.run_statement(query)
|
|
20
|
+
return [] unless results.error.nil?
|
|
21
|
+
|
|
22
|
+
if results.columns.size == 3 # boxes
|
|
23
|
+
results.rows.map do |row|
|
|
24
|
+
{
|
|
25
|
+
min_date: row[0],
|
|
26
|
+
max_date: row[1],
|
|
27
|
+
label: row[2],
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
elsif results.columns.size == 2 # lines
|
|
31
|
+
results.rows.map do |row|
|
|
32
|
+
{
|
|
33
|
+
min_date: row[0],
|
|
34
|
+
label: row[1],
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_query(annotation, max_date, min_date)
|
|
43
|
+
annotation = annotation.sub("{min_date}", "(:min_date)").sub("{max_date}", "(:max_date)")
|
|
44
|
+
ActiveRecord::Base.send(:sanitize_sql_array, [annotation, {min_date: min_date, max_date: max_date}])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Blazer.register_anomaly_detector "anomaly_detection" do |series|
|
|
2
|
+
anomalies = AnomalyDetection.detect(series.to_h, period: :auto)
|
|
3
|
+
anomalies.include?(series.last[0])
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
Blazer.register_anomaly_detector "prophet" do |series|
|
|
7
|
+
df = Rover::DataFrame.new(series[0..-2].map { |v| {"ds" => v[0], "y" => v[1]} })
|
|
8
|
+
m = Prophet.new(interval_width: 0.99)
|
|
9
|
+
m.logger.level = ::Logger::FATAL # no logging
|
|
10
|
+
m.fit(df)
|
|
11
|
+
future = Rover::DataFrame.new(series[-1..-1].map { |v| {"ds" => v[0]} })
|
|
12
|
+
forecast = m.predict(future).to_a[0]
|
|
13
|
+
lower = forecast["yhat_lower"]
|
|
14
|
+
upper = forecast["yhat_upper"]
|
|
15
|
+
value = series.last[1]
|
|
16
|
+
value < lower || value > upper
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Blazer.register_anomaly_detector "trend" do |series|
|
|
20
|
+
anomalies = Trend.anomalies(series.to_h)
|
|
21
|
+
anomalies.include?(series.last[0])
|
|
22
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Blazer
|
|
2
|
+
class CheckMailer < ActionMailer::Base
|
|
3
|
+
include ActionView::Helpers::TextHelper
|
|
4
|
+
|
|
5
|
+
default from: Blazer.from_email if Blazer.from_email
|
|
6
|
+
layout false
|
|
7
|
+
|
|
8
|
+
def state_change(check, state, state_was, rows_count, error, columns, rows, column_types, check_type)
|
|
9
|
+
@check = check
|
|
10
|
+
@state = state
|
|
11
|
+
@state_was = state_was
|
|
12
|
+
@rows_count = rows_count
|
|
13
|
+
@error = error
|
|
14
|
+
@columns = columns
|
|
15
|
+
@rows = rows
|
|
16
|
+
@column_types = column_types
|
|
17
|
+
@check_type = check_type
|
|
18
|
+
mail to: check.emails, reply_to: check.emails, subject: "Check #{state.titleize}: #{check.query.name}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failing_checks(email, checks)
|
|
22
|
+
@checks = checks
|
|
23
|
+
# add reply_to for mailing lists
|
|
24
|
+
mail to: email, reply_to: email, subject: "#{pluralize(checks.size, "Check")} Failing"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|