appsignal 0.4.7 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.ruby-version +1 -0
  2. data/README.md +20 -19
  3. data/appsignal.gemspec +2 -2
  4. data/lib/appsignal.rb +41 -18
  5. data/lib/appsignal/agent.rb +28 -54
  6. data/lib/appsignal/aggregator.rb +65 -0
  7. data/lib/appsignal/aggregator/post_processor.rb +27 -0
  8. data/lib/appsignal/config.rb +9 -4
  9. data/lib/appsignal/listener.rb +30 -0
  10. data/lib/appsignal/middleware.rb +4 -30
  11. data/lib/appsignal/middleware/action_view_sanitizer.rb +21 -0
  12. data/lib/appsignal/middleware/active_record_sanitizer.rb +60 -0
  13. data/lib/appsignal/middleware/chain.rb +99 -0
  14. data/lib/appsignal/middleware/delete_blanks.rb +12 -0
  15. data/lib/appsignal/railtie.rb +9 -1
  16. data/lib/appsignal/to_appsignal_hash.rb +23 -0
  17. data/lib/appsignal/transaction.rb +72 -16
  18. data/lib/appsignal/transaction/params_sanitizer.rb +91 -13
  19. data/lib/appsignal/transaction/transaction_formatter.rb +32 -68
  20. data/lib/appsignal/version.rb +1 -1
  21. data/spec/appsignal/agent_spec.rb +46 -156
  22. data/spec/appsignal/aggregator/post_processor_spec.rb +84 -0
  23. data/spec/appsignal/aggregator_spec.rb +182 -0
  24. data/spec/appsignal/inactive_railtie_spec.rb +2 -1
  25. data/spec/appsignal/{middleware_spec.rb → listener_spec.rb} +2 -2
  26. data/spec/appsignal/middleware/action_view_sanitizer_spec.rb +27 -0
  27. data/spec/appsignal/middleware/active_record_sanitizer_spec.rb +201 -0
  28. data/spec/appsignal/middleware/chain_spec.rb +168 -0
  29. data/spec/appsignal/middleware/delete_blanks_spec.rb +37 -0
  30. data/spec/appsignal/railtie_spec.rb +47 -34
  31. data/spec/appsignal/to_appsignal_hash_spec.rb +29 -0
  32. data/spec/appsignal/transaction/params_sanitizer_spec.rb +141 -36
  33. data/spec/appsignal/transaction/transaction_formatter_spec.rb +60 -155
  34. data/spec/appsignal/transaction_spec.rb +186 -53
  35. data/spec/appsignal/transmitter_spec.rb +11 -6
  36. data/spec/appsignal_spec.rb +33 -0
  37. data/spec/spec_helper.rb +9 -62
  38. data/spec/support/helpers/notification_helpers.rb +30 -0
  39. data/spec/support/helpers/transaction_helpers.rb +64 -0
  40. metadata +74 -63
  41. data/.rvmrc +0 -1
  42. data/lib/appsignal/transaction/faulty_request_formatter.rb +0 -30
  43. data/lib/appsignal/transaction/regular_request_formatter.rb +0 -11
  44. data/lib/appsignal/transaction/slow_request_formatter.rb +0 -34
  45. data/spec/appsignal/transaction/faulty_request_formatter_spec.rb +0 -49
  46. data/spec/appsignal/transaction/regular_request_formatter_spec.rb +0 -14
  47. data/spec/appsignal/transaction/slow_request_formatter_spec.rb +0 -76
