asklytics-influxdb-rails 1.0.0.beta3

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +78 -0
  5. data/.travis.yml +37 -0
  6. data/CHANGELOG.md +127 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +291 -0
  10. data/Rakefile +34 -0
  11. data/config.ru +7 -0
  12. data/gemfiles/Gemfile.rails-4.2.x +7 -0
  13. data/gemfiles/Gemfile.rails-5.0.x +7 -0
  14. data/gemfiles/Gemfile.rails-5.1.x +7 -0
  15. data/gemfiles/Gemfile.rails-5.2.x +7 -0
  16. data/influxdb-rails.gemspec +52 -0
  17. data/lib/httplog.rb +8 -0
  18. data/lib/influxdb-rails.rb +134 -0
  19. data/lib/influxdb/rails/air_traffic_controller.rb +41 -0
  20. data/lib/influxdb/rails/backtrace.rb +44 -0
  21. data/lib/influxdb/rails/configuration.rb +263 -0
  22. data/lib/influxdb/rails/context.rb +42 -0
  23. data/lib/influxdb/rails/exception_presenter.rb +94 -0
  24. data/lib/influxdb/rails/httplog/adapters/ethon.rb +62 -0
  25. data/lib/influxdb/rails/httplog/adapters/excon.rb +67 -0
  26. data/lib/influxdb/rails/httplog/adapters/http.rb +64 -0
  27. data/lib/influxdb/rails/httplog/adapters/httpclient.rb +76 -0
  28. data/lib/influxdb/rails/httplog/adapters/net_http.rb +53 -0
  29. data/lib/influxdb/rails/httplog/adapters/patron.rb +44 -0
  30. data/lib/influxdb/rails/httplog/helpers/al_helper.rb +12 -0
  31. data/lib/influxdb/rails/httplog/http_configuration.rb +55 -0
  32. data/lib/influxdb/rails/httplog/http_log.rb +332 -0
  33. data/lib/influxdb/rails/instrumentation.rb +34 -0
  34. data/lib/influxdb/rails/logger.rb +16 -0
  35. data/lib/influxdb/rails/middleware/hijack_render_exception.rb +16 -0
  36. data/lib/influxdb/rails/middleware/hijack_rescue_action_everywhere.rb +31 -0
  37. data/lib/influxdb/rails/middleware/render_subscriber.rb +28 -0
  38. data/lib/influxdb/rails/middleware/request_subscriber.rb +69 -0
  39. data/lib/influxdb/rails/middleware/simple_subscriber.rb +71 -0
  40. data/lib/influxdb/rails/middleware/sql_subscriber.rb +35 -0
  41. data/lib/influxdb/rails/middleware/subscriber.rb +44 -0
  42. data/lib/influxdb/rails/rack.rb +39 -0
  43. data/lib/influxdb/rails/railtie.rb +52 -0
  44. data/lib/influxdb/rails/sql/normalizer.rb +27 -0
  45. data/lib/influxdb/rails/sql/query.rb +32 -0
  46. data/lib/influxdb/rails/version.rb +5 -0
  47. data/lib/rails/generators/influxdb/influxdb_generator.rb +15 -0
  48. data/lib/rails/generators/influxdb/templates/initializer.rb +11 -0
  49. data/spec/controllers/widgets_controller_spec.rb +15 -0
  50. data/spec/integration/exceptions_spec.rb +37 -0
  51. data/spec/integration/integration_helper.rb +1 -0
  52. data/spec/integration/metrics_spec.rb +28 -0
  53. data/spec/shared_examples/tags.rb +42 -0
  54. data/spec/spec_helper.rb +31 -0
  55. data/spec/support/rails4/app.rb +44 -0
  56. data/spec/support/rails5/app.rb +44 -0
  57. data/spec/support/views/widgets/_item.html.erb +1 -0
  58. data/spec/support/views/widgets/index.html.erb +5 -0
  59. data/spec/unit/backtrace_spec.rb +85 -0
  60. data/spec/unit/configuration_spec.rb +125 -0
  61. data/spec/unit/context_spec.rb +40 -0
  62. data/spec/unit/exception_presenter_spec.rb +23 -0
  63. data/spec/unit/influxdb_rails_spec.rb +78 -0
  64. data/spec/unit/middleware/render_subscriber_spec.rb +92 -0
  65. data/spec/unit/middleware/request_subscriber_spec.rb +91 -0
  66. data/spec/unit/middleware/sql_subscriber_spec.rb +81 -0
  67. data/spec/unit/sql/normalizer_spec.rb +15 -0
  68. data/spec/unit/sql/query_spec.rb +29 -0
  69. metadata +487 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Patron)
