experimental-influxdb-rails 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +78 -0
  5. data/.travis.yml +37 -0
  6. data/CHANGELOG.md +133 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +292 -0
  10. data/Rakefile +34 -0
  11. data/config.ru +7 -0
  12. data/experimental-influxdb-rails.gemspec +35 -0
  13. data/gemfiles/Gemfile.rails-4.2.x +7 -0
  14. data/gemfiles/Gemfile.rails-5.0.x +7 -0
  15. data/gemfiles/Gemfile.rails-5.1.x +7 -0
  16. data/gemfiles/Gemfile.rails-5.2.x +7 -0
  17. data/lib/experimental-influxdb-rails.rb +123 -0
  18. data/lib/influxdb/rails/air_traffic_controller.rb +41 -0
  19. data/lib/influxdb/rails/backtrace.rb +44 -0
  20. data/lib/influxdb/rails/configuration.rb +211 -0
  21. data/lib/influxdb/rails/context.rb +51 -0
  22. data/lib/influxdb/rails/exception_presenter.rb +94 -0
  23. data/lib/influxdb/rails/instrumentation.rb +34 -0
  24. data/lib/influxdb/rails/logger.rb +16 -0
  25. data/lib/influxdb/rails/middleware/hijack_render_exception.rb +16 -0
  26. data/lib/influxdb/rails/middleware/hijack_rescue_action_everywhere.rb +31 -0
  27. data/lib/influxdb/rails/middleware/render_subscriber.rb +26 -0
  28. data/lib/influxdb/rails/middleware/request_subscriber.rb +59 -0
  29. data/lib/influxdb/rails/middleware/simple_subscriber.rb +49 -0
  30. data/lib/influxdb/rails/middleware/sql_subscriber.rb +38 -0
  31. data/lib/influxdb/rails/middleware/subscriber.rb +48 -0
  32. data/lib/influxdb/rails/rack.rb +24 -0
  33. data/lib/influxdb/rails/railtie.rb +51 -0
  34. data/lib/influxdb/rails/sql/normalizer.rb +27 -0
  35. data/lib/influxdb/rails/sql/query.rb +32 -0
  36. data/lib/influxdb/rails/version.rb +5 -0
  37. data/lib/rails/generators/influxdb/influxdb_generator.rb +15 -0
  38. data/lib/rails/generators/influxdb/templates/initializer.rb +11 -0
  39. data/spec/controllers/widgets_controller_spec.rb +15 -0
  40. data/spec/integration/exceptions_spec.rb +37 -0
  41. data/spec/integration/integration_helper.rb +1 -0
  42. data/spec/integration/metrics_spec.rb +28 -0
  43. data/spec/shared_examples/data.rb +67 -0
  44. data/spec/shared_examples/tags.rb +45 -0
  45. data/spec/spec_helper.rb +31 -0
  46. data/spec/support/rails4/app.rb +44 -0
  47. data/spec/support/rails5/app.rb +44 -0
  48. data/spec/support/views/widgets/_item.html.erb +1 -0
  49. data/spec/support/views/widgets/index.html.erb +5 -0
  50. data/spec/unit/backtrace_spec.rb +85 -0
  51. data/spec/unit/configuration_spec.rb +125 -0
  52. data/spec/unit/context_spec.rb +40 -0
  53. data/spec/unit/exception_presenter_spec.rb +23 -0
  54. data/spec/unit/influxdb_rails_spec.rb +78 -0
  55. data/spec/unit/middleware/render_subscriber_spec.rb +92 -0
  56. data/spec/unit/middleware/request_subscriber_spec.rb +94 -0
  57. data/spec/unit/middleware/sql_subscriber_spec.rb +95 -0
  58. data/spec/unit/sql/normalizer_spec.rb +15 -0
  59. data/spec/unit/sql/query_spec.rb +29 -0
  60. metadata +300 -0
