blazer_xlsx 3.0.5

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 (148) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +442 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +1093 -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 +84 -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/chartkick.js +2570 -0
  25. data/app/assets/javascripts/blazer/daterangepicker.js +1578 -0
  26. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  27. data/app/assets/javascripts/blazer/highlight.min.js +466 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10872 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  30. data/app/assets/javascripts/blazer/mapkick.bundle.js +1029 -0
  31. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1548 -0
  32. data/app/assets/javascripts/blazer/moment.js +5685 -0
  33. data/app/assets/javascripts/blazer/queries.js +130 -0
  34. data/app/assets/javascripts/blazer/rails-ujs.js +746 -0
  35. data/app/assets/javascripts/blazer/routes.js +26 -0
  36. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  37. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  38. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  39. data/app/assets/javascripts/blazer/vue.global.prod.js +1 -0
  40. data/app/assets/stylesheets/blazer/application.css +243 -0
  41. data/app/assets/stylesheets/blazer/bootstrap-propshaft.css +10 -0
  42. data/app/assets/stylesheets/blazer/bootstrap-sprockets.css.erb +10 -0
  43. data/app/assets/stylesheets/blazer/bootstrap.css +6828 -0
  44. data/app/assets/stylesheets/blazer/daterangepicker.css +410 -0
  45. data/app/assets/stylesheets/blazer/github.css +125 -0
  46. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  47. data/app/controllers/blazer/base_controller.rb +135 -0
  48. data/app/controllers/blazer/checks_controller.rb +56 -0
  49. data/app/controllers/blazer/dashboards_controller.rb +99 -0
  50. data/app/controllers/blazer/queries_controller.rb +472 -0
  51. data/app/controllers/blazer/uploads_controller.rb +147 -0
  52. data/app/helpers/blazer/base_helper.rb +39 -0
  53. data/app/models/blazer/audit.rb +6 -0
  54. data/app/models/blazer/check.rb +104 -0
  55. data/app/models/blazer/connection.rb +5 -0
  56. data/app/models/blazer/dashboard.rb +17 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +42 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/models/blazer/upload.rb +11 -0
  61. data/app/models/blazer/uploads_connection.rb +7 -0
  62. data/app/views/blazer/_nav.html.erb +18 -0
  63. data/app/views/blazer/_variables.html.erb +127 -0
  64. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  65. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  66. data/app/views/blazer/checks/_form.html.erb +79 -0
  67. data/app/views/blazer/checks/edit.html.erb +3 -0
  68. data/app/views/blazer/checks/index.html.erb +72 -0
  69. data/app/views/blazer/checks/new.html.erb +3 -0
  70. data/app/views/blazer/dashboards/_form.html.erb +82 -0
  71. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  72. data/app/views/blazer/dashboards/new.html.erb +3 -0
  73. data/app/views/blazer/dashboards/show.html.erb +53 -0
  74. data/app/views/blazer/queries/_caching.html.erb +16 -0
  75. data/app/views/blazer/queries/_cohorts.html.erb +48 -0
  76. data/app/views/blazer/queries/_form.html.erb +255 -0
  77. data/app/views/blazer/queries/docs.html.erb +147 -0
  78. data/app/views/blazer/queries/edit.html.erb +2 -0
  79. data/app/views/blazer/queries/home.html.erb +169 -0
  80. data/app/views/blazer/queries/new.html.erb +2 -0
  81. data/app/views/blazer/queries/run.html.erb +183 -0
  82. data/app/views/blazer/queries/schema.html.erb +55 -0
  83. data/app/views/blazer/queries/show.html.erb +72 -0
  84. data/app/views/blazer/uploads/_form.html.erb +27 -0
  85. data/app/views/blazer/uploads/edit.html.erb +3 -0
  86. data/app/views/blazer/uploads/index.html.erb +55 -0
  87. data/app/views/blazer/uploads/new.html.erb +3 -0
  88. data/app/views/layouts/blazer/application.html.erb +25 -0
  89. data/config/routes.rb +25 -0
  90. data/lib/blazer/adapters/athena_adapter.rb +182 -0
  91. data/lib/blazer/adapters/base_adapter.rb +76 -0
  92. data/lib/blazer/adapters/bigquery_adapter.rb +79 -0
  93. data/lib/blazer/adapters/cassandra_adapter.rb +70 -0
  94. data/lib/blazer/adapters/drill_adapter.rb +38 -0
  95. data/lib/blazer/adapters/druid_adapter.rb +102 -0
  96. data/lib/blazer/adapters/elasticsearch_adapter.rb +61 -0
  97. data/lib/blazer/adapters/hive_adapter.rb +55 -0
  98. data/lib/blazer/adapters/ignite_adapter.rb +64 -0
  99. data/lib/blazer/adapters/influxdb_adapter.rb +57 -0
  100. data/lib/blazer/adapters/neo4j_adapter.rb +62 -0
  101. data/lib/blazer/adapters/opensearch_adapter.rb +52 -0
  102. data/lib/blazer/adapters/presto_adapter.rb +54 -0
  103. data/lib/blazer/adapters/salesforce_adapter.rb +50 -0
  104. data/lib/blazer/adapters/snowflake_adapter.rb +82 -0
  105. data/lib/blazer/adapters/soda_adapter.rb +105 -0
  106. data/lib/blazer/adapters/spark_adapter.rb +14 -0
  107. data/lib/blazer/adapters/sql_adapter.rb +353 -0
  108. data/lib/blazer/adapters.rb +17 -0
  109. data/lib/blazer/anomaly_detectors.rb +22 -0
  110. data/lib/blazer/check_mailer.rb +27 -0
  111. data/lib/blazer/data_source.rb +266 -0
  112. data/lib/blazer/engine.rb +42 -0
  113. data/lib/blazer/forecasters.rb +7 -0
  114. data/lib/blazer/result.rb +178 -0
  115. data/lib/blazer/result_cache.rb +71 -0
  116. data/lib/blazer/run_statement.rb +45 -0
  117. data/lib/blazer/run_statement_job.rb +20 -0
  118. data/lib/blazer/slack_notifier.rb +94 -0
  119. data/lib/blazer/statement.rb +77 -0
  120. data/lib/blazer/version.rb +3 -0
  121. data/lib/blazer.rb +282 -0
  122. data/lib/generators/blazer/install_generator.rb +22 -0
  123. data/lib/generators/blazer/templates/config.yml.tt +79 -0
  124. data/lib/generators/blazer/templates/install.rb.tt +47 -0
  125. data/lib/generators/blazer/templates/uploads.rb.tt +10 -0
  126. data/lib/generators/blazer/uploads_generator.rb +18 -0
  127. data/lib/tasks/blazer.rake +20 -0
  128. data/licenses/LICENSE-ace.txt +24 -0
  129. data/licenses/LICENSE-bootstrap.txt +21 -0
  130. data/licenses/LICENSE-chart.js.txt +9 -0
  131. data/licenses/LICENSE-chartjs-adapter-date-fns.txt +9 -0
  132. data/licenses/LICENSE-chartkick.js.txt +22 -0
  133. data/licenses/LICENSE-date-fns.txt +21 -0
  134. data/licenses/LICENSE-daterangepicker.txt +21 -0
  135. data/licenses/LICENSE-fuzzysearch.txt +20 -0
  136. data/licenses/LICENSE-highlight.js.txt +29 -0
  137. data/licenses/LICENSE-jquery.txt +20 -0
  138. data/licenses/LICENSE-kurkle-color.txt +9 -0
  139. data/licenses/LICENSE-mapkick-bundle.txt +1029 -0
  140. data/licenses/LICENSE-moment-timezone.txt +20 -0
  141. data/licenses/LICENSE-moment.txt +22 -0
  142. data/licenses/LICENSE-rails-ujs.txt +20 -0
  143. data/licenses/LICENSE-selectize.txt +202 -0
  144. data/licenses/LICENSE-sortable.txt +21 -0
  145. data/licenses/LICENSE-stickytableheaders.txt +20 -0
  146. data/licenses/LICENSE-stupidtable.txt +19 -0
  147. data/licenses/LICENSE-vue.txt +21 -0
  148. metadata +271 -0