@@ -1,30 +1,4 @@
1
- require 'action_dispatch'
2
-
3
- module Appsignal
4
- class Middleware
5
- def initialize(app, options = {})
6
- @app, @options = app, options
7
- end
8
-
9
- def call(env)
10
- Appsignal::Transaction.create(env['action_dispatch.request_id'], env)
11
- @app.call(env)
12
- rescue Exception => exception
13
- unless in_ignored_exceptions?(exception)
14
- Appsignal::Transaction.current.add_exception(
15
- Appsignal::ExceptionNotification.new(env, exception)
16
- )
17
- end
18
- raise exception
19
- ensure
20
- Appsignal::Transaction.current.complete!
21
- end
22
-
23
- private
24
-
25
- def in_ignored_exceptions?(exception)
26
- Array.wrap(Appsignal.config[:ignore_exceptions]).
27
- include?(exception.class.name)
28
- end
29
- end
30
- end
1
+ require 'appsignal/middleware/chain'
2
+ require 'appsignal/middleware/delete_blanks'
3
+ require 'appsignal/middleware/action_view_sanitizer'
4
+ require 'appsignal/middleware/active_record_sanitizer'
@@ -0,0 +1,21 @@
1
+ module Appsignal
2
+ module Middleware
3
+ class ActionViewSanitizer
4
+ TARGET_EVENT_CATEGORY = 'action_view'.freeze
5
+
6
+ def call(event)
7
+ if event.name.end_with?(TARGET_EVENT_CATEGORY)
8
+ identifier = event.payload[:identifier]
9
+ if identifier
10
+ identifier.gsub!(root_path, '')
11
+ end
12
+ end
13
+ yield
14
+ end
15
+
16
+ def root_path
17
+ @root_path ||= "#{Rails.root.to_s}/"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_record'
2
+
3
+ module Appsignal
4
+ module Middleware
5
+ class ActiveRecordSanitizer
6
+ TARGET_EVENT_NAME = 'sql.active_record'.freeze
7
+
8
+ SINGLE_QUOTE = /\\'/.freeze
9
+ DOUBLE_QUOTE = /\\"/.freeze
10
+ QUOTED_DATA = /(?:"[^"]+"|'[^']+')/.freeze
11
+ SINGLE_QUOTED_DATA = /(?:'[^']+')/.freeze
12
+ IN_ARRAY = /(IN \()[^\)]+(\))/.freeze
13
+ NUMERIC_DATA = /\b\d+\b/.freeze
14
+
15
+ SANITIZED_VALUE = '\1?\2'.freeze
16
+
17
+ def call(event)
18
+ if event.name == TARGET_EVENT_NAME
19
+ unless schema_query?(event) || adapter_uses_prepared_statements?
20
+ query_string = event.payload[:sql]
21
+ if query_string
22
+ if adapter_uses_double_quoted_table_names?
23
+ query_string.gsub!(SINGLE_QUOTE, SANITIZED_VALUE)
24
+ query_string.gsub!(SINGLE_QUOTED_DATA, SANITIZED_VALUE)
25
+ else
26
+ query_string.gsub!(SINGLE_QUOTE, SANITIZED_VALUE)
27
+ query_string.gsub!(DOUBLE_QUOTE, SANITIZED_VALUE)
28
+ query_string.gsub!(QUOTED_DATA, SANITIZED_VALUE)
29
+ end
30
+ query_string.gsub!(IN_ARRAY, SANITIZED_VALUE)
31
+ query_string.gsub!(NUMERIC_DATA, SANITIZED_VALUE)
32
+ end
33
+ end
34
+ event.payload.delete(:connection_id)
35
+ event.payload.delete(:binds)
36
+ end
37
+ yield
38
+ end
39
+
40
+ def schema_query?(event)
41
+ event.payload[:name] == 'SCHEMA'
42
+ end
43
+
44
+ def connection_config
45
+ ActiveRecord::Base.connection_config
46
+ end
47
+
48
+ def adapter_uses_double_quoted_table_names?
49
+ adapter = connection_config[:adapter]
50
+ adapter =~ /postgres/ || adapter =~ /sqlite/
51
+ end
52
+
53
+ def adapter_uses_prepared_statements?
54
+ return false unless adapter_uses_double_quoted_table_names?
55
+ return true if connection_config[:prepared_statements].nil?
56
+ connection_config[:prepared_statements]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,99 @@
1
+ module Appsignal
2
+ # Middleware is code configured to run before/after a message is processed.
3
+ # It is patterned after Rack middleware.
4
+ #
5
+ # @example To add middleware:
6
+ #
7
+ # Appsignal.post_processing_middleware do |chain|
8
+ # chain.add MyPostProcessingHook
9
+ # end
10
+ #
11
+ # @example To insert immediately preceding another entry:
12
+ #
13
+ # Appsignal.post_process_middleware do |chain|
14
+ # chain.insert_before ActiveRecord, MyPostProcessingHook
15
+ # end
16
+ #
17
+ # @example To insert immediately after another entry:
18
+ #
19
+ # Appsignal.post_process_middleware do |chain|
20
+ # chain.insert_after ActiveRecord, MyPostProcessingHook
21
+ # end
22
+ #
23
+ # @example This is an example of a minimal middleware class:
24
+ #
25
+ # class MySHook
26
+ # def call(transaction)
27
+ # puts "Before post processing"
28
+ # yield
29
+ # puts "After post processing"
30
+ # end
31
+ # end
32
+ #
33
+ module Middleware
34
+ class Chain
35
+ attr_reader :entries
36
+
37
+ def initialize
38
+ @entries = []
39
+ yield self if block_given?
40
+ end
41
+
42
+ def remove(klass)
43
+ entries.delete_if { |entry| entry.klass == klass }
44
+ end
45
+
46
+ def add(klass, *args)
47
+ entries << Entry.new(klass, *args) unless exists?(klass)
48
+ end
49
+
50
+ def insert_before(oldklass, newklass, *args)
51
+ i = entries.index { |entry| entry.klass == newklass }
52
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
53
+ i = entries.find_index { |entry| entry.klass == oldklass } || 0
54
+ entries.insert(i, new_entry)
55
+ end
56
+
57
+ def insert_after(oldklass, newklass, *args)
58
+ i = entries.index { |entry| entry.klass == newklass }
59
+ new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
60
+ i = entries.find_index { |entry| entry.klass == oldklass } || entries.count - 1
61
+ entries.insert(i+1, new_entry)
62
+ end
63
+
64
+ def exists?(klass)
65
+ entries.any? { |entry| entry.klass == klass }
66
+ end
67
+
68
+ def retrieve
69
+ @retrieve ||= entries.map(&:make_new)
70
+ end
71
+
72
+ def clear
73
+ entries.clear
74
+ end
75
+
76
+ def invoke(*args)
77
+ chain = retrieve.dup
78
+ traverse_chain = lambda do
79
+ unless chain.empty?
80
+ chain.shift.call(*args, &traverse_chain)
81
+ end
82
+ end
83
+ traverse_chain.call
84
+ end
85
+ end
86
+
87
+ class Entry
88
+ attr_reader :klass
89
+ def initialize(klass, *args)
90
+ @klass = klass
91
+ @args = args
92
+ end
93
+
94
+ def make_new
95
+ @klass.new(*@args)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,12 @@
1
+ module Appsignal
2
+ module Middleware
3
+ class DeleteBlanks
4
+ def call(event)
5
+ event.payload.each do |key, value|
6
+ event.payload.delete(key) if value.blank?
7
+ end
8
+ yield
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,8 +1,14 @@
1
1
  module Appsignal
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "appsignal.configure_rails_initialization" do |app|
4
+ Appsignal.logger = Logger.new(Rails.root.join('log/appsignal.log')).tap do |l|
5
+ l.level = Logger::INFO
6
+ end
7
+ Appsignal.flush_in_memory_log
8
+
4
9
  if Appsignal.active?