4
+ module Patron
5
+ class Session
6
+ alias orig_request request
7
+ def request(action_name, url, headers, options = {})
8
+ bm = Benchmark.realtime do
9
+ @response = orig_request(action_name, url, headers, options)
10
+ end
11
+
12
+ if HttpLog.url_approved?(url)
13
+ # HttpLog.call(
14
+ # method: action_name,
15
+ # url: url,
16
+ # request_body: options[:data],
17
+ # request_headers: headers,
18
+ # response_code: @response.status,
19
+ # response_body: @response.body,
20
+ # response_headers: @response.headers,
21
+ # benchmark: bm,
22
+ # encoding: @response.headers['Content-Encoding'],
23
+ # content_type: @response.headers['Content-Type']
24
+ # )
25
+
26
+ HttpLog.save_in_db(
27
+ method: action_name,
28
+ url: url,
29
+ request_body: options[:data],
30
+ request_headers: headers,
31
+ response_code: @response.status,
32
+ response_body: @response.body,
33
+ response_headers: @response.headers,
34
+ benchmark: bm,
35
+ encoding: @response.headers['Content-Encoding'],
36
+ content_type: @response.headers['Content-Type']
37
+ )
38
+ end
39
+
40
+ @response
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+
4
+ module HttpLog
5
+ class AlHelper
6
+
7
+ def self.get_uid
8
+ SecureRandom.uuid
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpLog
4
+ class HttpConfiguration
5
+ attr_accessor :enabled,
6
+ :compact_log,
7
+ :json_log,
8
+ :logger,
9
+ :logger_method,
10
+ :severity,
11
+ :prefix,
12
+ :log_connect,
13
+ :log_request,
14
+ :log_headers,
15
+ :log_data,
16
+ :log_status,
17
+ :log_response,
18
+ :log_benchmark,
19
+ :url_whitelist_pattern,
20
+ :url_blacklist_pattern,
21
+ :color,
22
+ :prefix_data_lines,
23
+ :prefix_response_lines,
24
+ :prefix_line_numbers,
25
+ :filter_parameters,
26
+ :http_request_tracking_id,
27
+ :http_request_tracking_measurement
28
+
29
+ def initialize
30
+ @enabled = true
31
+ @compact_log = false
32
+ @json_log = false
33
+ @logger = Logger.new($stdout)
34
+ @logger_method = :log
35
+ @severity = Logger::Severity::DEBUG
36
+ @prefix = LOG_PREFIX
37
+ @log_connect = true
38
+ @http_request_tracking_id = "al-txn-id"
39
+ @http_request_tracking_measurement = "rails.http_client"
40
+ @log_request = true
41
+ @log_headers = false
42
+ @log_data = true
43
+ @log_status = true
44
+ @log_response = true
45
+ @log_benchmark = true
46
+ @url_whitelist_pattern = nil
47
+ @url_blacklist_pattern = nil
48
+ @color = false
49
+ @prefix_data_lines = false
50
+ @prefix_response_lines = false
51
+ @prefix_line_numbers = false
52
+ @filter_parameters = []
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'logger'
5
+ require 'benchmark'
6
+ require 'rainbow'
7
+ require 'rack'
8
+
9
+ module HttpLog
10
+ LOG_PREFIX = '[httplog] '.freeze
11
+ PARAM_MASK = '[FILTERED]'
12
+
13
+ class BodyParsingError < StandardError; end
14
+
15
+ class << self
16
+ attr_writer :http_configuration
17
+
18
+ def configuration
19
+ @http_configuration ||= HttpConfiguration.new
20
+ end
21
+ alias config configuration
22
+
23
+ def reset!
24
+ @http_configuration = nil
25
+ end
26
+
27
+ def configure
28
+ yield(configuration)
29
+ end
30
+
31
+ def call(options = {})
32
+ if config.json_log
33
+ log_json(options)
34
+ elsif config.compact_log
35
+ log_compact(options[:method], options[:url], options[:response_code], options[:benchmark])
36
+ else
37
+ HttpLog.log_request(options[:method], options[:url] )
38
+ HttpLog.log_headers(options[:request_headers])
39
+ HttpLog.log_data(options[:request_body])
40
+ HttpLog.log_status(options[:response_code])
41
+ HttpLog.log_benchmark(options[:benchmark])
42
+ HttpLog.log_headers(options[:response_headers])
43
+ HttpLog.log_body(options[:response_body], options[:encoding], options[:content_type])
44
+ end
45
+ end
46
+
47
+ def save_in_db(options = {})
48
+ #log("Connecting rails.http_client #{config.http_request_tracking_measurement} : #{config.http_request_tracking_id}")
49
+ x_request_id = "AL_NONE"
50
+ #al_txn_uid = nil
51
+ al_request_id = "AL_NONE"
52
+ al_request_guid = "AL_NONE"
53
+ al_request_client_id = "AL_NONE"
54
+
55
+ options[:request_headers].each do |header,value|
56
+ al_request_client_id = value if(header === "al-request-client-id")
57
+ #al_source = value if(header === "al-source")
58
+ al_request_id = Thread.current["al_request_id"] if Thread.current["al_request_id"]
59
+ al_request_guid = Thread.current["al_request_guid"] if Thread.current["al_request_guid"]
60
+
61
+ #al_txn_uid = value if(header === config.http_request_tracking_id)
62
+ end
63
+
64
+ data = nil
65
+
66
+ if options[:timestamp]
67
+ data = {
68
+ values: {value: options[:benchmark] , al_request_id: al_request_id, al_request_guid: al_request_guid, al_request_client_id: al_request_client_id},
69
+ tags: {app_name: Rails.application.class.parent, url: options[:url], method: options[:method], response_code: options[:response_code], location: InfluxDB::Rails.current.location},
70
+ timestamp: options[:timestamp]
71
+ }
72
+ else
73
+ data = {
74
+ values: {value: options[:benchmark] , al_request_id: al_request_id, al_request_client_id: al_request_client_id},
75
+ tags: {app_name: Rails.application.class.parent, url: options[:url], method: options[:method], response_code: options[:response_code], location: InfluxDB::Rails.current.location},
76
+ }
77
+ end
78
+
79
+ InfluxDB::Rails.client.write_point config.http_request_tracking_measurement, data
80
+ end
81
+
82
+ def series(options = {})
83
+ {
84
+ value: options[:benchmark]
85
+ }
86
+
87
+ end
88
+
89
+ def tags(options = {})
90
+ tags = {
91
+ method: options[:method],
92
+ url: options[:url],
93
+ app_name: configuration.application_name,
94
+ host: Socket.gethostname
95
+ }
96
+ super(tags)
97
+ end
98
+
99
+ def url_approved?(url)
100
+ return false if config.url_blacklist_pattern && url.to_s.match(config.url_blacklist_pattern)
101
+
102
+ !config.url_whitelist_pattern || url.to_s.match(config.url_whitelist_pattern)
103
+ end
104
+
105
+ def log(msg)
106
+ return unless config.enabled
107
+
108
+ config.logger.public_send(config.logger_method, config.severity, colorize(prefix + msg))
109
+ end
110
+
111
+ def log_connection(host, port = nil)
112
+ return if config.json_log || config.compact_log || !config.log_connect
113
+
114
+ log("Connecting: #{[host, port].compact.join(':')}")
115
+ end
116
+
117
+ def log_request(method, uri)
118
+ return unless config.log_request
119
+
120
+ log("Sending: #{method.to_s.upcase} #{masked(uri)}")
121
+ end
122
+
123
+ def log_headers(headers = {})
124
+ return unless config.log_headers
125
+
126
+ masked(headers).each do |key, value|
127
+ log("Header: #{key}: #{value}")
128
+ end
129
+ end
130
+
131
+ def log_status(status)
132
+ return unless config.log_status
133
+
134
+ status = Rack::Utils.status_code(status) unless status == /\d{3}/
135
+ log("Status: #{status}")
136
+ end
137
+
138
+ def log_benchmark(seconds)
139
+ return unless config.log_benchmark
140
+
141
+ log("Benchmark: #{seconds.to_f.round(6)} seconds")
142
+ end
143
+
144
+ def log_body(body, encoding = nil, content_type = nil)
145
+ return unless config.log_response
146
+
147
+ data = parse_body(body, encoding, content_type)
148
+
149
+ if config.prefix_response_lines
150
+ log('Response:')
151
+ log_data_lines(data)
152
+ else
153
+ log("Response:\n#{data}")
154
+ end
155
+ rescue BodyParsingError => e
156
+ log("Response: #{e.message}")
157
+ end
158
+
159
+ def parse_body(body, encoding, content_type)
160
+ unless text_based?(content_type)
161
+ raise BodyParsingError, "(not showing binary data)"
162
+ end
163
+
164
+ if body.is_a?(Net::ReadAdapter)
165
+ # open-uri wraps the response in a Net::ReadAdapter that defers reading
166
+ # the content, so the reponse body is not available here.
167
+ raise BodyParsingError, '(not available yet)'
168
+ end
169
+
170
+ if encoding =~ /gzip/ && body && !body.empty?
171
+ begin
172
+ sio = StringIO.new(body.to_s)
173
+ gz = Zlib::GzipReader.new(sio)
174
+ body = gz.read
175
+ rescue Zlib::GzipFile::Error
176
+ log("(gzip decompression failed)")
177
+ end
178
+ end
179
+
180
+ utf_encoded(body.to_s, content_type)
181
+ end
182
+
183
+ def log_data(data)
184
+ return unless config.log_data
185
+
186
+ data = utf_encoded(masked(data.dup).to_s) unless data.nil?
187
+
188
+ if config.prefix_data_lines
189
+ log('Data:')
190
+ log_data_lines(data)
191
+ else
192
+ log("Data: #{data}")
193
+ end
194
+ end
195
+
196
+ def log_compact(method, uri, status, seconds)
197
+ return unless config.compact_log
198
+ status = Rack::Utils.status_code(status) unless status == /\d{3}/
199
+ log("#{method.to_s.upcase} #{masked(uri)} completed with status code #{status} in #{seconds.to_f.round(6)} seconds")
200
+ end
201
+
202
+ def log_json(data = {})
203
+ return unless config.json_log
204
+
205
+ data[:response_code] = transform_response_code(data[:response_code]) if data[:response_code].is_a?(Symbol)
206
+
207
+ parsed_body = begin
208
+ parse_body(data[:response_body], data[:encoding], data[:content_type])
209
+ rescue BodyParsingError => e
210
+ e.message
211
+ end
212
+
213
+ if config.compact_log
214
+ log({
215
+ method: data[:method].to_s.upcase,
216
+ url: masked(data[:url]),
217
+ response_code: data[:response_code].to_i,
218
+ benchmark: data[:benchmark]
219
+ }.to_json)
220
+ else
221
+ log({
222
+ method: data[:method].to_s.upcase,
223
+ url: masked(data[:url]),
224
+ request_body: masked(data[:request_body]),
225
+ request_headers: masked(data[:request_headers].to_h),
226
+ response_code: data[:response_code].to_i,
227
+ response_body: parsed_body,
228
+ response_headers: data[:response_headers].to_h,
229
+ benchmark: data[:benchmark]
230
+ }.to_json)
231
+ end
232
+ end
233
+
234
+ def transform_response_code(response_code_name)
235
+ Rack::Utils::HTTP_STATUS_CODES.detect { |_k, v| v.to_s.casecmp(response_code_name.to_s).zero? }.first
236
+ end
237
+
238
+ def colorize(msg)
239
+ return msg unless config.color
240
+ if config.color.is_a?(Hash)
241
+ msg = Rainbow(msg).color(config.color[:color]) if config.color[:color]
242
+ msg = Rainbow(msg).bg(config.color[:background]) if config.color[:background]
243
+ else
244
+ msg = Rainbow(msg).color(config.color)
245
+ end
246
+ msg
247
+ rescue StandardError
248
+ warn "HTTPLOG CONFIGURATION ERROR: #{config.color} is not a valid color"
249
+ msg
250
+ end
251
+
252
+ private
253
+
254
+ def masked(msg, key=nil)
255
+ return msg if config.filter_parameters.empty?
256
+ return msg if msg.nil?
257
+
258
+ # If a key is given, msg is just the value and can be replaced
259
+ # in its entirety.
260
+ return (config.filter_parameters.include?(key.downcase) ? PARAM_MASK : msg) if key
261
+
262
+ # Otherwise, we'll parse Strings for key=valye pairs...
263
+ case msg
264
+ when *string_classes
265
+ config.filter_parameters.reduce(msg) do |m,key|
266
+ m.to_s.gsub(/(#{key})=[^&]+/i, "#{key}=#{PARAM_MASK}")
267
+ end
268
+ # ...and recurse over hashes
269
+ when *hash_classes
270
+ Hash[msg.map {|k,v| [k, masked(v, k)]}]
271
+ else
272
+ log "*** FILTERING NOT APPLIED BECAUSE #{msg.class} IS UNEXPECTED ***"
273
+ msg
274
+ end
275
+ end
276
+
277
+ def string_classes
278
+ @string_classes ||= begin
279
+ string_classes = [String]
280
+ string_classes << HTTP::Response::Body if defined?(HTTP::Response::Body)
281
+ string_classes << HTTP::URI if defined?(HTTP::URI)
282
+ string_classes << URI::HTTP if defined?(URI::HTTP)
283
+ string_classes << HTTP::FormData::Urlencoded if defined?(HTTP::FormData::Urlencoded)
284
+ string_classes
285
+ end
286
+ end
287
+
288
+ def hash_classes
289
+ @hash_classes ||= begin
290
+ hash_classes = [Hash, Enumerator]
291
+ hash_classes << HTTP::Headers if defined?(HTTP::Headers)
292
+ hash_classes
293
+ end
294
+ end
295
+
296
+ def utf_encoded(data, content_type = nil)
297
+ charset = content_type.to_s.scan(/; charset=(\S+)/).flatten.first || 'UTF-8'
298
+ begin
299
+ data.force_encoding(charset)
300
+ rescue StandardError
301
+ data.force_encoding('UTF-8')
302
+ end
303
+ data.encode('UTF-8', invalid: :replace, undef: :replace)
304
+ end
305
+
306
+ def text_based?(content_type)
307
+ # This is a very naive way of determining if the content type is text-based; but
308
+ # it will allow application/json and the like without having to resort to more
309
+ # heavy-handed checks.
310
+ content_type =~ /^text/ ||
311
+ content_type =~ /^application/ && !['application/octet-stream', 'application/pdf'].include?(content_type)
312
+ end
313
+
314
+ def log_data_lines(data)
315
+ data.each_line.with_index do |line, row|
316
+ if config.prefix_line_numbers
317
+ log("#{row + 1}: #{line.chomp}")
318
+ else
319
+ log(line.strip)
320
+ end
321
+ end
322
+ end
323
+
324
+ def prefix
325
+ if config.prefix.respond_to?(:call)
326
+ config.prefix.call
327
+ else
328
+ config.prefix.to_s
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,34 @@
1
+ module InfluxDB
2
+ module Rails
3
+ module Instrumentation # rubocop:disable Style/Documentation
4
+ def benchmark_for_instrumentation # rubocop:disable Metrics/MethodLength
5
+ start = Time.now
6
+ yield
7
+
8
+ c = InfluxDB::Rails.configuration
9
+ return if c.ignore_current_environment?
10
+
11
+ InfluxDB::Rails.client.write_point \
12
+ c.series_name_for_instrumentation,
13
+ values: {
14
+ value: ((Time.now - start) * 1000).ceil,
15
+ },
16
+ tags: configuration.tags_middleware.call(
17
+ method: "#{controller_name}##{action_name}",
18
+ server: Socket.gethostname
19
+ )
20
+ end
21
+
22
+ def self.included(base)
23
+ base.extend(ClassMethods)
24
+ end
25
+
26
+ module ClassMethods # rubocop:disable Style/Documentation
27
+ def instrument(methods = [])
28
+ methods = [methods] unless methods.is_a?(Array)
29
+ around_filter :benchmark_for_instrumentation, only: methods
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end