blazer 1.8.0 → 1.8.2
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.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTING.md +37 -0
- data/ISSUE_TEMPLATE.md +7 -0
- data/README.md +66 -19
- data/app/assets/javascripts/blazer/Chart.js +8647 -6771
- data/app/assets/javascripts/blazer/queries.js +5 -2
- data/app/controllers/blazer/queries_controller.rb +1 -1
- data/app/models/blazer/audit.rb +1 -1
- data/app/models/blazer/check.rb +1 -1
- data/app/models/blazer/dashboard.rb +1 -1
- data/app/models/blazer/dashboard_query.rb +1 -1
- data/app/models/blazer/query.rb +2 -2
- data/app/models/blazer/record.rb +5 -0
- data/app/views/blazer/queries/_form.html.erb +1 -1
- data/app/views/blazer/queries/run.html.erb +2 -0
- data/app/views/blazer/queries/show.html.erb +1 -1
- data/blazer.gemspec +2 -1
- data/lib/blazer.rb +35 -5
- data/lib/blazer/adapters/athena_adapter.rb +128 -0
- data/lib/blazer/adapters/cassandra_adapter.rb +59 -0
- data/lib/blazer/adapters/druid_adapter.rb +67 -0
- data/lib/blazer/data_source.rb +18 -15
- data/lib/blazer/engine.rb +0 -15
- data/lib/blazer/version.rb +1 -1
- metadata +27 -6
@@ -52,6 +52,9 @@ function runQueryHelper(query) {
|
|
52
52
|
}).fail( function(jqXHR, textStatus, errorThrown) {
|
53
53
|
if (!query.canceled) {
|
54
54
|
var message = (typeof errorThrown === "string") ? errorThrown : errorThrown.message
|
55
|
+
if (!message) {
|
56
|
+
message = "An error occurred"
|
57
|
+
}
|
55
58
|
query.error(message)
|
56
59
|
}
|
57
60
|
queryComplete(query)
|
@@ -92,7 +95,7 @@ function cancelQuery(query) {
|
|
92
95
|
var path = Routes.cancel_queries_path()
|
93
96
|
var data = {run_id: query.run_id, data_source: query.data_source}
|
94
97
|
if (navigator.sendBeacon) {
|
95
|
-
navigator.sendBeacon(path
|
98
|
+
navigator.sendBeacon(path + "?" + $.param(csrfProtect(data)))
|
96
99
|
} else {
|
97
100
|
// TODO make sync
|
98
101
|
$.post(path, data)
|
@@ -103,5 +106,5 @@ function csrfProtect(payload) {
|
|
103
106
|
var param = $("meta[name=csrf-param]").attr("content")
|
104
107
|
var token = $("meta[name=csrf-token]").attr("content")
|
105
108
|
if (param && token) payload[param] = token
|
106
|
-
return
|
109
|
+
return payload
|
107
110
|
}
|
data/app/models/blazer/audit.rb
CHANGED
data/app/models/blazer/check.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Blazer
|
2
|
-
class Dashboard <
|
2
|
+
class Dashboard < Record
|
3
3
|
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
|
4
4
|
has_many :dashboard_queries, dependent: :destroy
|
5
5
|
has_many :queries, through: :dashboard_queries
|
data/app/models/blazer/query.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Blazer
|
2
|
-
class Query <
|
2
|
+
class Query < Record
|
3
3
|
belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
|
4
4
|
has_many :checks, dependent: :destroy
|
5
5
|
has_many :dashboard_queries, dependent: :destroy
|
@@ -19,7 +19,7 @@ module Blazer
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def editable?(user)
|
22
|
-
editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == creator
|
22
|
+
editable = !persisted? || (name.present? && name.first != "*" && name.first != "#") || user == try(:creator)
|
23
23
|
editable &&= Blazer.query_editable.call(self, user) if Blazer.query_editable
|
24
24
|
editable
|
25
25
|
end
|
@@ -66,7 +66,7 @@
|
|
66
66
|
|
67
67
|
<script>
|
68
68
|
<%= blazer_js_var "params", variable_params %>
|
69
|
-
<%= blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, v.preview_statement] }] %>
|
69
|
+
<%= blazer_js_var "previewStatement", Hash[Blazer.data_sources.map { |k, v| [k, (v.preview_statement rescue "")] }] %>
|
70
70
|
|
71
71
|
var app = new Vue({
|
72
72
|
el: "#app",
|
@@ -111,6 +111,8 @@
|
|
111
111
|
<div class="results-container">
|
112
112
|
<% if @columns == ["QUERY PLAN"] %>
|
113
113
|
<pre><code><%= @rows.map { |r| r[0] }.join("\n") %></code></pre>
|
114
|
+
<% elsif @columns == ["PLAN"] && @data_source.adapter == "druid" %>
|
115
|
+
<pre><code><%= @rows[0][0] %></code></pre>
|
114
116
|
<% else %>
|
115
117
|
<table class="table results-table" style="margin-bottom: 0;">
|
116
118
|
<thead>
|
@@ -62,7 +62,7 @@
|
|
62
62
|
</script>
|
63
63
|
<% end %>
|
64
64
|
|
65
|
-
<%
|
65
|
+
<% unless %w(mongodb elasticsearch).include?(Blazer.data_sources[@query.data_source].adapter) %>
|
66
66
|
<script>
|
67
67
|
// do not highlight really long queries
|
68
68
|
// this can lead to performance issues
|
data/blazer.gemspec
CHANGED
@@ -17,7 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "
|
20
|
+
spec.add_dependency "railties", ">= 4"
|
21
|
+
spec.add_dependency "activerecord", ">= 4"
|
21
22
|
spec.add_dependency "chartkick"
|
22
23
|
spec.add_dependency "safely_block", ">= 0.1.1"
|
23
24
|
|
data/lib/blazer.rb
CHANGED
@@ -7,8 +7,11 @@ require "blazer/data_source"
|
|
7
7
|
require "blazer/result"
|
8
8
|
require "blazer/run_statement"
|
9
9
|
require "blazer/adapters/base_adapter"
|
10
|
+
require "blazer/adapters/athena_adapter"
|
10
11
|
require "blazer/adapters/bigquery_adapter"
|
12
|
+
require "blazer/adapters/cassandra_adapter"
|
11
13
|
require "blazer/adapters/drill_adapter"
|
14
|
+
require "blazer/adapters/druid_adapter"
|
12
15
|
require "blazer/adapters/elasticsearch_adapter"
|
13
16
|
require "blazer/adapters/mongodb_adapter"
|
14
17
|
require "blazer/adapters/presto_adapter"
|
@@ -23,8 +26,8 @@ module Blazer
|
|
23
26
|
attr_accessor :audit
|
24
27
|
attr_reader :time_zone
|
25
28
|
attr_accessor :user_name
|
26
|
-
|
27
|
-
|
29
|
+
attr_writer :user_class
|
30
|
+
attr_writer :user_method
|
28
31
|
attr_accessor :before_action
|
29
32
|
attr_accessor :from_email
|
30
33
|
attr_accessor :cache
|
@@ -58,6 +61,23 @@ module Blazer
|
|
58
61
|
@time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
|
59
62
|
end
|
60
63
|
|
64
|
+
def self.user_class
|
65
|
+
if !defined?(@user_class)
|
66
|
+
@user_class = settings.key?("user_class") ? settings["user_class"] : (User.name rescue nil)
|
67
|
+
end
|
68
|
+
@user_class
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.user_method
|
72
|
+
if !defined?(@user_method)
|
73
|
+
@user_method = settings["user_method"]
|
74
|
+
if user_class
|
75
|
+
@user_method ||= "current_#{user_class.to_s.downcase.singularize}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
@user_method
|
79
|
+
end
|
80
|
+
|
61
81
|
def self.settings
|
62
82
|
@settings ||= begin
|
63
83
|
path = Rails.root.join("config", "blazer.yml").to_s
|
@@ -129,7 +149,14 @@ module Blazer
|
|
129
149
|
break
|
130
150
|
end
|
131
151
|
end
|
132
|
-
|
152
|
+
|
153
|
+
begin
|
154
|
+
check.reload # in case state has changed since job started
|
155
|
+
check.update_state(result)
|
156
|
+
rescue ActiveRecord::RecordNotFound
|
157
|
+
# check deleted
|
158
|
+
end
|
159
|
+
|
133
160
|
# TODO use proper logfmt
|
134
161
|
Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
|
135
162
|
|
@@ -166,9 +193,12 @@ module Blazer
|
|
166
193
|
end
|
167
194
|
end
|
168
195
|
|
169
|
-
Blazer.register_adapter "
|
196
|
+
Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
|
170
197
|
Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
|
198
|
+
Blazer.register_adapter "cassandra", Blazer::Adapters::CassandraAdapter
|
199
|
+
Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
|
200
|
+
Blazer.register_adapter "druid", Blazer::Adapters::DruidAdapter
|
171
201
|
Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
|
172
|
-
Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
|
173
202
|
Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
|
203
|
+
Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
|
174
204
|
Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Blazer
|
2
|
+
module Adapters
|
3
|
+
class AthenaAdapter < BaseAdapter
|
4
|
+
def run_statement(statement, comment)
|
5
|
+
require "digest/md5"
|
6
|
+
|
7
|
+
columns = []
|
8
|
+
rows = []
|
9
|
+
error = nil
|
10
|
+
|
11
|
+
begin
|
12
|
+
resp =
|
13
|
+
client.start_query_execution(
|
14
|
+
query_string: statement,
|
15
|
+
# use token so we fetch cached results after query is run
|
16
|
+
client_request_token: Digest::MD5.hexdigest(statement),
|
17
|
+
query_execution_context: {
|
18
|
+
database: database,
|
19
|
+
},
|
20
|
+
result_configuration: {
|
21
|
+
output_location: settings["output_location"]
|
22
|
+
}
|
23
|
+
)
|
24
|
+
query_execution_id = resp.query_execution_id
|
25
|
+
|
26
|
+
timeout = data_source.timeout || 300
|
27
|
+
stop_at = Time.now + timeout
|
28
|
+
resp = nil
|
29
|
+
|
30
|
+
begin
|
31
|
+
resp = client.get_query_results(
|
32
|
+
query_execution_id: query_execution_id
|
33
|
+
)
|
34
|
+
rescue Aws::Athena::Errors::InvalidRequestException => e
|
35
|
+
if e.message != "Query has not yet finished. Current state: RUNNING"
|
36
|
+
raise e
|
37
|
+
end
|
38
|
+
if Time.now < stop_at
|
39
|
+
sleep(3)
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if resp && resp.result_set
|
45
|
+
column_info = resp.result_set.result_set_metadata.column_info
|
46
|
+
columns = column_info.map(&:name)
|
47
|
+
column_types = column_info.map(&:type)
|
48
|
+
|
49
|
+
untyped_rows = []
|
50
|
+
|
51
|
+
# paginated
|
52
|
+
resp.each do |page|
|
53
|
+
untyped_rows.concat page.result_set.rows.map { |r| r.data.map(&:var_char_value) }
|
54
|
+
end
|
55
|
+
|
56
|
+
utc = ActiveSupport::TimeZone['Etc/UTC']
|
57
|
+
|
58
|
+
rows = untyped_rows[1..-1] || []
|
59
|
+
column_types.each_with_index do |ct, i|
|
60
|
+
# TODO more column_types
|
61
|
+
case ct
|
62
|
+
when "timestamp"
|
63
|
+
rows.each do |row|
|
64
|
+
row[i] = utc.parse(row[i])
|
65
|
+
end
|
66
|
+
when "date"
|
67
|
+
rows.each do |row|
|
68
|
+
row[i] = Date.parse(row[i])
|
69
|
+
end
|
70
|
+
when "bigint"
|
71
|
+
rows.each do |row|
|
72
|
+
row[i] = row[i].to_i
|
73
|
+
end
|
74
|
+
when "double"
|
75
|
+
rows.each do |row|
|
76
|
+
row[i] = row[i].to_f
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
elsif resp
|
81
|
+
error = fetch_error(query_execution_id)
|
82
|
+
else
|
83
|
+
error = Blazer::TIMEOUT_MESSAGE
|
84
|
+
end
|
85
|
+
rescue Aws::Athena::Errors::InvalidRequestException => e
|
86
|
+
error = e.message
|
87
|
+
if error == "Query did not finish successfully. Final query state: FAILED"
|
88
|
+
error = fetch_error(query_execution_id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
[columns, rows, error]
|
93
|
+
end
|
94
|
+
|
95
|
+
def tables
|
96
|
+
glue.get_tables(database_name: database).table_list.map(&:name).sort
|
97
|
+
end
|
98
|
+
|
99
|
+
def schema
|
100
|
+
glue.get_tables(database_name: database).table_list.map { |t| {table: t.name, columns: t.storage_descriptor.columns.map { |c| {name: c.name, data_type: c.type} }} }
|
101
|
+
end
|
102
|
+
|
103
|
+
def preview_statement
|
104
|
+
"SELECT * FROM {table} LIMIT 10"
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def database
|
110
|
+
@database ||= settings["database"] || "default"
|
111
|
+
end
|
112
|
+
|
113
|
+
def fetch_error(query_execution_id)
|
114
|
+
client.get_query_execution(
|
115
|
+
query_execution_id: query_execution_id
|
116
|
+
).query_execution.status.state_change_reason
|
117
|
+
end
|
118
|
+
|
119
|
+
def client
|
120
|
+
@client ||= Aws::Athena::Client.new
|
121
|
+
end
|
122
|
+
|
123
|
+
def glue
|
124
|
+
@glue ||= Aws::Glue::Client.new
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Blazer
|
2
|
+
module Adapters
|
3
|
+
class CassandraAdapter < BaseAdapter
|
4
|
+
def run_statement(statement, comment)
|
5
|
+
columns = []
|
6
|
+
rows = []
|
7
|
+
error = nil
|
8
|
+
|
9
|
+
begin
|
10
|
+
response = session.execute("#{statement} /*#{comment}*/")
|
11
|
+
rows = response.map { |r| r.values }
|
12
|
+
columns = rows.any? ? response.first.keys : []
|
13
|
+
rescue => e
|
14
|
+
error = e.message
|
15
|
+
end
|
16
|
+
|
17
|
+
[columns, rows, error]
|
18
|
+
end
|
19
|
+
|
20
|
+
def tables
|
21
|
+
session.execute("SELECT table_name FROM system_schema.tables WHERE keyspace_name = '#{keyspace}'").map { |r| r["table_name"] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def schema
|
25
|
+
result = session.execute("SELECT keyspace_name, table_name, column_name, type, position FROM system_schema.columns WHERE keyspace_name = '#{keyspace}'")
|
26
|
+
result.map(&:values).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]} }} }
|
27
|
+
end
|
28
|
+
|
29
|
+
def preview_statement
|
30
|
+
"SELECT * FROM {table} LIMIT 10"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def cluster
|
36
|
+
@cluster ||= begin
|
37
|
+
require "cassandra"
|
38
|
+
options = {hosts: [uri.host]}
|
39
|
+
options[:port] = uri.port if uri.port
|
40
|
+
options[:username] = uri.user if uri.user
|
41
|
+
options[:password] = uri.password if uri.password
|
42
|
+
::Cassandra.cluster(options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def session
|
47
|
+
@session ||= cluster.connect(keyspace)
|
48
|
+
end
|
49
|
+
|
50
|
+
def uri
|
51
|
+
@uri ||= URI.parse(data_source.settings["url"])
|
52
|
+
end
|
53
|
+
|
54
|
+
def keyspace
|
55
|
+
@keyspace ||= uri.path[1..-1]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Blazer
|
2
|
+
module Adapters
|
3
|
+
class DruidAdapter < BaseAdapter
|
4
|
+
TIMESTAMP_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\z/
|
5
|
+
|
6
|
+
def run_statement(statement, comment)
|
7
|
+
columns = []
|
8
|
+
rows = []
|
9
|
+
error = nil
|
10
|
+
|
11
|
+
header = {"Content-Type" => "application/json", "Accept" => "application/json"}
|
12
|
+
timeout = data_source.timeout ? data_source.timeout.to_i : 300
|
13
|
+
data = {
|
14
|
+
query: statement,
|
15
|
+
context: {
|
16
|
+
timeout: timeout * 1000
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
uri = URI.parse("#{settings["url"]}/druid/v2/sql/")
|
21
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
22
|
+
http.read_timeout = timeout
|
23
|
+
|
24
|
+
begin
|
25
|
+
response = JSON.parse(http.post(uri.request_uri, data.to_json, header).body)
|
26
|
+
if response.is_a?(Hash)
|
27
|
+
error = response["errorMessage"] || "Unknown error: #{response.inspect}"
|
28
|
+
if error.include?("timed out")
|
29
|
+
error = Blazer::TIMEOUT_MESSAGE
|
30
|
+
end
|
31
|
+
else
|
32
|
+
columns = (response.first || {}).keys
|
33
|
+
rows = response.map { |r| r.values }
|
34
|
+
|
35
|
+
# Druid doesn't return column types
|
36
|
+
# and no timestamp type in JSON
|
37
|
+
rows.each do |row|
|
38
|
+
row.each_with_index do |v, i|
|
39
|
+
if v.is_a?(String) && TIMESTAMP_REGEX.match(v)
|
40
|
+
row[i] = Time.parse(v)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue => e
|
46
|
+
error = e.message
|
47
|
+
end
|
48
|
+
|
49
|
+
[columns, rows, error]
|
50
|
+
end
|
51
|
+
|
52
|
+
def tables
|
53
|
+
result = data_source.run_statement("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA') ORDER BY TABLE_NAME")
|
54
|
+
result.rows.map(&:first)
|
55
|
+
end
|
56
|
+
|
57
|
+
def schema
|
58
|
+
result = data_source.run_statement("SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA') ORDER BY 1, 2")
|
59
|
+
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]} }} }
|
60
|
+
end
|
61
|
+
|
62
|
+
def preview_statement
|
63
|
+
"SELECT * FROM {table} LIMIT 10"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|