sql-jarvis 1.8.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 (98) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/CHANGELOG.md +228 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +775 -0
  7. data/Rakefile +1 -0
  8. data/app/assets/fonts/blazer/glyphicons-halflings-regular.eot +0 -0
  9. data/app/assets/fonts/blazer/glyphicons-halflings-regular.svg +288 -0
  10. data/app/assets/fonts/blazer/glyphicons-halflings-regular.ttf +0 -0
  11. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff +0 -0
  12. data/app/assets/fonts/blazer/glyphicons-halflings-regular.woff2 +0 -0
  13. data/app/assets/javascripts/blazer/Chart.js +14145 -0
  14. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  15. data/app/assets/javascripts/blazer/ace.js +6 -0
  16. data/app/assets/javascripts/blazer/ace/ace.js +11 -0
  17. data/app/assets/javascripts/blazer/ace/ext-language_tools.js +5 -0
  18. data/app/assets/javascripts/blazer/ace/mode-sql.js +1 -0
  19. data/app/assets/javascripts/blazer/ace/snippets/sql.js +1 -0
  20. data/app/assets/javascripts/blazer/ace/snippets/text.js +1 -0
  21. data/app/assets/javascripts/blazer/ace/theme-twilight.js +1 -0
  22. data/app/assets/javascripts/blazer/application.js +79 -0
  23. data/app/assets/javascripts/blazer/bootstrap.js +2366 -0
  24. data/app/assets/javascripts/blazer/chartkick.js +1693 -0
  25. data/app/assets/javascripts/blazer/daterangepicker.js +1505 -0
  26. data/app/assets/javascripts/blazer/fuzzysearch.js +24 -0
  27. data/app/assets/javascripts/blazer/highlight.pack.js +1 -0
  28. data/app/assets/javascripts/blazer/jquery.js +10308 -0
  29. data/app/assets/javascripts/blazer/jquery.stickytableheaders.js +263 -0
  30. data/app/assets/javascripts/blazer/jquery_ujs.js +469 -0
  31. data/app/assets/javascripts/blazer/moment-timezone.js +1007 -0
  32. data/app/assets/javascripts/blazer/moment.js +3043 -0
  33. data/app/assets/javascripts/blazer/queries.js +110 -0
  34. data/app/assets/javascripts/blazer/routes.js +23 -0
  35. data/app/assets/javascripts/blazer/selectize.js +3667 -0
  36. data/app/assets/javascripts/blazer/stupidtable.js +114 -0
  37. data/app/assets/javascripts/blazer/vue.js +7515 -0
  38. data/app/assets/stylesheets/blazer/application.css +198 -0
  39. data/app/assets/stylesheets/blazer/bootstrap.css.erb +6202 -0
  40. data/app/assets/stylesheets/blazer/daterangepicker-bs3.css +375 -0
  41. data/app/assets/stylesheets/blazer/github.css +125 -0
  42. data/app/assets/stylesheets/blazer/selectize.default.css +387 -0
  43. data/app/controllers/blazer/base_controller.rb +103 -0
  44. data/app/controllers/blazer/checks_controller.rb +56 -0
  45. data/app/controllers/blazer/dashboards_controller.rb +105 -0
  46. data/app/controllers/blazer/queries_controller.rb +325 -0
  47. data/app/helpers/blazer/base_helper.rb +57 -0
  48. data/app/mailers/blazer/check_mailer.rb +27 -0
  49. data/app/models/blazer/audit.rb +6 -0
  50. data/app/models/blazer/check.rb +95 -0
  51. data/app/models/blazer/connection.rb +5 -0
  52. data/app/models/blazer/dashboard.rb +13 -0
  53. data/app/models/blazer/dashboard_query.rb +9 -0
  54. data/app/models/blazer/query.rb +31 -0
  55. data/app/models/blazer/record.rb +5 -0
  56. data/app/views/blazer/_nav.html.erb +16 -0
  57. data/app/views/blazer/_variables.html.erb +102 -0
  58. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  59. data/app/views/blazer/check_mailer/state_change.html.erb +47 -0
  60. data/app/views/blazer/checks/_form.html.erb +71 -0
  61. data/app/views/blazer/checks/edit.html.erb +1 -0
  62. data/app/views/blazer/checks/index.html.erb +40 -0
  63. data/app/views/blazer/checks/new.html.erb +1 -0
  64. data/app/views/blazer/dashboards/_form.html.erb +76 -0
  65. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  66. data/app/views/blazer/dashboards/new.html.erb +1 -0
  67. data/app/views/blazer/dashboards/show.html.erb +47 -0
  68. data/app/views/blazer/queries/_form.html.erb +240 -0
  69. data/app/views/blazer/queries/edit.html.erb +2 -0
  70. data/app/views/blazer/queries/home.html.erb +152 -0
  71. data/app/views/blazer/queries/new.html.erb +2 -0
  72. data/app/views/blazer/queries/run.html.erb +163 -0
  73. data/app/views/blazer/queries/schema.html.erb +18 -0
  74. data/app/views/blazer/queries/show.html.erb +73 -0
  75. data/app/views/layouts/blazer/application.html.erb +24 -0
  76. data/blazer.gemspec +26 -0
  77. data/config/routes.rb +16 -0
  78. data/lib/blazer.rb +185 -0
  79. data/lib/blazer/adapters/athena_adapter.rb +128 -0
  80. data/lib/blazer/adapters/base_adapter.rb +53 -0
  81. data/lib/blazer/adapters/bigquery_adapter.rb +67 -0
  82. data/lib/blazer/adapters/drill_adapter.rb +28 -0
  83. data/lib/blazer/adapters/elasticsearch_adapter.rb +49 -0
  84. data/lib/blazer/adapters/mongodb_adapter.rb +39 -0
  85. data/lib/blazer/adapters/presto_adapter.rb +45 -0
  86. data/lib/blazer/adapters/sql_adapter.rb +182 -0
  87. data/lib/blazer/data_source.rb +193 -0
  88. data/lib/blazer/detect_anomalies.R +19 -0
  89. data/lib/blazer/engine.rb +47 -0
  90. data/lib/blazer/result.rb +170 -0
  91. data/lib/blazer/run_statement.rb +40 -0
  92. data/lib/blazer/run_statement_job.rb +21 -0
  93. data/lib/blazer/version.rb +3 -0
  94. data/lib/generators/blazer/install_generator.rb +39 -0
  95. data/lib/generators/blazer/templates/config.yml +62 -0
  96. data/lib/generators/blazer/templates/install.rb +45 -0
  97. data/lib/tasks/blazer.rake +10 -0
  98. metadata +211 -0
