blazer 2.2.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of blazer might be problematic. Click here for more details.

Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +310 -0
  3. data/CONTRIBUTING.md +42 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +1041 -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/Chart.js +14456 -0
  13. data/app/assets/javascripts/blazer/Sortable.js +1540 -0
  14. data/app/assets/javascripts/blazer/ace.js +6 -0
  15. data/app/assets/javascripts/blazer/ace/ace.js +21301 -0
  16. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +1993 -0
  17. data/app/assets/javascripts/blazer/ace/mode-sql.js +110 -0
  18. data/app/assets/javascripts/blazer/ace/snippets/sql.js +40 -0
  19. data/app/assets/javascripts/blazer/ace/snippets/text.js +14 -0
  20. data/app/assets/javascripts/blazer/ace/theme-twilight.js +116 -0
  21. data/app/assets/javascripts/blazer/application.js +81 -0
  22. data/app/assets/javascripts/blazer/bootstrap.js +2377 -0
  23. data/app/assets/javascripts/blazer/chartkick.js +2214 -0
  24. data/app/assets/javascripts/blazer/daterangepicker.js +1653 -0
  25. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  26. data/app/assets/javascripts/blazer/highlight.min.js +3 -0
  27. data/app/assets/javascripts/blazer/jquery-ujs.js +555 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10364 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +325 -0
  30. data/app/assets/javascripts/blazer/moment-timezone-with-data.js +1212 -0
  31. data/app/assets/javascripts/blazer/moment.js +3043 -0
  32. data/app/assets/javascripts/blazer/queries.js +110 -0
  33. data/app/assets/javascripts/blazer/routes.js +26 -0
  34. data/app/assets/javascripts/blazer/selectize.js +3891 -0
  35. data/app/assets/javascripts/blazer/stupidtable-custom-settings.js +13 -0
  36. data/app/assets/javascripts/blazer/stupidtable.js +281 -0
  37. data/app/assets/javascripts/blazer/vue.js +10947 -0
  38. data/app/assets/stylesheets/blazer/application.css +234 -0
  39. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6756 -0
  40. data/app/assets/stylesheets/blazer/daterangepicker.css +269 -0
  41. data/app/assets/stylesheets/blazer/github.css +125 -0
  42. data/app/assets/stylesheets/blazer/selectize.css +403 -0
  43. data/app/controllers/blazer/base_controller.rb +124 -0
  44. data/app/controllers/blazer/checks_controller.rb +56 -0
  45. data/app/controllers/blazer/dashboards_controller.rb +101 -0
  46. data/app/controllers/blazer/queries_controller.rb +347 -0
  47. data/app/helpers/blazer/base_helper.rb +43 -0
  48. data/app/mailers/blazer/check_mailer.rb +27 -0
  49. data/app/mailers/blazer/slack_notifier.rb +79 -0
  50. data/app/models/blazer/audit.rb +6 -0
  51. data/app/models/blazer/check.rb +104 -0
  52. data/app/models/blazer/connection.rb +5 -0
  53. data/app/models/blazer/dashboard.rb +17 -0
  54. data/app/models/blazer/dashboard_query.rb +9 -0
  55. data/app/models/blazer/query.rb +40 -0
  56. data/app/models/blazer/record.rb +5 -0
  57. data/app/views/blazer/_nav.html.erb +15 -0
  58. data/app/views/blazer/_variables.html.erb +124 -0
  59. data/app/views/blazer/check_mailer/failing_checks.html.erb +7 -0
  60. data/app/views/blazer/check_mailer/state_change.html.erb +48 -0
  61. data/app/views/blazer/checks/_form.html.erb +79 -0
  62. data/app/views/blazer/checks/edit.html.erb +3 -0
  63. data/app/views/blazer/checks/index.html.erb +69 -0
  64. data/app/views/blazer/checks/new.html.erb +3 -0
  65. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  66. data/app/views/blazer/dashboards/edit.html.erb +3 -0
  67. data/app/views/blazer/dashboards/new.html.erb +3 -0
  68. data/app/views/blazer/dashboards/show.html.erb +51 -0
  69. data/app/views/blazer/queries/_form.html.erb +250 -0
  70. data/app/views/blazer/queries/docs.html.erb +131 -0
  71. data/app/views/blazer/queries/edit.html.erb +2 -0
  72. data/app/views/blazer/queries/home.html.erb +163 -0
  73. data/app/views/blazer/queries/new.html.erb +2 -0
  74. data/app/views/blazer/queries/run.html.erb +198 -0
  75. data/app/views/blazer/queries/schema.html.erb +55 -0
  76. data/app/views/blazer/queries/show.html.erb +75 -0
  77. data/app/views/layouts/blazer/application.html.erb +24 -0
  78. data/config/routes.rb +20 -0
  79. data/lib/blazer.rb +231 -0
  80. data/lib/blazer/adapters/athena_adapter.rb +129 -0
  81. data/lib/blazer/adapters/base_adapter.rb +53 -0
  82. data/lib/blazer/adapters/bigquery_adapter.rb +68 -0
  83. data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
  84. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  85. data/lib/blazer/adapters/druid_adapter.rb +67 -0
  86. data/lib/blazer/adapters/elasticsearch_adapter.rb +46 -0
  87. data/lib/blazer/adapters/influxdb_adapter.rb +45 -0
  88. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  89. data/lib/blazer/adapters/neo4j_adapter.rb +47 -0
  90. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  91. data/lib/blazer/adapters/salesforce_adapter.rb +45 -0
  92. data/lib/blazer/adapters/snowflake_adapter.rb +73 -0
  93. data/lib/blazer/adapters/soda_adapter.rb +96 -0
  94. data/lib/blazer/adapters/sql_adapter.rb +221 -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 +43 -0
  98. data/lib/blazer/result.rb +218 -0
  99. data/lib/blazer/run_statement.rb +40 -0
  100. data/lib/blazer/run_statement_job.rb +18 -0
  101. data/lib/blazer/version.rb +3 -0
  102. data/lib/generators/blazer/install_generator.rb +22 -0
  103. data/lib/generators/blazer/templates/config.yml.tt +73 -0
  104. data/lib/generators/blazer/templates/install.rb.tt +46 -0
  105. data/lib/tasks/blazer.rake +11 -0
  106. metadata +231 -0