5
- app.middleware.insert_before ActionDispatch::RemoteIp, Appsignal::Middleware
10
+ app.middleware.
11
+ insert_before(ActionDispatch::RemoteIp, Appsignal::Listener)
6
12
 
7
13
  Appsignal.subscriber = ActiveSupport::Notifications.subscribe(/^[^!]/) do |*args|
8
14
  if Appsignal::Transaction.current
@@ -17,3 +23,5 @@ module Appsignal
17
23
  end
18
24
  end
19
25
  end
26
+
27
+ require 'appsignal/to_appsignal_hash'
@@ -0,0 +1,23 @@
1
+ module Appsignal
2
+ module ToAppsignalHash
3
+
4
+ def to_appsignal_hash
5
+ {
6
+ :name => name,
7
+ :duration => duration,
8
+ :time => time.to_f,
9
+ :end => self.end.to_f,
10
+ :payload => payload
11
+ }
12
+ end
13
+
14
+ end
15
+ end
16
+
17
+ module ActiveSupport
18
+ module Notifications
19
+ class Event
20
+ include Appsignal::ToAppsignalHash
21
+ end
22
+ end
23
+ end
@@ -1,8 +1,20 @@
1
1
  require 'socket'
2
2
  require 'appsignal/transaction/transaction_formatter'
3
+ require 'appsignal/transaction/params_sanitizer'
3
4
 
4
5
  module Appsignal
5
6
  class Transaction