@@ -0,0 +1,2 @@
1
+ <% blazer_title "New Query" %>
2
+ <%= render partial: "form" %>
@@ -0,0 +1,163 @@
1
+ <% if @error %>
2
+ <div class="alert alert-danger"><%= @error.first(200) %></div>
3
+ <% elsif !@success %>
4
+ <% if @only_chart %>
5
+ <p class="text-muted">Select variables</p>
6
+ <% else %>
7
+ <div class="alert alert-info">Can’t preview queries with variables...yet!</div>
8
+ <% end %>
9
+ <% else %>
10
+ <% unless @only_chart %>
11
+ <% if @cached_at || @just_cached %>
12
+ <p class="text-muted" style="float: right;">
13
+ <% if @cached_at %>
14
+ Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
15
+ <% elsif !params[:data_source] %>
16
+ Cached just now
17
+ <% if @data_source.cache_mode == "slow" %>
18
+ (over <%= "%g" % @data_source.cache_slow_threshold %>s)
19
+ <% end %>
20
+ <% end %>
21
+
22
+ <% if @query && !params[:data_source] %>
23
+ <%= link_to "Refresh", refresh_query_path(@query, variable_params), method: :post %>
24
+ <% end %>
25
+ </p>
26
+ <% end %>
27
+ <p class="text-muted">
28
+ <%= pluralize(@rows.size, "row") %>
29
+
30
+ <% @checks.select(&:state).each do |check| %>
31
+ &middot; <small class="check-state <%= check.state.parameterize.gsub("-", "_") %>"><%= link_to check.state.upcase, edit_check_path(check) %></small>
32
+ <% if check.try(:message) %>
33
+ &middot; <%= check.message %>
34
+ <% end %>
35
+ <% end %>
36
+ </p>
37
+ <% end %>
38
+ <% if @rows.any? %>
39
+ <% values = @rows.first %>
40
+ <% chart_id = SecureRandom.hex %>
41
+ <% column_types = @result.column_types %>
42
+ <% chart_type = @result.chart_type %>
43
+ <% chart_options = {id: chart_id, min: nil} %>
44
+ <% series_library = {} %>
45
+ <% target_index = @columns.index { |k| k.downcase == "target" } %>
46
+ <% if target_index %>
47
+ <% series_library[target_index - 1] = {pointStyle: "line", hitRadius: 5, borderColor: "#109618", pointBackgroundColor: "#109618", backgroundColor: "#109618"} %>
48
+ <% end %>
49
+ <% if blazer_maps? && @markers.any? %>
50
+ <div id="map" style="height: <%= @only_chart ? 300 : 500 %>px;"></div>
51
+ <script>
52
+ L.mapbox.accessToken = '<%= Blazer.mapbox_access_token %>';
53
+ var map = L.mapbox.map('map', 'ankane.ioo8nki0');
54
+ <%= blazer_js_var "markers", @markers %>
55
+ var featureLayer = L.mapbox.featureLayer().addTo(map);
56
+ var geojson = [];
57
+ for (var i = 0; i < markers.length; i++) {
58
+ var marker = markers[i];
59
+ geojson.push({
60
+ type: 'Feature',
61
+ geometry: {
62
+ type: 'Point',
63
+ coordinates: [
64
+ marker.longitude,
65
+ marker.latitude
66
+ ]
67
+ },
68
+ properties: {
69
+ description: marker.title,
70
+ 'marker-color': '#f86767',
71
+ 'marker-size': 'medium'
72
+ }
73
+ });
74
+ }
75
+ featureLayer.setGeoJSON(geojson);
76
+ map.fitBounds(featureLayer.getBounds());
77
+ </script>
78
+ <% elsif chart_type == "line" %>
79
+ <%= line_chart @columns[1..-1].each_with_index.map{ |k, i| {name: blazer_series_name(k), data: @rows.map{ |r| [r[0], r[i + 1]] }, library: series_library[i]} }, chart_options %>
80
+ <% elsif chart_type == "line2" %>
81
+ <%= line_chart @rows.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.map { |v2| [v2[0], v2[2]] }, library: series_library[i]} }, chart_options %>
82
+ <% elsif chart_type == "bar" %>
83
+ <%= column_chart (values.size - 1).times.map { |i| name = @columns[i + 1]; {name: blazer_series_name(name), data: @rows.first(20).map { |r| [(@boom[@columns[0]] || {})[r[0].to_s] || r[0], r[i + 1]] } } }, id: chart_id %>
84
+ <% elsif chart_type == "bar2" %>
85
+ <% first_20 = @rows.group_by { |r| r[0] }.values.first(20).flatten(1) %>
86
+ <% labels = first_20.map { |r| r[0] }.uniq %>
87
+ <% series = first_20.map { |r| r[1] }.uniq %>
88
+ <% labels.each do |l| %>
89
+ <% series.each do |s| %>
90
+ <% first_20 << [l, s, 0] unless first_20.find { |r| r[0] == l && r[1] == s } %>
91
+ <% end %>
92
+ <% end %>
93
+ <%= column_chart first_20.group_by { |r| v = r[1]; (@boom[@columns[1]] || {})[v.to_s] || v }.each_with_index.map { |(name, v), i| {name: blazer_series_name(name), data: v.sort_by { |r2| labels.index(r2[0]) }.map { |v2| v3 = v2[0]; [(@boom[@columns[0]] || {})[v3.to_s] || v3, v2[2]] }} }, id: chart_id %>
94
+ <% elsif chart_type == "scatter" %>
95
+ <%= scatter_chart @rows, xtitle: @columns[0], ytitle: @columns[1], id: chart_id %>
96
+ <% elsif @only_chart %>
97
+ <% if @rows.size == 1 && @rows.first.size == 1 %>
98
+ <% v = @rows.first.first %>
99
+ <% if v.is_a?(String) && v == "" %>
100
+ <div class="text-muted">empty string</div>
101
+ <% else %>
102
+ <p style="font-size: 160px;"><%= blazer_format_value(@columns.first, v) %></p>
103
+ <% end %>
104
+ <% else %>
105
+ <% @no_chart = true %>
106
+ <% end %>
107
+ <% end %>
108
+
109
+ <% unless @only_chart && !@no_chart %>
110
+ <% header_width = 100 / @columns.size.to_f %>
111
+ <div class="results-container">
112
+ <% if @columns == ["QUERY PLAN"] %>
113
+ <pre><code><%= @rows.map { |r| r[0] }.join("\n") %></code></pre>
114
+ <% else %>
115
+ <table class="table results-table" style="margin-bottom: 0;">
116
+ <thead>
117
+ <tr>
118
+ <% @columns.each_with_index do |key, i| %>
119
+ <% type = @column_types[i] %>
120
+ <th style="width: <%= header_width %>%;" data-sort="<%= type %>">
121
+ <div style="min-width: <%= @min_width_types.include?(i) ? 180 : 60 %>px;">
122
+ <%= key %>
123
+ </div>
124
+ </th>
125
+ <% end %>
126
+ </tr>
127
+ </thead>
128
+ <tbody>
129
+ <% @rows.each do |row| %>
130
+ <tr>
131
+ <% row.each_with_index do |v, i| %>
132
+ <% k = @columns[i] %>
133
+ <td>
134
+ <% if v.is_a?(Time) %>
135
+ <% v = blazer_time_value(@data_source, k, v) %>
136
+ <% end %>
137
+
138
+ <% unless v.nil? %>
139
+ <% if v.is_a?(String) && v == "" %>
140
+ <div class="text-muted">empty string</div>
141
+ <% elsif @linked_columns[k] %>
142
+ <%= link_to blazer_format_value(k, v), @linked_columns[k].gsub("{value}", u(v.to_s)), target: "_blank" %>
143
+ <% else %>
144
+ <%= blazer_format_value(k, v) %>
145
+ <% end %>
146
+ <% end %>
147
+
148
+ <% if v2 = (@boom[k] || {})[v.nil? ? v : v.to_s] %>
149
+ <div class="text-muted"><%= v2 %></div>
150
+ <% end %>
151
+ </td>
152
+ <% end %>
153
+ </tr>
154
+ <% end %>
155
+ </tbody>
156
+ </table>
157
+ <% end %>
158
+ </div>
159
+ <% end %>
160
+ <% elsif @only_chart %>
161
+ <p class="text-muted">No rows</p>
162
+ <% end %>
163
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <% @schema.each do |table| %>
2
+ <h4>
3
+ <%= table[:table] %>
4
+ <% if table[:schema] != "public" %>
5
+ <small><%= table[:schema] %></small>
6
+ <% end %>
7
+ </h4>
8
+ <table class="table" style="max-width: 500px;">
9
+ <tbody>
10
+ <% table[:columns].each do |column| %>
11
+ <tr>
12
+ <td style="width: 60%;"><%= column[:name] %></td>
13
+ <td class="text-muted"><%= column[:data_type] %></td>
14
+ </tr>
15
+ <% end %>
16
+ </tbody>
17
+ </table>
18
+ <% end %>
@@ -0,0 +1,73 @@
1
+ <% blazer_title @query.name %>
2
+
3
+ <div class="topbar">
4
+ <div class="container">
5
+ <div class="row" style="padding-top: 13px;">
6
+ <div class="col-sm-9">
7
+ <%= render partial: "blazer/nav" %>
8
+ <h3 style="margin: 0; line-height: 34px; display: inline;">
9
+ <%= @query.name %>
10
+ </h3>
11
+ </div>
12
+ <div class="col-sm-3 text-right">
13
+ <%= link_to "Edit", edit_query_path(@query, variable_params), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %>
14
+ <%= link_to "Fork", new_query_path(variable_params.merge(fork_query_id: @query.id, data_source: @query.data_source, name: @query.name)), class: "btn btn-info" %>
15
+
16
+ <% if !@error && @success %>
17
+ <%= button_to "Download", run_queries_path(query_id: @query.id, format: "csv"), params: {statement: @statement}, class: "btn btn-primary" %>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div style="margin-bottom: 60px;"></div>
25
+
26
+ <% if @sql_errors.any? %>
27
+ <div class="alert alert-danger">
28
+ <ul>
29
+ <% @sql_errors.each do |message| %>
30
+ <li><%= message %></li>
31
+ <% end %>
32
+ </ul>
33
+ </div>
34
+ <% end %>
35
+
36
+ <% if @query.description.present? %>
37
+ <p><%= @query.description %></p>
38
+ <% end %>
39
+
40
+ <%= render partial: "blazer/variables", locals: {action: query_path(@query)} %>
41
+
42
+ <pre id="code"><code><%= @statement %></code></pre>
43
+
44
+ <% if @success %>
45
+ <div id="results">
46
+ <p class="text-muted">Loading...</p>
47
+ </div>
48
+
49
+ <script>
50
+ function showRun(data) {
51
+ $("#results").html(data)
52
+ $("#results table").stupidtable().stickyTableHeaders({fixedOffset: 60})
53
+ }
54
+
55
+ function showError(message) {
56
+ $("#results").addClass("query-error").html(message)
57
+ }
58
+
59
+ <%= blazer_js_var "data", variable_params.merge(statement: @statement, query_id: @query.id) %>
60
+
61
+ runQuery(data, showRun, showError)
62
+ </script>
63
+ <% end %>
64
+
65
+ <% if %w[sql presto drill bigquery athena].include?(Blazer.data_sources[@query.data_source].adapter) %>
66
+ <script>
67
+ // do not highlight really long queries
68
+ // this can lead to performance issues
69
+ if ($("code").text().length < 10000) {
70
+ hljs.highlightBlock(document.getElementById("code"));
71
+ }
72
+ </script>
73
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= blazer_title ? blazer_title : "Blazer" %></title>
5
+
6
+ <meta charset="utf-8" />
7
+
8
+ <%= stylesheet_link_tag "blazer/application" %>
9
+ <%= javascript_include_tag "blazer/application" %>
10
+ <script>
11
+ <%= blazer_js_var "rootPath", root_path %>
12
+ </script>
13
+ <% if blazer_maps? %>
14
+ <%= stylesheet_link_tag "https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css" %>
15
+ <%= javascript_include_tag "https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js" %>
16
+ <% end %>
17
+ <%= csrf_meta_tags %>
18
+ </head>
19
+ <body>
20
+ <div class="container">
21
+ <%= yield %>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "blazer/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sql-jarvis"
8
+ spec.version = Blazer::VERSION
9
+ spec.authors = ["ThanhKhoaIT"]
10
+ spec.email = ["thanhkhoait@gmail.com"]
11
+ spec.summary = "Fork from ankane! Explore your data with SQL. Easily create charts and dashboards, and share them with your team."
12
+ spec.homepage = "https://github.com/ThanhKhoaIT/blazer"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "rails", ">= 4"
21
+ spec.add_dependency "chartkick"
22
+ spec.add_dependency "safely_block", ">= 0.1.1"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ end
@@ -0,0 +1,16 @@
1
+ Blazer::Engine.routes.draw do
2
+ resources :queries do
3
+ post :run, on: :collection # err on the side of caution
4
+ post :cancel, on: :collection
5
+ post :refresh, on: :member
6
+ get :tables, on: :collection
7
+ get :schema, on: :collection
8
+ end
9
+ resources :checks, except: [:show] do
10
+ get :run, on: :member
11
+ end
12
+ resources :dashboards do
13
+ post :refresh, on: :member
14
+ end
15
+ root to: "queries#home"
16
+ end
@@ -0,0 +1,185 @@
1
+ require "csv"
2
+ require "yaml"
3
+ require "chartkick"
4
+ require "safely/core"
5
+ require "blazer/version"
6
+ require "blazer/data_source"
7
+ require "blazer/result"
8
+ require "blazer/run_statement"
9
+ require "blazer/adapters/base_adapter"
10
+ require "blazer/adapters/athena_adapter"
11
+ require "blazer/adapters/bigquery_adapter"
12
+ require "blazer/adapters/drill_adapter"
13
+ require "blazer/adapters/elasticsearch_adapter"
14
+ require "blazer/adapters/mongodb_adapter"
15
+ require "blazer/adapters/presto_adapter"
16
+ require "blazer/adapters/sql_adapter"
17
+ require "blazer/engine"
18
+
19
+ module Blazer
20
+ class Error < StandardError; end
21
+ class TimeoutNotSupported < Error; end
22
+
23
+ class << self
24
+ attr_accessor :audit
25
+ attr_reader :time_zone
26
+ attr_accessor :user_name
27
+ attr_accessor :user_class
28
+ attr_accessor :user_method
29
+ attr_accessor :before_action
30
+ attr_accessor :from_email
31
+ attr_accessor :cache
32
+ attr_accessor :transform_statement
33
+ attr_accessor :check_schedules
34
+ attr_accessor :mapbox_access_token
35
+ attr_accessor :anomaly_checks
36
+ attr_accessor :async
37
+ attr_accessor :images
38
+ attr_accessor :query_editable
39
+ end
40
+ self.audit = true
41
+ self.user_name = :name
42
+ self.check_schedules = ["5 minutes", "1 hour", "1 day"]
43
+ self.mapbox_access_token = nil
44
+ self.anomaly_checks = false
45
+ self.async = false
46
+ self.images = false
47
+
48
+ TIMEOUT_MESSAGE = "Query timed out :("
49
+ TIMEOUT_ERRORS = [
50
+ "canceling statement due to statement timeout", # postgres
51
+ "canceling statement due to conflict with recovery", # postgres
52
+ "cancelled on user's request", # redshift
53
+ "canceled on user's request", # redshift
54
+ "system requested abort", # redshift
55
+ "maximum statement execution time exceeded" # mysql
56
+ ]
57
+ BELONGS_TO_OPTIONAL = {}
58
+ BELONGS_TO_OPTIONAL[:optional] = true if Rails::VERSION::MAJOR >= 5
59
+
60
+ def self.time_zone=(time_zone)
61
+ @time_zone = time_zone.is_a?(ActiveSupport::TimeZone) ? time_zone : ActiveSupport::TimeZone[time_zone.to_s]
62
+ end
63
+
64
+ def self.settings
65
+ @settings ||= begin
66
+ path = Rails.root.join("config", "blazer.yml").to_s
67
+ if File.exist?(path)
68
+ YAML.load(ERB.new(File.read(path)).result)
69
+ else
70
+ {}
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.data_sources
76
+ @data_sources ||= begin
77
+ ds = Hash[
78
+ settings["data_sources"].map do |id, s|
79
+ [id, Blazer::DataSource.new(id, s)]
80
+ end
81
+ ]
82
+ ds.default = ds.values.first
83
+ ds
84
+
85
+ # TODO Blazer 2.0
86
+ # ds2 = Hash.new { |hash, key| raise Blazer::Error, "Unknown data source: #{key}" }
87
+ # ds.each do |k, v|
88
+ # ds2[k] = v
89
+ # end
90
+ # ds2
91
+ end
92
+ end
93
+
94
+ def self.extract_vars(statement)
95
+ # strip commented out lines
96
+ # and regex {1} or {1,2}
97
+ statement.gsub(/\-\-.+/, "").gsub(/\/\*.+\*\//m, "").scan(/\{\w*?\}/i).map { |v| v[1...-1] }.reject { |v| /\A\d+(\,\d+)?\z/.match(v) || v.empty? }.uniq
98
+ end
99
+
100
+ def self.run_checks(schedule: nil)
101
+ checks = Blazer::Check.includes(:query)
102
+ checks = checks.where(schedule: schedule) if schedule
103
+ checks.find_each do |check|
104
+ next if check.state == "disabled"
105
+ Safely.safely { run_check(check) }
106
+ end
107
+ end
108
+
109
+ def self.run_check(check)
110
+ rows = nil
111
+ error = nil
112
+ tries = 1
113
+
114
+ ActiveSupport::Notifications.instrument("run_check.blazer", check_id: check.id, query_id: check.query.id, state_was: check.state) do |instrument|
115
+ # try 3 times on timeout errors
116
+ data_source = data_sources[check.query.data_source]
117
+ statement = check.query.statement
118
+ Blazer.transform_statement.call(data_source, statement) if Blazer.transform_statement
119
+
120
+ while tries <= 3
121
+ result = data_source.run_statement(statement, refresh_cache: true, check: check, query: check.query)
122
+ if result.timed_out?
123
+ Rails.logger.info "[blazer timeout] query=#{check.query.name}"
124
+ tries += 1
125
+ sleep(10)
126
+ elsif result.error.to_s.start_with?("PG::ConnectionBad")
127
+ data_source.reconnect
128
+ Rails.logger.info "[blazer reconnect] query=#{check.query.name}"
129
+ tries += 1
130
+ sleep(10)
131
+ else
132
+ break
133
+ end
134
+ end
135
+
136
+ begin
137
+ check.reload # in case state has changed since job started
138
+ check.update_state(result)
139
+ rescue ActiveRecord::RecordNotFound
140
+ # check deleted
141
+ end
142
+
143
+ # TODO use proper logfmt
144
+ Rails.logger.info "[blazer check] query=#{check.query.name} state=#{check.state} rows=#{result.rows.try(:size)} error=#{result.error}"
145
+
146
+ instrument[:statement] = statement
147
+ instrument[:data_source] = data_source
148
+ instrument[:state] = check.state
149
+ instrument[:rows] = result.rows.try(:size)
150
+ instrument[:error] = result.error
151
+ instrument[:tries] = tries
152
+ end
153
+ end
154
+
155
+ def self.send_failing_checks
156
+ emails = {}
157
+ Blazer::Check.includes(:query).where(state: ["failing", "error", "timed out", "disabled"]).find_each do |check|
158
+ check.split_emails.each do |email|
159
+ (emails[email] ||= []) << check
160
+ end
161
+ end
162
+
163
+ emails.each do |email, checks|
164
+ Safely.safely do
165
+ Blazer::CheckMailer.failing_checks(email, checks).deliver_now
166
+ end
167
+ end
168
+ end
169
+
170
+ def self.adapters
171
+ @adapters ||= {}
172
+ end
173
+
174
+ def self.register_adapter(name, adapter)
175
+ adapters[name] = adapter
176
+ end
177
+ end
178
+
179
+ Blazer.register_adapter "drill", Blazer::Adapters::DrillAdapter
180
+ Blazer.register_adapter "bigquery", Blazer::Adapters::BigQueryAdapter
181
+ Blazer.register_adapter "athena", Blazer::Adapters::AthenaAdapter
182
+ Blazer.register_adapter "elasticsearch", Blazer::Adapters::ElasticsearchAdapter
183
+ Blazer.register_adapter "mongodb", Blazer::Adapters::MongodbAdapter
184
+ Blazer.register_adapter "presto", Blazer::Adapters::PrestoAdapter
185
+ Blazer.register_adapter "sql", Blazer::Adapters::SqlAdapter