railsblazer 2.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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/ISSUE_TEMPLATE.md +0 -0
  4. data/.gitignore +14 -0
  5. data/CHANGELOG.md +247 -0
  6. data/CONTRIBUTING.md +42 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +855 -0
  10. data/Rakefile +1 -0
  11. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  12. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  13. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  14. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  15. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  16. data/app/assets/javascripts/blazer/Chart.js +14145 -0
  17. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  18. data/app/assets/javascripts/blazer/ace.js +6 -0
  19. data/app/assets/javascripts/blazer/ace/ace.js +11 -0
  20. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
  21. data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
  22. data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
  23. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
  24. data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
  25. data/app/assets/javascripts/blazer/application.js +79 -0
  26. data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
  27. data/app/assets/javascripts/blazer/chartkick.js +1693 -0
  28. data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
  29. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  30. data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
  31. data/app/assets/javascripts/blazer/jquery.js +10308 -0
  32. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
  33. data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
  34. data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
  35. data/app/assets/javascripts/blazer/moment.js +3043 -0
  36. data/app/assets/javascripts/blazer/queries.js +110 -0
  37. data/app/assets/javascripts/blazer/routes.js +23 -0
  38. data/app/assets/javascripts/blazer/selectize.js +3667 -0
  39. data/app/assets/javascripts/blazer/stupidtable.js +114 -0
  40. data/app/assets/javascripts/blazer/vue.js +7515 -0
  41. data/app/assets/stylesheets/blazer/application.css +198 -0
  42. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
  43. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
  44. data/app/assets/stylesheets/blazer/github.css +125 -0
  45. data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
  46. data/app/controllers/blazer/base_controller.rb +113 -0
  47. data/app/controllers/blazer/checks_controller.rb +56 -0
  48. data/app/controllers/blazer/dashboards_controller.rb +105 -0
  49. data/app/controllers/blazer/queries_controller.rb +337 -0
  50. data/app/helpers/blazer/base_helper.rb +57 -0
  51. data/app/mailers/blazer/check_mailer.rb +27 -0
  52. data/app/mailers/blazer/slack_notifier.rb +76 -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 +13 -0
  57. data/app/models/blazer/dashboard_query.rb +9 -0
  58. data/app/models/blazer/query.rb +40 -0
  59. data/app/models/blazer/record.rb +5 -0
  60. data/app/views/blazer/_nav.html.erb +16 -0
  61. data/app/views/blazer/_variables.html.erb +102 -0
  62. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  63. data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
  64. data/app/views/blazer/checks/_form.html.erb +79 -0
  65. data/app/views/blazer/checks/edit.html.erb +1 -0
  66. data/app/views/blazer/checks/index.html.erb +43 -0
  67. data/app/views/blazer/checks/new.html.erb +1 -0
  68. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  69. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  70. data/app/views/blazer/dashboards/new.html.erb +1 -0
  71. data/app/views/blazer/dashboards/show.html.erb +47 -0
  72. data/app/views/blazer/queries/_form.html.erb +240 -0
  73. data/app/views/blazer/queries/edit.html.erb +2 -0
  74. data/app/views/blazer/queries/home.html.erb +152 -0
  75. data/app/views/blazer/queries/new.html.erb +2 -0
  76. data/app/views/blazer/queries/run.html.erb +165 -0
  77. data/app/views/blazer/queries/schema.html.erb +20 -0
  78. data/app/views/blazer/queries/show.html.erb +73 -0
  79. data/app/views/layouts/blazer/application.html.erb +24 -0
  80. data/blazer-0.0.1.gem +0 -0
  81. data/blazer.gemspec +27 -0
  82. data/config/routes.rb +16 -0
  83. data/lib/blazer.rb +223 -0
  84. data/lib/blazer/adapters/athena_adapter.rb +128 -0
  85. data/lib/blazer/adapters/base_adapter.rb +53 -0
  86. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  87. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  88. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  89. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  90. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  91. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  92. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  93. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +182 -0
  95. data/lib/blazer/data_source.rb +195 -0
  96. data/lib/blazer/detect_anomalies.R +19 -0
  97. data/lib/blazer/engine.rb +30 -0
  98. data/lib/blazer/result.rb +170 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +21 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +39 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +62 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. data/railsblazer-0.0.1.gem +0 -0
  107. metadata +234 -0