@@ -0,0 +1,42 @@
1
+ module Blazer
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Blazer
4
+
5
+ initializer "blazer" do |app|
6
+ if defined?(Sprockets) && Sprockets::VERSION.to_i >= 4
7
+ app.config.assets.precompile += [
8
+ "blazer/application.js",
9
+ "blazer/application.css",
10
+ "blazer/glyphicons-halflings-regular.eot",
11
+ "blazer/glyphicons-halflings-regular.svg",
12
+ "blazer/glyphicons-halflings-regular.ttf",
13
+ "blazer/glyphicons-halflings-regular.woff",
14
+ "blazer/glyphicons-halflings-regular.woff2",
15
+ "blazer/favicon.png"
16
+ ]
17
+ else
18
+ # use a proc instead of a string
19
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
20
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.(eot|svg|ttf|woff|woff2)\z/ }
21
+ app.config.assets.precompile << proc { |path| path == "blazer/favicon.png" }
22
+ end
23
+
24
+ Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
25
+ Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
26
+ Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
27
+ Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
28
+ Blazer.before_action = Blazer.settings["before_action_method"] if Blazer.settings["before_action_method"]
29
+ Blazer.check_schedules = Blazer.settings["check_schedules"] if Blazer.settings.key?("check_schedules")
30
+ Blazer.cache ||= Rails.cache
31
+
32
+ Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
33
+ Blazer.forecasting = Blazer.settings["forecasting"] || false
34
+ Blazer.async = Blazer.settings["async"] || false
35
+ Blazer.images = Blazer.settings["images"] || false
36
+ Blazer.override_csp = Blazer.settings["override_csp"] || false
37
+ Blazer.slack_oauth_token = Blazer.settings["slack_oauth_token"] || ENV["BLAZER_SLACK_OAUTH_TOKEN"]
38
+ Blazer.slack_webhook_url = Blazer.settings["slack_webhook_url"] || ENV["BLAZER_SLACK_WEBHOOK_URL"]
39
+ Blazer.mapbox_access_token = Blazer.settings["mapbox_access_token"] || ENV["MAPBOX_ACCESS_TOKEN"]
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ Blazer.register_forecaster "prophet" do |series, count:|
2
+ Prophet.forecast(series, count: count)
3
+ end
4
+
5
+ Blazer.register_forecaster "trend" do |series, count:|
6
+ Trend.forecast(series, count: count)
7
+ end
@@ -0,0 +1,178 @@
1
+ module Blazer
2
+ class Result
3
+ attr_reader :data_source, :columns, :rows, :error, :forecast_error
4
+ attr_accessor :cached_at, :just_cached
5
+
6
+ def initialize(data_source, columns, rows, error, cached_at, just_cached)
7
+ @data_source = data_source
8
+ @columns = columns
9
+ @rows = rows
10
+ @error = error
11
+ @cached_at = cached_at
12
+ @just_cached = just_cached
13
+ end
14
+
15
+ def timed_out?
16
+ error == Blazer::TIMEOUT_MESSAGE
17
+ end
18
+
19
+ def cached?
20
+ cached_at.present?
21
+ end
22
+
23
+ def smart_values
24
+ @smart_values ||= begin
25
+ smart_values = {}
26
+ columns.each_with_index do |key, i|
27
+ smart_columns_data_source =
28
+ ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_columns[key] }
29
+
30
+ if smart_columns_data_source
31
+ query = smart_columns_data_source.smart_columns[key]
32
+ res =
33
+ if query.is_a?(Hash)
34
+ query
35
+ else
36
+ values = rows.map { |r| r[i] }.compact.uniq
37
+ result = smart_columns_data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
38
+ result.rows
39
+ end
40
+
41
+ smart_values[key] = res.to_h { |k, v| [k.nil? ? k : k.to_s, v] }
42
+ end
43
+ end
44
+ smart_values
45
+ end
46
+ end
47
+
48
+ def column_types
49
+ @column_types ||= begin
50
+ columns.each_with_index.map do |k, i|
51
+ v = (rows.find { |r| r[i] } || {})[i]
52
+ if smart_values[k]
53
+ "string"
54
+ elsif v.is_a?(Numeric)
55
+ "numeric"
56
+ elsif v.is_a?(Time) || v.is_a?(Date)
57
+ "time"
58
+ elsif v.nil?
59
+ nil
60
+ elsif v.is_a?(String) && v.encoding == Encoding::BINARY
61
+ "binary"
62
+ else
63
+ "string"
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def chart_type
70
+ @chart_type ||= begin
71
+ if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
72
+ "line"
73
+ elsif column_types == ["time", "string", "numeric"]
74
+ "line2"
75
+ elsif column_types == ["string", "numeric"] && @columns.last == "pie"
76
+ "pie"
77
+ elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
78
+ "bar"
79
+ elsif column_types == ["string", "string", "numeric"]
80
+ "bar2"
81
+ elsif column_types == ["numeric", "numeric"]
82
+ "scatter"
83
+ end
84
+ end
85
+ end
86
+
87
+ def forecastable?
88
+ @forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
89
+ end
90
+
91
+ # TODO cache it?
92
+ # don't want to put result data (even hashed version)
93
+ # into cache without developer opt-in
94
+ def forecast
95
+ count = (@rows.size * 0.25).round.clamp(30, 365)
96
+
97
+ forecaster = Blazer.forecasters.fetch(Blazer.forecasting)
98
+ forecast = forecaster.call(@rows.to_h, count: count)
99
+
100
+ # round integers
101
+ if @rows[0][1].is_a?(Integer)
102
+ forecast = forecast.map { |k, v| [k, v.round] }.to_h
103
+ end
104
+
105
+ @rows.each do |row|
106
+ row[2] = nil
107
+ end
108
+ @rows.unshift(*forecast.map { |k, v| [k, nil, v] })
109
+ @columns << "forecast"
110
+
111
+ # reset cache
112
+ @column_types = nil
113
+ @chart_type = nil
114
+
115
+ forecast
116
+ rescue => e
117
+ @forecast_error = String.new("Error generating forecast")
118
+ @forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
119
+ nil
120
+ end
121
+
122
+ def detect_anomaly
123
+ anomaly = nil
124
+ message = nil
125
+
126
+ if rows.empty?
127
+ message = "No data"
128
+ else
129
+ if chart_type == "line" || chart_type == "line2"
130
+ series = []
131
+
132
+ if chart_type == "line"
133
+ columns[1..-1].each_with_index.each do |k, i|
134
+ series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
135
+ end
136
+ else
137
+ rows.group_by { |r| v = r[1]; (smart_values[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
138
+ series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
139
+ end
140
+ end
141
+
142
+ current_series = nil
143
+ begin
144
+ anomalies = []
145
+ series.each do |s|
146
+ current_series = s[:name]
147
+ anomalies << s[:name] if anomaly?(s[:data])
148
+ end
149
+ anomaly = anomalies.any?
150
+ if anomaly
151
+ if anomalies.size == 1
152
+ message = "Anomaly detected in #{anomalies.first}"
153
+ else
154
+ message = "Anomalies detected in #{anomalies.to_sentence}"
155
+ end
156
+ else
157
+ message = "No anomalies detected"
158
+ end
159
+ rescue => e
160
+ message = "#{current_series}: #{e.message}"
161
+ raise e if Rails.env.development?
162
+ end
163
+ else
164
+ message = "Bad format"
165
+ end
166
+ end
167
+
168
+ [anomaly, message]
169
+ end
170
+
171
+ def anomaly?(series)
172
+ series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
173
+
174
+ anomaly_detector = Blazer.anomaly_detectors.fetch(Blazer.anomaly_checks)
175
+ anomaly_detector.call(series)
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,71 @@
1
+ module Blazer
2
+ class ResultCache
3
+ def initialize(data_source)
4
+ @data_source = data_source
5
+ end
6
+
7
+ def write_run(run_id, result)
8
+ write(run_cache_key(run_id), result, expires_in: 30.seconds)
9
+ end
10
+
11
+ def read_run(run_id)
12
+ read(run_cache_key(run_id))
13
+ end
14
+
15
+ def delete_run(run_id)
16
+ delete(run_cache_key(run_id))
17
+ end
18
+
19
+ def write_statement(statement, result, expires_in:)
20
+ write(statement_cache_key(statement), result, expires_in: expires_in) if caching?
21
+ end
22
+
23
+ def read_statement(statement)
24
+ read(statement_cache_key(statement)) if caching?
25
+ end
26
+
27
+ def delete_statement(statement)
28
+ delete(statement_cache_key(statement)) if caching?
29
+ end
30
+
31
+ private
32
+
33
+ def write(key, result, expires_in:)
34
+ raise ArgumentError, "expected Blazer::Result" unless result.is_a?(Blazer::Result)
35
+ value = [result.columns, result.rows, result.error, result.cached_at, result.just_cached]
36
+ cache.write(key, value, expires_in: expires_in)
37
+ end
38
+
39
+ def read(key)
40
+ value = cache.read(key)
41
+ if value
42
+ columns, rows, error, cached_at, just_cached = value
43
+ Blazer::Result.new(@data_source, columns, rows, error, cached_at, just_cached)
44
+ end
45
+ end
46
+
47
+ def delete(key)
48
+ cache.delete(key)
49
+ end
50
+
51
+ def caching?
52
+ @data_source.cache_mode != "off"
53
+ end
54
+
55
+ def cache_key(key)
56
+ (["blazer", "v5", @data_source.id] + key).join("/")
57
+ end
58
+
59
+ def statement_cache_key(statement)
60
+ cache_key(["statement", Digest::SHA256.hexdigest(statement.bind_statement.to_s.gsub("\r\n", "\n") + statement.bind_values.to_json)])
61
+ end
62
+
63
+ def run_cache_key(run_id)
64
+ cache_key(["run", run_id])
65
+ end
66
+
67
+ def cache
68
+ Blazer.cache
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,45 @@
1
+ module Blazer
2
+ class RunStatement
3
+ def perform(statement, options = {})
4
+ query = options[:query]
5
+
6
+ data_source = statement.data_source
7
+ statement.bind
8
+
9
+ # audit
10
+ if Blazer.audit
11
+ audit_statement = statement.bind_statement
12
+ audit_statement += "\n\n#{statement.bind_values.to_json}" if statement.bind_values.any?
13
+ audit = Blazer::Audit.new(statement: audit_statement)
14
+ audit.query = query
15
+ audit.data_source = data_source.id
16
+ # only set user if present to avoid error with Rails 7.1 when no user model
17
+ audit.user = options[:user] unless options[:user].nil?
18
+ audit.save!
19
+ end
20
+
21
+ start_time = Blazer.monotonic_time
22
+ result = data_source.run_statement(statement, options)
23
+ duration = Blazer.monotonic_time - start_time
24
+
25
+ if Blazer.audit
26
+ audit.duration = duration if audit.respond_to?(:duration=)
27
+ audit.error = result.error if audit.respond_to?(:error=)
28
+ audit.timed_out = result.timed_out? if audit.respond_to?(:timed_out=)
29
+ audit.cached = result.cached? if audit.respond_to?(:cached=)
30
+ if !result.cached? && duration >= 10
31
+ audit.cost = data_source.cost(statement) if audit.respond_to?(:cost=)
32
+ end
33
+ audit.save! if audit.changed?
34
+ end
35
+
36
+ if query && !result.timed_out? && !result.cached? && !query.variables.any?
37
+ query.checks.each do |check|
38
+ check.update_state(result)
39
+ end
40
+ end
41
+
42
+ result
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ module Blazer
2
+ class RunStatementJob < ActiveJob::Base
3
+ self.queue_adapter = :async
4
+
5
+ def perform(data_source_id, statement, options)
6
+ statement = Blazer::Statement.new(statement, data_source_id)
7
+ statement.values = options.delete(:values)
8
+ data_source = statement.data_source
9
+ begin
10
+ ActiveRecord::Base.connection_pool.with_connection do
11
+ Blazer::RunStatement.new.perform(statement, options)
12
+ end
13
+ rescue Exception => e
14
+ result = Blazer::Result.new(data_source, [], [], "Unknown error", nil, false)
15
+ data_source.result_cache.write_run(options[:run_id], result)
16
+ raise e
17
+ end
18
+ end
19
+ end
20
+ end
@@ -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.5"
3
+ end