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,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", "salesforce"].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,43 @@
1
+ module Blazer
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Blazer
4
+
5
+ initializer "blazer" do |app|
6
+ if defined?(Sprockets) && Sprockets::VERSION >= "4"
7
+ app.config.assets.precompile << "blazer/application.js"
8
+ app.config.assets.precompile << "blazer/application.css"
9
+ app.config.assets.precompile << "blazer/glyphicons-halflings-regular.eot"
10
+ app.config.assets.precompile << "blazer/glyphicons-halflings-regular.svg"
11
+ app.config.assets.precompile << "blazer/glyphicons-halflings-regular.ttf"
12
+ app.config.assets.precompile << "blazer/glyphicons-halflings-regular.woff"
13
+ app.config.assets.precompile << "blazer/glyphicons-halflings-regular.woff2"
14
+ app.config.assets.precompile << "blazer/favicon.png"
15
+ else
16
+ # use a proc instead of a string
17
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/application\.(js|css)\z/ }
18
+ app.config.assets.precompile << proc { |path| path =~ /\Ablazer\/.+\.(eot|svg|ttf|woff|woff2)\z/ }
19
+ app.config.assets.precompile << proc { |path| path == "blazer/favicon.png" }
20
+ end
21
+
22
+ Blazer.time_zone ||= Blazer.settings["time_zone"] || Time.zone
23
+ Blazer.audit = Blazer.settings.key?("audit") ? Blazer.settings["audit"] : true
24
+ Blazer.user_name = Blazer.settings["user_name"] if Blazer.settings["user_name"]
25
+ Blazer.from_email = Blazer.settings["from_email"] if Blazer.settings["from_email"]
26
+ Blazer.before_action = Blazer.settings["before_action_method"] if Blazer.settings["before_action_method"]
27
+ Blazer.check_schedules = Blazer.settings["check_schedules"] if Blazer.settings.key?("check_schedules")
28
+ Blazer.cache ||= Rails.cache
29
+
30
+ Blazer.anomaly_checks = Blazer.settings["anomaly_checks"] || false
31
+ Blazer.forecasting = Blazer.settings["forecasting"] || false
32
+ Blazer.async = Blazer.settings["async"] || false
33
+ if Blazer.async
34
+ require "blazer/run_statement_job"
35
+ end
36
+
37
+ Blazer.images = Blazer.settings["images"] || false
38
+ Blazer.override_csp = Blazer.settings["override_csp"] || false
39
+ Blazer.slack_webhook_url = Blazer.settings["slack_webhook_url"] || ENV["BLAZER_SLACK_WEBHOOK_URL"]
40
+ Blazer.mapbox_access_token = Blazer.settings["mapbox_access_token"] || ENV["MAPBOX_ACCESS_TOKEN"]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,218 @@
1
+ module Blazer
2
+ class Result
3
+ attr_reader :data_source, :columns, :rows, :error, :cached_at, :just_cached, :forecast_error
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 == ["string", "numeric"] && @columns.last == "pie"
73
+ "pie"
74
+ elsif column_types.compact.size >= 2 && column_types == ["string"] + (column_types.compact.size - 1).times.map { "numeric" }
75
+ "bar"
76
+ elsif column_types == ["string", "string", "numeric"]
77
+ "bar2"
78
+ elsif column_types == ["numeric", "numeric"]
79
+ "scatter"
80
+ end
81
+ end
82
+ end
83
+
84
+ def forecastable?
85
+ @forecastable ||= Blazer.forecasting && column_types == ["time", "numeric"] && @rows.size >= 10
86
+ end
87
+
88
+ # TODO cache it?
89
+ # don't want to put result data (even hashed version)
90
+ # into cache without developer opt-in
91
+ def forecast
92
+ count = (@rows.size * 0.25).round.clamp(30, 365)
93
+
94
+ case Blazer.forecasting
95
+ when "prophet"
96
+ require "prophet"
97
+ forecast = Prophet.forecast(@rows.to_h, count: count)
98
+ else
99
+ require "trend"
100
+ forecast = Trend.forecast(@rows.to_h, count: count)
101
+ end
102
+
103
+ # round integers
104
+ if @rows[0][1].is_a?(Integer)
105
+ forecast = forecast.map { |k, v| [k, v.round] }.to_h
106
+ end
107
+
108
+ @rows.each do |row|
109
+ row[2] = nil
110
+ end
111
+ @rows.unshift(*forecast.map { |k, v| [k, nil, v] })
112
+ @columns << "forecast"
113
+
114
+ # reset cache
115
+ @column_types = nil
116
+ @chart_type = nil
117
+
118
+ forecast
119
+ rescue => e
120
+ @forecast_error = String.new("Error generating forecast")
121
+ @forecast_error << ": #{e.message.sub("Invalid parameter: ", "")}"
122
+ nil
123
+ end
124
+
125
+ def detect_anomaly
126
+ anomaly = nil
127
+ message = nil
128
+
129
+ if rows.empty?
130
+ message = "No data"
131
+ else
132
+ if chart_type == "line" || chart_type == "line2"
133
+ series = []
134
+
135
+ if chart_type == "line"
136
+ columns[1..-1].each_with_index.each do |k, i|
137
+ series << {name: k, data: rows.map{ |r| [r[0], r[i + 1]] }}
138
+ end
139
+ else
140
+ rows.group_by { |r| v = r[1]; (boom[columns[1]] || {})[v.to_s] || v }.each_with_index.map do |(name, v), i|
141
+ series << {name: name, data: v.map { |v2| [v2[0], v2[2]] }}
142
+ end
143
+ end
144
+
145
+ current_series = nil
146
+ begin
147
+ anomalies = []
148
+ series.each do |s|
149
+ current_series = s[:name]
150
+ anomalies << s[:name] if anomaly?(s[:data])
151
+ end
152
+ anomaly = anomalies.any?
153
+ if anomaly
154
+ if anomalies.size == 1
155
+ message = "Anomaly detected in #{anomalies.first}"
156
+ else
157
+ message = "Anomalies detected in #{anomalies.to_sentence}"
158
+ end
159
+ else
160
+ message = "No anomalies detected"
161
+ end
162
+ rescue => e
163
+ message = "#{current_series}: #{e.message}"
164
+ raise e if Rails.env.development?
165
+ end
166
+ else
167
+ message = "Bad format"
168
+ end
169
+ end
170
+
171
+ [anomaly, message]
172
+ end
173
+
174
+ def anomaly?(series)
175
+ series = series.reject { |v| v[0].nil? }.sort_by { |v| v[0] }
176
+
177
+ if Blazer.anomaly_checks == "trend"
178
+ anomalies = Trend.anomalies(Hash[series])
179
+ anomalies.include?(series.last[0])
180
+ else
181
+ csv_str =
182
+ CSV.generate do |csv|
183
+ csv << ["timestamp", "count"]
184
+ series.each do |row|
185
+ csv << row
186
+ end
187
+ end
188
+
189
+ r_script = %x[which Rscript].chomp
190
+ type = series.any? && series.last.first.to_time - series.first.first.to_time >= 2.weeks ? "ts" : "vec"
191
+ args = [type, csv_str]
192
+ raise "R not found" if r_script.empty?
193
+ command = "#{r_script} --vanilla #{File.expand_path("../detect_anomalies.R", __FILE__)} #{args.map { |a| Shellwords.escape(a) }.join(" ")}"
194
+ output = %x[#{command}]
195
+ if output.empty?
196
+ raise "Unknown R error"
197
+ end
198
+
199
+ rows = CSV.parse(output, headers: true)
200
+ error = rows.first && rows.first["x"]
201
+ raise error if error
202
+
203
+ timestamps = []
204
+ if type == "ts"
205
+ rows.each do |row|
206
+ timestamps << Time.parse(row["timestamp"])
207
+ end
208
+ timestamps.include?(series.last[0].to_time)
209
+ else
210
+ rows.each do |row|
211
+ timestamps << row["index"].to_i
212
+ end
213
+ timestamps.include?(series.length)
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end