7
+ # Based on what Rails uses + some variables we'd like to show
8
+ ENV_METHODS = %w(CONTENT_LENGTH AUTH_TYPE GATEWAY_INTERFACE
9
+ PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR
10
+ REQUEST_METHOD SERVER_NAME SERVER_PORT SERVER_PROTOCOL
11
+
12
+ HTTP_X_REQUEST_START HTTP_X_MIDDLEWARE_START HTTP_X_QUEUE_START
13
+ HTTP_X_QUEUE_TIME HTTP_X_HEROKU_QUEUE_WAIT_TIME HTTP_X_APPLICATION_START
14
+ HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE
15
+ HTTP_CACHE_CONTROL HTTP_CONNECTION HTTP_USER_AGENT HTTP_FROM HTTP_NEGOTIATE
16
+ HTTP_PRAGMA HTTP_REFERER).freeze
17
+
6
18
  def self.create(key, env)
7
19
  Thread.current[:appsignal_transaction_id] = key
8
20
  Appsignal.transactions[key] = Appsignal::Transaction.new(key, env)
@@ -12,7 +24,8 @@ module Appsignal
12
24
  Appsignal.transactions[Thread.current[:appsignal_transaction_id]]
13
25
  end
14
26
 
15
- attr_reader :id, :events, :process_action_event, :action, :exception, :env
27
+ attr_reader :id, :events, :process_action_event, :action, :exception, :env,
28
+ :fullpath, :time
16
29
 
17
30
  def initialize(id, env)
18
31
  @id = id
@@ -22,15 +35,22 @@ module Appsignal
22
35
  @env = env
23
36
  end
24
37
 
38
+ def sanitized_environment
39
+ @sanitized_environment ||= {}
40
+ end
41
+
42
+ def sanitized_session_data
43
+ @sanitized_session_data ||= {}
44
+ end
45
+
25
46
  def request
26
47
  ActionDispatch::Request.new(@env)
27
48
  end
28
49
 
29
50
  def set_process_action_event(event)
30
51
  @process_action_event = event
31
- if @process_action_event && @process_action_event.payload
32
- @action = "#{process_action_event.payload[:controller]}#"\
33
- "#{process_action_event.payload[:action]}"
52
+ if event && event.payload
53
+ @action = "#{event.payload[:controller]}##{event.payload[:action]}"
34
54
  end
35
55
  end
36
56
 
@@ -39,6 +59,7 @@ module Appsignal
39
59
  end
40
60
 
41
61
  def add_exception(ex)
62
+ @time = Time.now.utc.to_f
42
63
  @exception = ex
43
64
  end
44
65
 
@@ -48,30 +69,65 @@ module Appsignal
48
69
 
49
70
  def slow_request?
50
71
  return false unless process_action_event && process_action_event.payload
51
- Appsignal.config[:slow_request_threshold] <= process_action_event.duration
72
+ Appsignal.config[:slow_request_threshold] <=
73
+ process_action_event.duration
52
74
  end
53
75
 
54
- def clear_payload_and_events!
55
- @process_action_event.payload.clear
56
- @events.clear
76
+ def slower?(transaction)
77
+ process_action_event.duration > transaction.process_action_event.duration
78
+ end
79
+
80
+ def truncate!
81
+ process_action_event.payload.clear
82
+ events.clear
83
+ sanitized_environment.clear
84
+ sanitized_session_data.clear
85
+ @env = nil
86
+ end
87
+
88
+ def convert_values_to_primitives!
89
+ Appsignal::ParamsSanitizer.sanitize!(@process_action_event.payload) if @process_action_event
90
+ @events.each { |o| Appsignal::ParamsSanitizer.sanitize!(o.payload) }
91
+ add_sanitized_context!
92
+ end
93
+
94
+ def type
95
+ return :exception if exception?
96
+ return :slow_request if slow_request?
97
+ :regular_request
57
98
  end
58
99
 
59
100
  def to_hash
60
- if exception?
61
- TransactionFormatter.faulty(self)
62
- elsif slow_request?
63
- TransactionFormatter.slow(self)
64
- else
65
- TransactionFormatter.regular(self)
66
- end.to_hash
101
+ TransactionFormatter.new(self).to_hash
67
102
  end
68
103
 
69
104
  def complete!
70
105
  Thread.current[:appsignal_transaction_id] = nil
