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.

@@ -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, csrfProtect(data))
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 new Blob([JSON.stringify(payload)], {type : "application/json; charset=utf-8"})
109
+ return payload
107
110
  }
@@ -183,7 +183,7 @@ module Blazer
183
183
 
184
184
  def cancel
185
185
  Blazer.data_sources[params[:data_source]].cancel(blazer_run_id)
186
- render json: {}
186
+ head :ok
187
187
  end
188
188
 
189
189
  private
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class Audit < ActiveRecord::Base
2
+ class Audit < Record
3
3
  belongs_to :user, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s)
4
4
  belongs_to :query, Blazer::BELONGS_TO_OPTIONAL
5
5
  end
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class Check < ActiveRecord::Base
2
+ class Check < Record
3
3
  belongs_to :creator, Blazer::BELONGS_TO_OPTIONAL.merge(class_name: Blazer.user_class.to_s) if Blazer.user_class
4
4
  belongs_to :query
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class Dashboard < ActiveRecord::Base
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
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class DashboardQuery < ActiveRecord::Base
2
+ class DashboardQuery < Record
3
3
  belongs_to :dashboard
4
4
  belongs_to :query
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Blazer
2
- class Query < ActiveRecord::Base
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
@@ -0,0 +1,5 @@
1
+ module Blazer
2
+ class Record < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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
- <% if %w[sql presto drill bigquery].include?(Blazer.data_sources[@query.data_source].adapter) %>
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 "rails"
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
- attr_accessor :user_class
27
- attr_accessor :user_method
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
- check.update_state(result)
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 "drill", Blazer::Adapters::DrillAdapter
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