appsignal 0.4.7 → 0.5.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 (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