@@ -0,0 +1,195 @@
1
+ require "digest/md5"
2
+
3
+ module Blazer
4
+ class DataSource
5
+ extend Forwardable
6
+
7
+ attr_reader :id, :settings
8
+
9
+ def_delegators :adapter_instance, :schema, :tables, :preview_statement, :reconnect, :cost, :explain, :cancel
10
+
11
+ def initialize(id, settings)
12
+ @id = id
13
+ @settings = settings
14
+ end
15
+
16
+ def adapter
17
+ settings["adapter"] || detect_adapter
18
+ end
19
+
20
+ def name
21
+ settings["name"] || @id
22
+ end
23
+
24
+ def linked_columns
25
+ settings["linked_columns"] || {}
26
+ end
27
+
28
+ def smart_columns
29
+ settings["smart_columns"] || {}
30
+ end
31
+
32
+ def smart_variables
33
+ settings["smart_variables"] || {}
34
+ end
35
+
36
+ def variable_defaults
37
+ settings["variable_defaults"] || {}
38
+ end
39
+
40
+ def timeout
41
+ settings["timeout"]
42
+ end
43
+
44
+ def cache
45
+ @cache ||= begin
46
+ if settings["cache"].is_a?(Hash)
47
+ settings["cache"]
48
+ elsif settings["cache"]
49
+ {
50
+ "mode" => "all",
51
+ "expires_in" => settings["cache"]
52
+ }
53
+ else
54
+ {
55
+ "mode" => "off"
56
+ }
57
+ end
58
+ end
59
+ end
60
+
61
+ def cache_mode
62
+ cache["mode"]
63
+ end
64
+
65
+ def cache_expires_in
66
+ (cache["expires_in"] || 60).to_f
67
+ end
68
+
69
+ def cache_slow_threshold
70
+ (cache["slow_threshold"] || 15).to_f
71
+ end
72
+
73
+ def local_time_suffix
74
+ @local_time_suffix ||= Array(settings["local_time_suffix"])
75
+ end
76
+
77
+ def read_cache(cache_key)
78
+ value = Blazer.cache.read(cache_key)
79
+ if value
80
+ Blazer::Result.new(self, *Marshal.load(value), nil)
81
+ end
82
+ end
83
+
84
+ def run_results(run_id)
85
+ read_cache(run_cache_key(run_id))
86
+ end
87
+
88
+ def delete_results(run_id)
89
+ Blazer.cache.delete(run_cache_key(run_id))
90
+ end
91
+
92
+ def run_statement(statement, options = {})
93
+ async = options[:async]
94
+ result = nil
95
+ if cache_mode != "off"
96
+ if options[:refresh_cache]
97
+ clear_cache(statement) # for checks
98
+ else
99
+ result = read_cache(statement_cache_key(statement))
100
+ end
101
+ end
102
+
103
+ unless result
104
+ comment = "blazer"
105
+ if options[:user].respond_to?(:id)
106
+ comment << ",user_id:#{options[:user].id}"
107
+ end
108
+ if options[:user].respond_to?(Blazer.user_name)
109
+ # only include letters, numbers, and spaces to prevent injection
110
+ comment << ",user_name:#{options[:user].send(Blazer.user_name).to_s.gsub(/[^a-zA-Z0-9 ]/, "")}"
111
+ end
112
+ if options[:query].respond_to?(:id)
113
+ comment << ",query_id:#{options[:query].id}"
114
+ end
115
+ if options[:check]
116
+ comment << ",check_id:#{options[:check].id},check_emails:#{options[:check].emails}"
117
+ end
118
+ if options[:run_id]
119
+ comment << ",run_id:#{options[:run_id]}"
120
+ end
121
+ result = run_statement_helper(statement, comment, async ? options[:run_id] : nil)
122
+ end
123
+
124
+ result
125
+ end
126
+
127
+ def clear_cache(statement)
128
+ Blazer.cache.delete(statement_cache_key(statement))
129
+ end
130
+
131
+ def cache_key(key)
132
+ (["blazer", "v4"] + key).join("/")
133
+ end
134
+
135
+ def statement_cache_key(statement)
136
+ cache_key(["statement", id, Digest::MD5.hexdigest(statement.to_s.gsub("\r\n", "\n"))])
137
+ end
138
+
139
+ def run_cache_key(run_id)
140
+ cache_key(["run", run_id])
141
+ end
142
+
143
+ protected
144
+
145
+ def adapter_instance
146
+ @adapter_instance ||= begin
147
+ unless settings["url"] || Rails.env.development? || ["bigquery", "athena", "snowflake"].include?(settings["adapter"])
148
+ raise Blazer::Error, "Empty url for data source: #{id}"
149
+ end
150
+
151
+ unless Blazer.adapters[adapter]
152
+ raise Blazer::Error, "Unknown adapter"
153
+ end
154
+
155
+ Blazer.adapters[adapter].new(self)
156
+ end
157
+ end
158
+
159
+ def run_statement_helper(statement, comment, run_id)
160
+ start_time = Time.now
161
+ columns, rows, error = adapter_instance.run_statement(statement, comment)
162
+ duration = Time.now - start_time
163
+
164
+ cache_data = nil
165
+ cache = !error && (cache_mode == "all" || (cache_mode == "slow" && duration >= cache_slow_threshold))
166
+ if cache || run_id
167
+ cache_data = Marshal.dump([columns, rows, error, cache ? Time.now : nil]) rescue nil
168
+ end
169
+
170
+ if cache && cache_data && adapter_instance.cachable?(statement)
171
+ Blazer.cache.write(statement_cache_key(statement), cache_data, expires_in: cache_expires_in.to_f * 60)
172
+ end
173
+
174
+ if run_id
175
+ unless cache_data
176
+ error = "Error storing the results of this query :("
177
+ cache_data = Marshal.dump([[], [], error, nil])
178
+ end
179
+ Blazer.cache.write(run_cache_key(run_id), cache_data, expires_in: 30.seconds)
180
+ end
181
+
182
+ Blazer::Result.new(self, columns, rows, error, nil, cache && !cache_data.nil?)
183
+ end
184
+
185
+ def detect_adapter
186
+ schema = settings["url"].to_s.split("://").first
187
+ case schema
188
+ when "mongodb", "presto", "cassandra"
189
+ schema
190
+ else
191
+ "sql"
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,19 @@
1
+ tryCatch({
2
+ library(AnomalyDetection)
3
+
4
+ args <- commandArgs(trailingOnly = TRUE)
5
+
6
+ con <- textConnection(args[2])
7
+ data <- read.csv(con, stringsAsFactors = FALSE)
8
+ data$timestamp <- as.POSIXct(data$timestamp)
9
+
10
+ if (identical(args[1], "ts")) {
11
+ res <- AnomalyDetectionTs(data, direction = "both", alpha = 0.05, max_anoms = 0.2)
12
+ } else {
13
+ res <- AnomalyDetectionVec(data$count, direction = "both", alpha = 0.05, max_anoms = 0.2, period = length(data$count) / 2 - 1)
14
+ }
15
+
16
+ write.csv(res$anoms)
17
+ }, error = function (e) {
18
+ write.csv(geterrmessage())
19
+ })
@@ -0,0 +1,30 @@
1
+ module Blazer
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Blazer
4
+
5
+ initializer "blazer" do |app|
6
+ # use a proc instead of a string
7
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
8
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.(eot|svg|ttf|woff)\z/ }
9
+ app.config.assets.prefix = Blazer.settings["assets_prefix"] || "/assets"
10
+
11
+ Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
12
+ Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
13
+ Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
14
+ Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
15
+ Blazer.before_action = Blazer.settings["before_action_method"] if Blazer.settings["before_action_method"]
16
+ Blazer.check_schedules = Blazer.settings["check_schedules"] if Blazer.settings.key?("check_schedules")
17
+ Blazer.cache ||= Rails.cache
18
+
19
+ Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
20
+ Blazer.async = Blazer.settings["async"] || false
21
+ if Blazer.async
22
+ require "blazer/run_statement_job"
23
+ end
24
+
25
+ Blazer.images = Blazer.settings["images"] || false
26
+ Blazer.override_csp = Blazer.settings["override_csp"] || false
27
+ Blazer.slack_webhook_url = ENV["BLAZER_SLACK_WEBHOOK_URL"]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,170 @@
1
+ module Blazer
2
+ class Result
3
+ attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached
4
+
5
+ def initialize(data_source, columns, rows, error, cached_at, just_cached)
6
+ @data_source = data_source
7
+ @columns = columns
8
+ @rows = rows
9
+ @error = error
10
+ @cached_at = cached_at
11
+ @just_cached = just_cached
12
+ end
13
+
14
+ def timed_out?
15
+ error == Blazer::TIMEOUT_MESSAGE
16
+ end
17
+
18
+ def cached?
19
+ cached_at.present?
20
+ end
21
+
22
+ def boom
23
+ @boom ||= begin
24
+ boom = {}
25
+ columns.each_with_index do |key, i|
26
+ smart_columns_data_source =
27
+ ([data_source] + Array(data_source.settings["inherit_smart_settings"]).map { |ds| Blazer.data_sources[ds] }).find { |ds| ds.smart_columns[key] }
28
+
29
+ if smart_columns_data_source
30
+ query = smart_columns_data_source.smart_columns[key]
31
+ res =
32
+ if query.is_a?(Hash)
33
+ query
34
+ else
35
+ values = rows.map { |r| r[i] }.compact.uniq
36
+ result = smart_columns_data_source.run_statement(ActiveRecord::Base.send(:sanitize_sql_array, [query.sub("{value}", "(?)"), values]))
37
+ result.rows
38
+ end
39
+
40
+ boom[key] = Hash[res.map { |k, v| [k.nil? ? k : k.to_s, v] }]
41
+ end
42
+ end
43
+ boom
44
+ end
45
+ end
46
+
47
+ def column_types
48
+ @column_types ||= begin
49
+ columns.each_with_index.map do |k, i|
50
+ v = (rows.find { |r| r[i] } || {})[i]
51
+ if boom[k]
52
+ "string"
53
+ elsif v.is_a?(Numeric)
54
+ "numeric"
55
+ elsif v.is_a?(Time) || v.is_a?(Date)
56
+ "time"
57
+ elsif v.nil?
58
+ nil
59
+ else
60
+ "string"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def chart_type
67
+ @chart_type ||= begin
68
+ if column_types.compact.size >= 2 && column_types.compact == ["time"] + (column_types.compact.size - 1).times.map { "numeric" }
69
+ "line"
70
+ elsif column_types == ["time", "string", "numeric"]
71
+ "line2"
72
+ elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
73
+ "bar"
74
+ elsif column_types == ["string", "string", "numeric"]
75
+ "bar2"
76
+ elsif column_types == ["numeric", "numeric"]
77
+ "scatter"
78
+ end
79
+ end
80
+ end
81
+
82
+ def detect_anomaly
83
+ anomaly = nil
84
+ message = nil
85
+
86
+ if rows.empty?
87
+ message = "No data"
88
+ else
89
+ if chart_type == "line" || chart_type == "line2"
90
+ series = []
91
+
92
+ if chart_type == "line"
93
+ columns[1..-1].each_with_index.each do |k, i|
94
+ series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
95
+ end
96
+ else
97
+ rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
98
+ series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
99
+ end
100
+ end
101
+
102
+ current_series = nil
103
+ begin
104
+ anomalies = []
105
+ series.each do |s|
106
+ current_series = s[:name]
107
+ anomalies << s[:name] if anomaly?(s[:data])
108
+ end
109
+ anomaly = anomalies.any?
110
+ if anomaly
111
+ if anomalies.size == 1
112
+ message = "Anomaly detected in #{anomalies.first}"
113
+ else
114
+ message = "Anomalies detected in #{anomalies.to_sentence}"
115
+ end
116
+ else
117
+ message = "No anomalies detected"
118
+ end
119
+ rescue => e
120
+ message = "#{current_series}: #{e.message}"
121
+ raise e if Rails.env.development?
122
+ end
123
+ else
124
+ message = "Bad format"
125
+ end
126
+ end
127
+
128
+ [anomaly, message]
129
+ end
130
+
131
+ def anomaly?(series)
132
+ series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
133
+
134
+ csv_str =
135
+ CSV.generate do |csv|
136
+ csv << ["timestamp", "count"]
137
+ series.each do |row|
138
+ csv << row
139
+ end
140
+ end
141
+
142
+ r_script = %x[which Rscript].chomp
143
+ type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
144
+ args = [type, csv_str]
145
+ raise "R not found" if r_script.empty?
146
+ command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
147
+ output = %x[#{command}]
148
+ if output.empty?
149
+ raise "Unknown R error"
150
+ end
151
+
152
+ rows = CSV.parse(output, headers: true)
153
+ error = rows.first && rows.first["x"]
154
+ raise error if error
155
+
156
+ timestamps = []
157
+ if type == "ts"
158
+ rows.each do |row|
159
+ timestamps << Time.parse(row["timestamp"])
160
+ end
161
+ timestamps.include?(series.last[0].to_time)
162
+ else
163
+ rows.each do |row|
164
+ timestamps << row["index"].to_i
165
+ end
166
+ timestamps.include?(series.length)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,40 @@
1
+ module Blazer
2
+ class RunStatement
3
+ def perform(data_source, statement, options = {})
4
+ query = options[:query]
5
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
6
+
7
+ # audit
8
+ if Blazer.audit
9
+ audit = Blazer::Audit.new(statement: statement)
10
+ audit.query = query
11
+ audit.data_source = data_source.id
12
+ audit.user = options[:user]
13
+ audit.save!
14
+ end
15
+
16
+ start_time = Time.now
17
+ result = data_source.run_statement(statement, options)
18
+ duration = Time.now - start_time
19
+
20
+ if Blazer.audit
21
+ audit.duration = duration if audit.respond_to?(:duration=)
22
+ audit.error = result.error if audit.respond_to?(:error=)
23
+ audit.timed_out = result.timed_out? if audit.respond_to?(:timed_out=)
24
+ audit.cached = result.cached? if audit.respond_to?(:cached=)
25
+ if !result.cached? && duration >= 10
26
+ audit.cost = data_source.cost(statement) if audit.respond_to?(:cost=)
27
+ end
28
+ audit.save! if audit.changed?
29
+ end
30
+
31
+ if query && !result.timed_out? && !query.variables.any?
32
+ query.checks.each do |check|
33
+ check.update_state(result)
34
+ end
35
+ end
36
+
37
+ result
38
+ end
39
+ end
40
+ end