71
106
  current_transaction = Appsignal.transactions.delete(@id)
72
107
  if process_action_event || exception?
73
- Appsignal.agent.add_to_queue(current_transaction)
108
+ Appsignal.enqueue(current_transaction)
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ def add_sanitized_context!
115
+ sanitize_environment!
116
+ sanitize_session_data!
117
+ @env = nil
118
+ end
119
+
120
+ def sanitize_environment!
121
+ env.each do |key, value|
122
+ sanitized_environment[key] = value if ENV_METHODS.include?(key)
74
123
  end
75
124
  end
125
+
126
+ def sanitize_session_data!
127
+ @sanitized_session_data =
128
+ Appsignal::ParamsSanitizer.sanitize(request.session)
129
+ @fullpath = request.fullpath
130
+ end
131
+
76
132
  end
77
133
  end
@@ -2,35 +2,113 @@ module Appsignal
2
2
  class ParamsSanitizer
3
3
  class << self
4
4
  def sanitize(params)
5
- sanitize_hash(params)
5
+ ParamsSanitizerCopy.sanitize_value(params)
6
6
  end
7
7
 
8
- protected
8
+ def sanitize!(params)
9
+ ParamsSanitizerDestructive.sanitize_value(params)
10
+ end
9
11
 
10
- def sanitize_hash(hash)
11
- out = {}
12
- hash.each_pair do |key, value|
13
- out[key] = sanitize_value(value)
14
- end
15
- out
12
+ def scrub(params)
13
+ ParamsSanitizerCopyScrub.sanitize_value(params)
16
14
  end
17
15
 
18
- def sanitize_array(array)
19
- array.map { |value| sanitize_value(value) }
16
+ def scrub!(params)
17
+ ParamsSanitizerDestructiveScrub.sanitize_value(params)
20
18
  end
21
19
 
20
+ protected
21
+
22
22
  def sanitize_value(value)
23
23
  case value
24
24
  when Hash
25
25
  sanitize_hash(value)
26
26
  when Array
27
27
  sanitize_array(value)
28
- when String
29
- value
28
+ when Fixnum, String, Symbol
29
+ unmodified(value)
30
30
  else
31
- value.inspect
31
+ inspected(value)
32
32
  end
33
33
  end
34
+
35
+ def sanitize_hash_with_target(source_hash, target_hash)
36
+ source_hash.each_pair do |key, value|
37
+ target_hash[key] = sanitize_value(value)
38
+ end
39
+ target_hash
40
+ end
41
+
42
+ def sanitize_array_with_target(source_array, target_array)
43
+ source_array.each_with_index do |item, index|
44
+ target_array[index] = sanitize_value(item)
45
+ end
46
+ target_array
47
+ end
48
+
49
+ def unmodified(value)
50
+ value
51
+ end
52
+
53
+ def inspected(value)
54
+ value.inspect
55
+ end
56
+ end
57
+ end
58
+
59
+ class ParamsSanitizerCopy < ParamsSanitizer
60
+ class << self
61
+ protected
62
+
63
+ def sanitize_hash(hash)
64
+ sanitize_hash_with_target(hash, {})
65
+ end
66
+
67
+ def sanitize_array(array)
68
+ sanitize_array_with_target(array, [])
69
+ end
70
+ end
71
+ end
72
+
73
+ class ParamsSanitizerDestructive < ParamsSanitizer
74
+ class << self
75
+ protected
76
+
77
+ def sanitize_hash(hash)
78
+ sanitize_hash_with_target(hash, hash)
79
+ end
80
+
81
+ def sanitize_array(array)
82
+ sanitize_array_with_target(array, array)
83
+ end
84
+ end
85
+ end
86
+
87
+ class ParamsSanitizerCopyScrub < ParamsSanitizerCopy
88
+ class << self
89
+ protected
90
+
91
+ def unmodified(value)
92
+ '?'
93
+ end
94
+
95
+ def inspected(value)
96
+ '?'
97
+ end
98
+ end
99
+ end
100
+
101
+ class ParamsSanitizerDestructiveScrub < ParamsSanitizerDestructive
102
+ class << self
103
+ protected
104
+
105
+ def unmodified(value)
106
+ '?'
107
+ end
108
+
109
+ def inspected(value)
110
+ '?'
111
+ end
34
112
  end
35
113
  end
36
114
  end