@@ -0,0 +1,45 @@
1
+ module Blazer
2
+ module Adapters
3
+ class PrestoAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ begin
10
+ columns, rows = client.run("#{statement} /*#{comment}*/")
11
+ columns = columns.map(&:name)
12
+ rescue => e
13
+ error = e.message
14
+ end
15
+
16
+ [columns, rows, error]
17
+ end
18
+
19
+ def tables
20
+ _, rows = client.run("SHOW TABLES")
21
+ rows.map(&:first)
22
+ end
23
+
24
+ def preview_statement
25
+ "SELECT * FROM {table} LIMIT 10"
26
+ end
27
+
28
+ protected
29
+
30
+ def client
31
+ @client ||= begin
32
+ uri = URI.parse(settings["url"])
33
+ query = uri.query ? CGI::parse(uri.query) : {}
34
+ Presto::Client.new(
35
+ server: "#{uri.host}:#{uri.port}",
36
+ catalog: uri.path.to_s.sub(/\A\//, ""),
37
+ schema: query["schema"] || "public",
38
+ user: uri.user,
39
+ http_debug: false
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module Blazer
2
+ module Adapters
3
+ class SalesforceAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ # remove comments manually
10
+ statement = statement.gsub(/--.+/, "")
11
+ # only supports single line /* */ comments
12
+ # regex not perfect, but should be good enough
13
+ statement = statement.gsub(/\/\*.+\*\//, "")
14
+
15
+ # remove trailing semicolon
16
+ statement = statement.sub(/;\s*\z/, "")
17
+
18
+ begin
19
+ response = client.query(statement)
20
+ rows = response.map { |r| r.to_hash.except("attributes").values }
21
+ columns = rows.any? ? response.first.to_hash.except("attributes").keys : []
22
+ rescue => e
23
+ error = e.message
24
+ end
25
+
26
+ [columns, rows, error]
27
+ end
28
+
29
+ def tables
30
+ # cache
31
+ @tables ||= client.describe.select { |r| r.queryable }.map(&:name)
32
+ end
33
+
34
+ def preview_statement
35
+ "SELECT Id FROM {table} LIMIT 10"
36
+ end
37
+
38
+ protected
39
+
40
+ def client
41
+ @client ||= Restforce.new
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,73 @@
1
+ module Blazer
2
+ module Adapters
3
+ class SnowflakeAdapter < SqlAdapter
4
+ def initialize(data_source)
5
+ @data_source = data_source
6
+
7
+ @@registered ||= begin
8
+ require "active_record/connection_adapters/odbc_adapter"
9
+ require "odbc_adapter/adapters/postgresql_odbc_adapter"
10
+
11
+ ODBCAdapter.register(/snowflake/, ODBCAdapter::Adapters::PostgreSQLODBCAdapter) do
12
+ # Explicitly turning off prepared statements as they are not yet working with
13
+ # snowflake + the ODBC ActiveRecord adapter
14
+ def prepared_statements
15
+ false
16
+ end
17
+
18
+ # Quoting needs to be changed for snowflake
19
+ def quote_column_name(name)
20
+ name.to_s
21
+ end
22
+
23
+ private
24
+
25
+ # Override dbms_type_cast to get the values encoded in UTF-8
26
+ def dbms_type_cast(columns, values)
27
+ int_column = {}
28
+ columns.each_with_index do |c, i|
29
+ int_column[i] = c.type == 3 && c.scale == 0
30
+ end
31
+
32
+ float_column = {}
33
+ columns.each_with_index do |c, i|
34
+ float_column[i] = c.type == 3 && c.scale != 0
35
+ end
36
+
37
+ values.each do |row|
38
+ row.each_index do |idx|
39
+ val = row[idx]
40
+ if val
41
+ if int_column[idx]
42
+ row[idx] = val.to_i
43
+ elsif float_column[idx]
44
+ row[idx] = val.to_f
45
+ elsif val.is_a?(String)
46
+ row[idx] = val.force_encoding('UTF-8')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ @connection_model =
56
+ Class.new(Blazer::Connection) do
57
+ def self.name
58
+ "Blazer::Connection::SnowflakeAdapter#{object_id}"
59
+ end
60
+ if data_source.settings["conn_str"]
61
+ establish_connection(adapter: "odbc", conn_str: data_source.settings["conn_str"])
62
+ elsif data_source.settings["dsn"]
63
+ establish_connection(adapter: "odbc", dsn: data_source.settings["dsn"])
64
+ end
65
+ end
66
+ end
67
+
68
+ def cancel(run_id)
69
+ # todo
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,96 @@
1
+ module Blazer
2
+ module Adapters
3
+ class SodaAdapter < BaseAdapter
4
+ def run_statement(statement, comment)
5
+ columns = []
6
+ rows = []
7
+ error = nil
8
+
9
+ # remove comments manually
10
+ statement = statement.gsub(/--.+/, "")
11
+ # only supports single line /* */ comments
12
+ # regex not perfect, but should be good enough
13
+ statement = statement.gsub(/\/\*.+\*\//, "")
14
+
15
+ # remove trailing semicolon
16
+ statement = statement.sub(/;\s*\z/, "")
17
+
18
+ # remove whitespace
19
+ statement = statement.squish
20
+
21
+ uri = URI(settings["url"])
22
+ uri.query = URI.encode_www_form("$query" => statement)
23
+
24
+ req = Net::HTTP::Get.new(uri)
25
+ req["X-App-Token"] = settings["app_token"] if settings["app_token"]
26
+
27
+ options = {
28
+ use_ssl: uri.scheme == "https",
29
+ open_timeout: 3,
30
+ read_timeout: 30
31
+ }
32
+
33
+ begin
34
+ # use Net::HTTP instead of soda-ruby for types and better error messages
35
+ res = Net::HTTP.start(uri.hostname, uri.port, options) do |http|
36
+ http.request(req)
37
+ end
38
+
39
+ if res.is_a?(Net::HTTPSuccess)
40
+ body = JSON.parse(res.body)
41
+
42
+ columns = JSON.parse(res["x-soda2-fields"])
43
+ column_types = columns.zip(JSON.parse(res["x-soda2-types"])).to_h
44
+
45
+ columns.reject! { |f| f.start_with?(":@") }
46
+ # rows can be missing some keys in JSON, so need to map by column
47
+ rows = body.map { |r| columns.map { |c| r[c] } }
48
+
49
+ columns.each_with_index do |column, i|
50
+ # nothing to do for boolean
51
+ case column_types[column]
52
+ when "number"
53
+ # check if likely an integer column
54
+ if rows.all? { |r| r[i].to_i == r[i].to_f }
55
+ rows.each do |row|
56
+ row[i] = row[i].to_i
57
+ end
58
+ else
59
+ rows.each do |row|
60
+ row[i] = row[i].to_f
61
+ end
62
+ end
63
+ when "floating_timestamp"
64
+ # check if likely a date column
65
+ if rows.all? { |r| r[i].end_with?("T00:00:00.000") }
66
+ rows.each do |row|
67
+ row[i] = Date.parse(row[i])
68
+ end
69
+ else
70
+ utc = ActiveSupport::TimeZone["Etc/UTC"]
71
+ rows.each do |row|
72
+ row[i] = utc.parse(row[i])
73
+ end
74
+ end
75
+ end
76
+ end
77
+ else
78
+ error = JSON.parse(res.body)["message"] rescue "Bad response: #{res.code}"
79
+ end
80
+ rescue => e
81
+ error = e.message
82
+ end
83
+
84
+ [columns, rows, error]
85
+ end
86
+
87
+ def preview_statement
88
+ "SELECT * LIMIT 10"
89
+ end
90
+
91
+ def tables
92
+ ["all"]
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,221 @@
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)
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
+ result = select_all("#{statement} /*#{comment}*/")
28
+ columns = result.columns
29
+ result.rows.each do |untyped_row|
30
+ rows << (result.column_types.empty? ? untyped_row : columns.each_with_index.map { |c, i| untyped_row[i] ? result.column_types[c].send(:cast_value, untyped_row[i]) : untyped_row[i] })
31
+ end
32
+ end
33
+ rescue => e
34
+ error = e.message.sub(/.+ERROR: /, "")
35
+ error = Blazer::TIMEOUT_MESSAGE if Blazer::TIMEOUT_ERRORS.any? { |e| error.include?(e) }
36
+ reconnect if error.include?("PG::ConnectionBad")
37
+ end
38
+
39
+ [columns, rows, error]
40
+ end
41
+
42
+ def tables
43
+ sql = add_schemas("SELECT table_schema, table_name FROM information_schema.tables")
44
+ result = data_source.run_statement(sql, refresh_cache: true)
45
+ if postgresql? || redshift? || snowflake?
46
+ result.rows.sort_by { |r| [r[0] == default_schema ? "" : r[0], r[1]] }.map do |row|
47
+ table =
48
+ if row[0] == default_schema
49
+ row[1]
50
+ else
51
+ "#{row[0]}.#{row[1]}"
52
+ end
53
+
54
+ table = table.downcase if snowflake?
55
+
56
+ {
57
+ table: table,
58
+ value: connection_model.connection.quote_table_name(table)
59
+ }
60
+ end
61
+ else
62
+ result.rows.map(&:second).sort
63
+ end
64
+ end
65
+
66
+ def schema
67
+ sql = add_schemas("SELECT table_schema, table_name, column_name, data_type, ordinal_position FROM information_schema.columns")
68
+ result = data_source.run_statement(sql)
69
+ 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]] }
70
+ end
71
+
72
+ def preview_statement
73
+ if sqlserver?
74
+ "SELECT TOP (10) * FROM {table}"
75
+ else
76
+ "SELECT * FROM {table} LIMIT 10"
77
+ end
78
+ end
79
+
80
+ def reconnect
81
+ connection_model.establish_connection(settings["url"])
82
+ end
83
+
84
+ def cost(statement)
85
+ result = explain(statement)
86
+ if sqlserver?
87
+ result["TotalSubtreeCost"]
88
+ else
89
+ match = /cost=\d+\.\d+..(\d+\.\d+) /.match(result)
90
+ match[1] if match
91
+ end
92
+ end
93
+
94
+ def explain(statement)
95
+ if postgresql? || redshift?
96
+ select_all("EXPLAIN #{statement}").rows.first.first
97
+ elsif sqlserver?
98
+ begin
99
+ execute("SET SHOWPLAN_ALL ON")
100
+ result = select_all(statement).each.first
101
+ ensure
102
+ execute("SET SHOWPLAN_ALL OFF")
103
+ end
104
+ result
105
+ end
106
+ rescue
107
+ nil
108
+ end
109
+
110
+ def cancel(run_id)
111
+ if postgresql?
112
+ select_all("SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND query LIKE ?", ["%,run_id:#{run_id}%"])
113
+ elsif redshift?
114
+ first_row = select_all("SELECT pid FROM stv_recents WHERE status = 'Running' AND query LIKE ?", ["%,run_id:#{run_id}%"]).first
115
+ if first_row
116
+ select_all("CANCEL #{first_row["pid"].to_i}")
117
+ end
118
+ end
119
+ end
120
+
121
+ def cachable?(statement)
122
+ !%w[CREATE ALTER UPDATE INSERT DELETE].include?(statement.split.first.to_s.upcase)
123
+ end
124
+
125
+ protected
126
+
127
+ def select_all(statement, params = [])
128
+ statement = connection_model.send(:sanitize_sql_array, [statement] + params) if params.any?
129
+ connection_model.connection.select_all(statement)
130
+ end
131
+
132
+ # seperate from select_all to prevent mysql error
133
+ def execute(statement)
134
+ connection_model.connection.execute(statement)
135
+ end
136
+
137
+ def postgresql?
138
+ ["PostgreSQL", "PostGIS"].include?(adapter_name)
139
+ end
140
+
141
+ def redshift?
142
+ ["Redshift"].include?(adapter_name)
143
+ end
144
+
145
+ def mysql?
146
+ ["MySQL", "Mysql2", "Mysql2Spatial"].include?(adapter_name)
147
+ end
148
+
149
+ def sqlserver?
150
+ ["SQLServer", "tinytds", "mssql"].include?(adapter_name)
151
+ end
152
+
153
+ def snowflake?
154
+ data_source.adapter == "snowflake"
155
+ end
156
+
157
+ def adapter_name
158
+ # prevent bad data source from taking down queries/new
159
+ connection_model.connection.adapter_name rescue nil
160
+ end
161
+
162
+ def default_schema
163
+ @default_schema ||= begin
164
+ if postgresql? || redshift?
165
+ "public"
166
+ elsif sqlserver?
167
+ "dbo"
168
+ else
169
+ connection_model.connection_config[:database]
170
+ end
171
+ end
172
+ end
173
+
174
+ def add_schemas(query)
175
+ if settings["schemas"]
176
+ where = "table_schema IN (?)"
177
+ schemas = settings["schemas"]
178
+ else
179
+ where = "table_schema NOT IN (?)"
180
+ schemas = ["information_schema"]
181
+ schemas.map!(&:upcase) if snowflake?
182
+ schemas << "pg_catalog" if postgresql? || redshift?
183
+ end
184
+ connection_model.send(:sanitize_sql_array, ["#{query} WHERE #{where}", schemas])
185
+ end
186
+
187
+ def set_timeout(timeout)
188
+ if postgresql? || redshift?
189
+ execute("SET #{use_transaction? ? "LOCAL " : ""}statement_timeout = #{timeout.to_i * 1000}")
190
+ elsif mysql?
191
+ # use send as this method is private in Rails 4.2
192
+ mariadb = connection_model.connection.send(:mariadb?) rescue false
193
+ if mariadb
194
+ execute("SET max_statement_time = #{timeout.to_i * 1000}")
195
+ else
196
+ execute("SET max_execution_time = #{timeout.to_i * 1000}")
197
+ end
198
+ else
199
+ raise Blazer::TimeoutNotSupported, "Timeout not supported for #{adapter_name} adapter"
200
+ end
201
+ end
202
+
203
+ def use_transaction?
204
+ settings.key?("use_transaction") ? settings["use_transaction"] : true
205
+ end
206
+
207
+ def in_transaction
208
+ connection_model.connection_pool.with_connection do
209
+ if use_transaction?
210
+ connection_model.transaction do
211
+ yield
212
+ raise ActiveRecord::Rollback
213
+ end
214
+ else
215
+ yield
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end