@@ -0,0 +1,51 @@
1
+ module InfluxDB
2
+ module Rails
3
+ class Context # rubocop:disable Style/Documentation
4
+ def controller
5
+ Thread.current[:_influxdb_rails_controller]
6
+ end
7
+
8
+ def controller=(value)
9
+ Thread.current[:_influxdb_rails_controller] = value
10
+ end
11
+
12
+ def action
13
+ Thread.current[:_influxdb_rails_action]
14
+ end
15
+
16
+ def action=(value)
17
+ Thread.current[:_influxdb_rails_action] = value
18
+ end
19
+
20
+ def location
21
+ [
22
+ controller,
23
+ action,
24
+ ].reject(&:blank?).join("#")
25
+ end
26
+
27
+ def reset
28
+ Thread.current[:_influxdb_rails_controller] = nil
29
+ Thread.current[:_influxdb_rails_action] = nil
30
+ Thread.current[:_influxdb_rails_tags] = nil
31
+ Thread.current[:_influxdb_rails_values] = nil
32
+ end
33
+
34
+ def tags
35
+ Thread.current[:_influxdb_rails_tags] || {}
36
+ end
37
+
38
+ def tags=(tags)
39
+ Thread.current[:_influxdb_rails_tags] = tags
40
+ end
41
+
42
+ def values
43
+ Thread.current[:_influxdb_rails_values] || {}
44
+ end
45
+
46
+ def values=(values)
47
+ Thread.current[:_influxdb_rails_values] = values
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,94 @@
1
+ require "base64"
2
+ require "socket"
3
+ require "json"
4
+
5
+ module InfluxDB
6
+ module Rails
7
+ class ExceptionPresenter # rubocop:disable Style/Documentation
8
+ attr_reader :exception
9
+ attr_reader :backtrace
10
+ attr_reader :params
11
+ attr_reader :session_data
12
+ attr_reader :current_user
13
+ attr_reader :controller
14
+ attr_reader :action
15
+ attr_reader :request_url
16
+ attr_reader :referer
17
+ attr_reader :remote_ip
18
+ attr_reader :user_agent
19
+ attr_reader :custom_data
20
+
21
+ def initialize(ex, params = {})
22
+ ex = ex.continued_exception if ex.respond_to?(:continued_exception)
23
+ ex = ex.original_exception if ex.respond_to?(:original_exception)
24
+
25
+ @exception = ex.is_a?(String) ? Exception.new(ex) : ex
26
+ @backtrace = InfluxDB::Rails::Backtrace.new(@exception.backtrace)
27
+ @dimensions = {}
28
+ configure_from_params(params)
29
+
30
+ @environment_variables = ENV.to_hash || {}
31
+ end
32
+
33
+ def context # rubocop:disable Metrics/MethodLength
34
+ c = {
35
+ application_name: InfluxDB::Rails.configuration.application_name,
36
+ application_root: InfluxDB::Rails.configuration.application_root,
37
+ framework: InfluxDB::Rails.configuration.framework,
38
+ framework_version: InfluxDB::Rails.configuration.framework_version,
39
+ language: "Ruby",
40
+ language_version: "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
41
+ custom_data: @custom_data,
42
+ }
43
+
44
+ InfluxDB::Rails.configuration.add_custom_exception_data(self)
45
+ c
46
+ end
47
+
48
+ def dimensions
49
+ {
50
+ class: @exception.class.to_s,
51
+ method: "#{@controller}##{@action}",
52
+ filename: File.basename(@backtrace.lines.first.try(:file)),
53
+ server: Socket.gethostname,
54
+ status: "open",
55
+ }.merge(@dimensions)
56
+ end
57
+
58
+ def values
59
+ {
60
+ exception_message: @exception.message,
61
+ exception_backtrace: JSON.generate(@backtrace.to_a),
62
+ }
63
+ end
64
+
65
+ def request_data
66
+ {
67
+ params: @params,
68
+ session_data: @session_data,
69
+ controller: @controller,
70
+ action: @action,
71
+ request_url: @request_url,
72
+ referer: @referer,
73
+ remote_ip: @remote_ip,
74
+ user_agent: @user_agent,
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def configure_from_params(params)
81
+ @params = params[:params]
82
+ @session_data = params[:session_data] || {}
83
+ @current_user = params[:current_user]
84
+ @controller = params[:controller]
85
+ @action = params[:action]
86
+ @request_url = params[:request_url]
87
+ @user_agent = params[:user_agent]
88
+ @referer = params[:referer]
89
+ @remote_ip = params[:remote_ip]
90
+ @custom_data = params[:custom_data] || {}
91
+ end
92
+ end
93
+ end
94
+ 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
@@ -0,0 +1,16 @@
1
+ module InfluxDB
2
+ module Rails
3
+ module Logger # rubocop:disable Style/Documentation
4
+ PREFIX = "[InfluxDB::Rails] ".freeze
5
+
6
+ private
7
+
8
+ def log(level, message)
9
+ c = InfluxDB::Rails.configuration
10
+ return if level != :error && !c.debug?
11
+
12
+ c.logger&.send(level, PREFIX + message)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module InfluxDB
2
+ module Rails
3
+ module Middleware
4
+ module HijackRenderException # rubocop:disable Style/Documentation
5
+ def render_exception(env, ex)
6
+ controller = env["action_controller.instance"] || env.controller_instance
7
+ request_data = controller.try(:influxdb_request_data) || {}
8
+ unless InfluxDB::Rails.configuration.ignore_user_agent?(request_data[:user_agent])
9
+ InfluxDB::Rails.report_exception_unless_ignorable(ex, request_data)
10
+ end
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ module InfluxDB
2
+ module Rails
3
+ module Middleware
4
+ module HijackRescueActionEverywhere # rubocop:disable Style/Documentation
5
+ def self.included(base)
6
+ base.send(:alias_method_chain, :rescue_action_in_public, :influxdb)
7
+ base.send(:alias_method_chain, :rescue_action_locally, :influxdb)
8
+ end
9
+
10
+ private
11
+
12
+ def rescue_action_in_public_with_influxdb(ex)
13
+ handle_exception(ex)
14
+ rescue_action_in_public_without_influxdb(ex)
15
+ end
16
+
17
+ def rescue_action_locally_with_influxdb(ex)
18
+ handle_exception(ex)
19
+ rescue_action_locally_without_influxdb(ex)
20
+ end
21
+
22
+ def handle_exception(ex)
23
+ request_data = influxdb_request_data || {}
24
+ return if InfluxDB::Rails.configuration.ignore_user_agent?(request_data[:user_agent])
25
+
26
+ InfluxDB::Rails.report_exception_unless_ignorable(ex, request_data)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ require "influxdb/rails/middleware/simple_subscriber"
2
+
3
+ module InfluxDB
4
+ module Rails
5
+ module Middleware
6
+ class RenderSubscriber < SimpleSubscriber # :nodoc:
7
+ private
8
+
9
+ def values(started, finished, payload)
10
+ super(started, finished, payload).merge(
11
+ count: payload[:count],
12
+ cache_hits: payload[:cache_hits]
13
+ ).reject { |_, value| value.nil? }
14
+ end
15
+
16
+ def tags(payload)
17
+ tags = {
18
+ location: location,
19
+ filename: payload[:identifier],
20
+ }
21
+ super(tags)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ require "influxdb/rails/middleware/subscriber"
2
+
3
+ module InfluxDB
4
+ module Rails
5
+ module Middleware
6
+ class RequestSubscriber < Subscriber # :nodoc:
7
+ def call(_name, start, finish, _id, payload) # rubocop:disable Metrics/MethodLength
8
+ return unless enabled?
9
+
10
+ finished = InfluxDB.convert_timestamp(finish.utc, configuration.time_precision)
11
+ started = InfluxDB.convert_timestamp(start.utc, configuration.time_precision)
12
+ tags = tags(payload)
13
+ begin
14
+ series(payload, start, finish).each do |series_name, value|
15
+ InfluxDB::Rails.client.write_point \
16
+ series_name,
17
+ values: values(value, started),
18
+ tags: tags,
19
+ timestamp: finished
20
+ end
21
+ rescue StandardError => e
22
+ log :error, "[InfluxDB::Rails] Unable to write points: #{e.message}"
23
+ ensure
24
+ InfluxDB::Rails.current.reset
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def values(duration, started)
31
+ addititional_values = InfluxDB::Rails.current.values
32
+ { value: duration, started: started }.merge(addititional_values).reject do |_, value|
33
+ value.nil? || value == ""
34
+ end
35
+ end
36
+
37
+ def series(payload, start, finish)
38
+ {
39
+ configuration.series_name_for_controller_runtimes => ((finish - start) * 1000).ceil,
40
+ configuration.series_name_for_view_runtimes => (payload[:view_runtime] || 0).ceil,
41
+ configuration.series_name_for_db_runtimes => (payload[:db_runtime] || 0).ceil,
42
+ }
43
+ end
44
+
45
+ def tags(payload)
46
+ tags = {
47
+ method: "#{payload[:controller]}##{payload[:action]}",
48
+ status: payload[:status],
49
+ format: payload[:format],
50
+ http_method: payload[:method],
51
+ server: Socket.gethostname,
52
+ app_name: configuration.application_name,
53
+ }
54
+ super(tags)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ require "influxdb/rails/middleware/subscriber"
2
+
3
+ module InfluxDB
4
+ module Rails
5
+ module Middleware
6
+ # Subscriber acts as base class for different *Subscriber classes,
7
+ # which are intended as ActiveSupport::Notifications.subscribe
8
+ # consumers.
9
+ class SimpleSubscriber < Subscriber
10
+ attr_reader :series_name
11
+
12
+ def initialize(configuration, series_name)
13
+ super(configuration)
14
+ @series_name = series_name
15
+ end
16
+
17
+ def call(_name, started, finished, _unique_id, payload)
18
+ return unless enabled?
19
+
20
+ begin
21
+ InfluxDB::Rails.client.write_point series_name,
22
+ values: values(started, finished, payload),
23
+ tags: tags(payload),
24
+ timestamp: timestamp(finished.utc)
25
+ rescue StandardError => e
26
+ log :error, "[InfluxDB::Rails] Unable to write points: #{e.message}"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def values(started, finished, _payload)
33
+ result = { value: ((finished - started) * 1000).ceil }
34
+ result.merge(InfluxDB::Rails.current.values).reject do |_, value|
35
+ value.nil? || value == ""
36
+ end
37
+ end
38
+
39
+ def timestamp(finished)
40
+ InfluxDB.convert_timestamp(finished.utc, configuration.time_precision)
41
+ end
42
+
43
+ def enabled?
44
+ super && series_name.present?
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ require "influxdb/rails/middleware/simple_subscriber"
2
+ require "influxdb/rails/sql/query"
3
+
4
+ module InfluxDB
5
+ module Rails
6
+ module Middleware
7
+ class SqlSubscriber < SimpleSubscriber # :nodoc:
8
+ def call(_name, started, finished, _unique_id, payload)
9
+ return unless InfluxDB::Rails::Sql::Query.new(payload).track?
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def values(started, finished, payload)
17
+ super.merge(sql: InfluxDB::Rails::Sql::Normalizer.new(payload[:sql]).perform)
18
+ end
19
+
20
+ def location
21
+ result = super
22
+ result.empty? ? :raw : result
23
+ end
24
+
25
+ def tags(payload)
26
+ query = InfluxDB::Rails::Sql::Query.new(payload)
27
+ tags = {
28
+ location: location,
29
+ operation: query.operation,
30
+ class_name: query.class_name,
31
+ name: query.name,
32
+ }
33
+ super(tags)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ require "influxdb/rails/logger"
2
+
3
+ module InfluxDB
4
+ module Rails
5
+ module Middleware
6
+ # Subscriber acts as base class for different *Subscriber classes,
7
+ # which are intended as ActiveSupport::Notifications.subscribe
8
+ # consumers.
9
+ class Subscriber
10
+ include InfluxDB::Rails::Logger
11
+
12
+ attr_reader :configuration
13
+
14
+ def initialize(configuration)
15
+ @configuration = configuration
16
+ end
17
+
18
+ def call(*)
19
+ raise NotImplementedError, "must be implemented in subclass"
20
+ end
21
+
22
+ private
23
+
24
+ def tags(tags)
25
+ result = tags.merge(InfluxDB::Rails.current.tags)
26
+ result = configuration.tags_middleware.call(result)
27
+ result.reject! do |_, value|
28
+ value.nil? || value == ""
29
+ end
30
+ result
31
+ end
32
+
33
+ def enabled?
34
+ configuration.instrumentation_enabled? &&
35
+ !configuration.ignore_current_environment?
36
+ end
37
+
38
+ def location
39
+ current = InfluxDB::Rails.current
40
+ [
41
+ current.controller,
42
+ current.action,
43
+ ].reject(&:blank?).join("#")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end