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,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