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
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+
3
+ describe Appsignal::Aggregator do
4
+ let(:aggregator) { Appsignal::Aggregator.new }
5
+
6
+ describe "#add" do
7
+ subject { aggregator.add(transaction) }
8
+
9
+ context "adding a regular transaction" do
10
+ let(:transaction) { regular_transaction }
11
+
12
+ specify { transaction.should_receive(:truncate!) }
13
+ end
14
+
15
+ context "adding a slow transaction" do
16
+ let(:transaction) { slow_transaction }
17
+
18
+ specify {
19
+ aggregator.should_receive(:pre_process_slowness!).with(transaction)
20
+ }
21
+ end
22
+
23
+ context "adding a transaction with an exception" do
24
+ let(:transaction) { transaction_with_exception }
25
+
26
+ specify { transaction.should_receive(:convert_values_to_primitives!) }
27
+ end
28
+
29
+ after { subject }
30
+ end
31
+
32
+ describe "#has_transactions?" do
33
+ subject { aggregator.has_transactions? }
34
+
35
+ context "with an empty queue" do
36
+ it { should be_false }
37
+ end
38
+
39
+ context "with an non empty queue" do
40
+ before { aggregator.add(regular_transaction) }
41
+
42
+ it { should be_true }
43
+ end
44
+ end
45
+
46
+ describe "#post_processed_queue!" do
47
+ let(:transaction) { slow_transaction }
48
+ let(:other_transaction) { regular_transaction }
49
+ subject { aggregator.post_processed_queue! }
50
+ before do
51
+ aggregator.add(transaction)
52
+ aggregator.add(other_transaction)
53
+ end
54
+
55
+ it { should be_a Array }
56
+
57
+ it "calls to_hash on each transaction in the queue" do
58
+ transaction.should_receive(:to_hash)
59
+ other_transaction.should_receive(:to_hash)
60
+ subject
61
+ end
62
+ end
63
+
64
+ # protected
65
+
66
+ describe "#similar_slowest" do
67
+ subject { aggregator.send(:similar_slowest, transaction) }
68
+ before { aggregator.add(other_transaction) }
69
+
70
+ context "passing a transaction concerning a different action" do
71
+ let(:transaction) { slow_transaction }
72
+ let(:other_transaction) do
73
+ time = Time.parse('01-01-2001 10:01:00')
74
+ appsignal_transaction(
75
+ :process_action_event => notification_event(
76
+ :payload => create_payload(
77
+ :action => 'show',
78
+ :controller => 'SomeOtherController'
79
+ )
80
+ )
81
+ )
82
+ end
83
+
84
+ it { should be_nil }
85
+ end
86
+
87
+ context "passing concerning the same action" do
88
+ let(:transaction) { slow_transaction }
89
+ let(:other_transaction) { slower_transaction }
90
+
91
+ it { should == other_transaction }
92
+ end
93
+ end
94
+
95
+ describe "#pre_process_slowness!" do
96
+ subject { aggregator.send(:pre_process_slowness!, transaction) }
97
+
98
+ context "without a similar slow transaction" do
99
+ let(:transaction) { slow_transaction }
100
+
101
+ it "calls convert_values_to_primitives on transaction" do
102
+ transaction.should_receive(:convert_values_to_primitives!)
103
+ subject
104
+ end
105
+
106
+ it "indexes the slow transaction" do
107
+ expect { subject }.to change(aggregator, :slowness_index).
108
+ to({transaction.action => transaction})
109
+ end
110
+ end
111
+
112
+ context "with a non similar slow transaction" do
113
+ let(:transaction) { slow_transaction }
114
+ let(:other_transaction) do
115
+ time = Time.parse('01-01-2001 10:01:00')
116
+ appsignal_transaction(
117
+ :process_action_event => notification_event(
118
+ :action => 'foooo',
119
+ :start => time,
120
+ :ending => time + Appsignal.config[:slow_request_threshold] / 500.0
121
+ )
122
+ )
123
+ end
124
+
125
+ it "calls convert_values_to_primitives on transaction" do
126
+ transaction.should_receive(:convert_values_to_primitives!)
127
+ subject
128
+ end
129
+
130
+ it "indexes the slow transaction" do
131
+ expect { subject }.to change(aggregator, :slowness_index).to({
132
+ other_transaction.action => other_transaction,
133
+ transaction.action => transaction
134
+ })
135
+ end
136
+ end
137
+
138
+ context "with a similar but slower transaction" do
139
+ let(:transaction) { slow_transaction }
140
+ let(:slower) { slower_transaction }
141
+ before { aggregator.add(slower) }
142
+
143
+ it "calls truncate on the transaction" do
144
+ transaction.should_receive(:truncate!)
145
+ transaction.should_not_receive(:convert_values_to_primitives!)
146
+ subject
147
+ end
148
+
149
+ it "does not index the slow transaction" do
150
+ expect { subject }.to_not change(aggregator, :slowness_index)
151
+ end
152
+
153
+ it "does not modify the slow transaction" do
154
+ slower.should_not_receive(:truncate!)
155
+ slower.should_not_receive(:convert_values_to_primitives!)
156
+ subject
157
+ end
158
+ end
159
+
160
+ context "with a similar but faster transaction" do
161
+ let(:transaction) { slower_transaction }
162
+ let(:faster_transaction) { slow_transaction }
163
+ before { aggregator.add(faster_transaction) }
164
+
165
+ it "calls truncate on the faster transaction" do
166
+ faster_transaction.should_receive(:truncate!)
167
+ subject
168
+ end
169
+
170
+ it "calls convert_values_to_primitives on the (slower) transaction" do
171
+ transaction.should_receive(:convert_values_to_primitives!)
172
+ subject
173
+ end
174
+
175
+ it "indexes the slow transaction" do
176
+ expect { subject }.to change(aggregator, :slowness_index).
177
+ to({faster_transaction.action => transaction})
178
+ end
179
+ end
180
+ end
181
+
182
+ end
@@ -21,7 +21,8 @@ describe "Inactive Appsignal::Railtie" do
21
21
  end
22
22
  MyTempApp::Application.initialize!
23
23
 
24
- MyTempApp::Application.middleware.to_a.should_not include Appsignal::Middleware
24
+ MyTempApp::Application.middleware.to_a.
25
+ should_not include Appsignal::Listener
25
26
  end
26
27
  Process.wait(pid)
27
28
  raise 'Example failed' unless $?.exitstatus == 0
@@ -10,11 +10,11 @@ class AppWithError
10
10
  end
11
11
  end
12
12
 
13
- describe Appsignal::Middleware do
13
+ describe Appsignal::Listener do
14
14
  describe '#call' do
15
15
  let(:app) { stub(:call => true) }
16
16
  let(:env) { {'action_dispatch.request_id' => '1'} }
17
- let(:middleware) { Appsignal::Middleware.new(app, {})}
17
+ let(:middleware) { Appsignal::Listener.new(app, {})}
18
18
  let(:current) { stub(:complete! => true, :add_exception => true) }
19
19
  before { Appsignal::Transaction.stub(:current => current) }
20
20
 
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Appsignal::Middleware::ActionViewSanitizer do
4
+ let(:klass) { Appsignal::Middleware::ActionViewSanitizer }
5
+ let(:sanitizer) { klass.new }
6
+
7
+ describe "#call" do
8
+ before { Rails.root.stub(:to_s => '/var/www/app/20130101') }
9
+ let(:event) do
10
+ notification_event(
11
+ :name => 'render_partial.action_view',
12
+ :payload => create_payload(payload)
13
+ )
14
+ end
15
+ let(:payload) do
16
+ {
17
+ :identifier => '/var/www/app/20130101/app/views/home/index/html.erb'
18
+ }
19
+ end
20
+ subject { event.payload }
21
+ before { sanitizer.call(event) { } }
22
+
23
+ it "should strip Rails root from the path" do
24
+ payload[:identifier].should == 'app/views/home/index/html.erb'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+
3
+ describe Appsignal::Middleware::ActiveRecordSanitizer do
4
+ let(:klass) { Appsignal::Middleware::ActiveRecordSanitizer }
5
+ let(:sql_event_sanitizer) { klass.new }
6
+ let(:connection_config) { {} }
7
+ before do
8
+ ActiveRecord::Base.stub(
9
+ :connection_config => connection_config
10
+ )
11
+ end
12
+
13
+ describe "#call" do
14
+ let(:name) { 'Model load' }
15
+ let(:binds) { [] }
16
+ let(:event) do
17
+ notification_event(
18
+ :name => 'sql.active_record',
19
+ :payload => create_payload(
20
+ :name => name,
21
+ :sql => sql,
22
+ :binds => binds,
23
+ :connection_id => 1111
24
+ )
25
+ )
26
+ end
27
+ subject { event.payload[:sql] }
28
+ before { sql_event_sanitizer.call(event) { } }
29
+
30
+ context "connection id and bindings" do
31
+ let(:sql) { '' }
32
+ subject { event.payload }
33
+
34
+ it { should_not have_key(:connection_id) }
35
+ it { should_not have_key(:binds) }
36
+ end
37
+
38
+ context "with backtick table names" do
39
+ before { sql_event_sanitizer.stub(:adapter_uses_double_quoted_table_names? => false) }
40
+
41
+ context "single quoted data value" do
42
+ let(:sql) { "SELECT `table`.* FROM `table` WHERE `id` = 'secret'" }
43
+
44
+ it { should == "SELECT `table`.* FROM `table` WHERE `id` = ?" }
45
+
46
+ context "with an escaped single quote" do
47
+ let(:sql) { "`id` = '\\'big\\' secret'" }
48
+
49
+ it { should == "`id` = ?" }
50
+ end
51
+
52
+ context "with an escaped double quote" do
53
+ let(:sql) { "`id` = '\\\"big\\\" secret'" }
54
+
55
+ it { should == "`id` = ?" }
56
+ end
57
+ end
58
+
59
+ context "double quoted data value" do
60
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` = "secret"' }
61
+
62
+ it { should == 'SELECT `table`.* FROM `table` WHERE `id` = ?' }
63
+
64
+ context "with an escaped single quote" do
65
+ let(:sql) { '`id` = "\\\'big\\\' secret"' }
66
+
67
+ it { should == "`id` = ?" }
68
+ end
69
+
70
+ context "with an escaped double quote" do
71
+ let(:sql) { '`id` = "\\"big\\" secret"' }
72
+
73
+ it { should == "`id` = ?" }
74
+ end
75
+ end
76
+
77
+ context "numeric parameter" do
78
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` = 1' }
79
+
80
+ it { should == 'SELECT `table`.* FROM `table` WHERE `id` = ?' }
81
+ end
82
+
83
+ context "parameter array" do
84
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` IN (1, 2)' }
85
+
86
+ it { should == 'SELECT `table`.* FROM `table` WHERE `id` IN (?)' }
87
+ end
88
+ end
89
+
90
+ context "with double quote style table names and no prepared statements" do
91
+ let(:connection_config) { {:adapter => 'postgresql', :prepared_statements => false} }
92
+
93
+ context "single quoted data value" do
94
+ let(:sql) { "SELECT \"table\".* FROM \"table\" WHERE \"id\" = 'secret'" }
95
+
96
+ it { should == "SELECT \"table\".* FROM \"table\" WHERE \"id\" = ?" }
97
+
98
+ context "with an escaped single quote" do
99
+ let(:sql) { "\"id\" = '\\'big\\' secret'" }
100
+
101
+ it { should == "\"id\" = ?" }
102
+ end
103
+
104
+ context "with an escaped double quote" do
105
+ let(:sql) { "\"id\" = '\\\"big\\\" secret'" }
106
+
107
+ it { should == "\"id\" = ?" }
108
+ end
109
+ end
110
+
111
+ context "numeric parameter" do
112
+ let(:sql) { 'SELECT "table".* FROM "table" WHERE "id"=1' }
113
+
114
+ it { should == 'SELECT "table".* FROM "table" WHERE "id"=?' }
115
+ end
116
+ end
117
+
118
+ context "skip sanitization for prepared statements" do
119
+ let(:connection_config) { {:adapter => 'postgresql'} }
120
+
121
+ let(:sql) { 'SELECT "table".* FROM "table" WHERE "id"=$1' }
122
+
123
+ it { should == 'SELECT "table".* FROM "table" WHERE "id"=$1' }
124
+ end
125
+
126
+ context "skip sanitization for schema queries" do
127
+ let(:name) { 'SCHEMA' }
128
+ let(:sql) { 'SET client_min_messages TO 22' }
129
+
130
+ it { should == 'SET client_min_messages TO 22' }
131
+ end
132
+ end
133
+
134
+ describe "#schema_query?" do
135
+ let(:payload) { {} }
136
+ let(:event) { notification_event(:payload => payload) }
137
+ subject { sql_event_sanitizer.schema_query?(event) }
138
+
139
+ it { should be_false }
140
+
141
+ context "when name is schema" do
142
+ let(:payload) { {:name => 'SCHEMA'} }
143
+
144
+ it { should be_true }
145
+ end
146
+ end
147
+
148
+ context "connection config" do
149
+ describe "#connection_config" do
150
+ let(:connection_config) { {:adapter => 'adapter'} }
151
+
152
+ subject { sql_event_sanitizer.connection_config }
153
+
154
+ it { should == {:adapter => 'adapter'} }
155
+ end
156
+
157
+ describe "#adapter_uses_double_quoted_table_names?" do
158
+ subject { sql_event_sanitizer.adapter_uses_double_quoted_table_names? }
159
+
160
+ context "when using mysql" do
161
+ let(:connection_config) { {:adapter => 'mysql'} }
162
+
163
+ it { should be_false }
164
+ end
165
+
166
+ context "when using postgresql" do
167
+ let(:connection_config) { {:adapter => 'postgresql'} }
168
+
169
+ it { should be_true }
170
+ end
171
+
172
+ context "when using sqlite" do
173
+ let(:connection_config) { {:adapter => 'sqlite'} }
174
+
175
+ it { should be_true }
176
+ end
177
+ end
178
+
179
+ describe "adapter_uses_prepared_statements?" do
180
+ subject { sql_event_sanitizer.adapter_uses_prepared_statements? }
181
+
182
+ context "when using mysql" do
183
+ let(:connection_config) { {:adapter => 'mysql'} }
184
+
185
+ it { should be_false }
186
+ end
187
+
188
+ context "when using postgresql" do
189
+ let(:connection_config) { {:adapter => 'postgresql'} }
190
+
191
+ it { should be_true }
192
+ end
193
+
194
+ context "when using postgresql and prepared statements is disabled" do
195
+ let(:connection_config) { {:adapter => 'postgresql', :prepared_statements => false} }
196
+
197
+ it { should be_false }
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ describe Appsignal::Middleware do
4
+ let(:chain_klass) { Appsignal::Middleware::Chain }
5
+ let(:entry_klass) { Appsignal::Middleware::Entry }
6
+ let(:chain) { chain_klass.new }
7
+ let(:entry) { entry_klass.new(object, {:foo => :bar}) }
8
+
9
+ describe Appsignal::Middleware::Chain do
10
+ let(:object) { mock(:object) }
11
+ let(:other_object) { mock(:other_object) }
12
+
13
+ describe "#initialize" do
14
+ it "yields itself when passing a block" do
15
+ chain_klass.new { |o| o.should be_instance_of chain_klass }
16
+ end
17
+ end
18
+
19
+ describe "#remove" do
20
+ subject { chain.remove(object) }
21
+ before { chain.add(other_object) }
22
+
23
+ context "when it doesn't find the object" do
24
+ specify { expect { subject }.to_not raise_error }
25
+ end
26
+
27
+ context "when it finds the object" do
28
+ before { chain.add(object) }
29
+
30
+ specify { expect { subject }.to change(chain.entries, :count).by(-1) }
31
+ end
32
+ end
33
+
34
+ describe "#add" do
35
+ subject { chain.add(object) }
36
+
37
+ context "adding a new object" do
38
+ specify { expect { subject }.to change(chain.entries, :count).by(1) }
39
+ end
40
+
41
+ context "trying to add a duplicate (even with different arguments)" do
42
+ before { chain.add(object, :some_arguments) }
43
+
44
+ specify { expect { subject }.to_not change(chain.entries, :count).by(1) }
45
+ end
46
+ end
47
+
48
+ context "with a chain holding three items" do
49
+ subject { chain.entries.map(&:klass) }
50
+ before { [:first, :second, :third].each { |o| chain.add(o) } }
51
+
52
+ describe "#insert_before" do
53
+
54
+ context "before the second" do
55
+ before { chain.insert_before(:second, :object) }
56
+
57
+ it { should == [:first, :object, :second, :third] }
58
+ end
59
+
60
+ context "before the first" do
61
+ before { chain.insert_before(:first, :object) }
62
+
63
+ it { should == [:object, :first, :second, :third] }
64
+ end
65
+ end
66
+
67
+ describe "#insert_after" do
68
+
69
+ context "after the second" do
70
+ before { chain.insert_after(:second, :object) }
71
+
72
+ it { should == [:first, :second, :object, :third] }
73
+ end
74
+
75
+ context "after the third" do
76
+ before { chain.insert_after(:third, :object) }
77
+
78
+ it { should == [:first, :second, :third, :object] }
79
+ end
80
+ end
81
+
82
+ describe "#exists?" do
83
+ subject { chain.exists?(object) }
84
+
85
+ context "when it is in the chain" do
86
+ let(:object) { :first }
87
+
88
+ it { should be_true }
89
+ end
90
+
91
+ context "when it is not" do
92
+ let(:object) { :unknown }
93
+
94
+ it { should be_false }
95
+ end
96
+ end
97
+
98
+ describe "#retrieve" do
99
+ specify { chain.entries.each { |o| o.should_receive(:make_new) } }
100
+
101
+ after { chain.retrieve }
102
+ end
103
+
104
+ describe "#clear" do
105
+ before { chain.clear }
106
+ subject { chain.entries }
107
+
108
+ it { should be_empty }
109
+ end
110
+ end
111
+
112
+ describe "#invoke" do
113
+ let(:recorder) { [] }
114
+ subject { chain.invoke(:unsused) }
115
+ before(:all) do
116
+ TestInvoker1 = Struct.new(:id, :recorder) do
117
+ def call(event)
118
+ recorder << "Before#{id}"
119
+ yield
120
+ recorder << "After#{id}"
121
+ end
122
+ end
123
+ TestInvoker3 = Class.new(TestInvoker1)
124
+ end
125
+
126
+ context "all yielding entries" do
127
+ before do
128
+ TestInvoker2 = Class.new(TestInvoker1)
129
+
130
+ chain.add(TestInvoker1, 1, recorder)
131
+ chain.add(TestInvoker2, 2, recorder)
132
+ chain.add(TestInvoker3, 3, recorder)
133
+ end
134
+
135
+ it { should == %w(Before1 Before2 Before3 After3 After2 After1) }
136
+ end
137
+
138
+ context "a non yielding entry" do
139
+ before do
140
+ TestBlocker = Struct.new(:recorder) do
141
+ def call(event)
142
+ recorder << 'End-of-the-line!'
143
+ end
144
+ end
145
+
146
+ chain.add(TestInvoker1, 1, recorder)
147
+ chain.add(TestBlocker, recorder)
148
+ chain.add(TestInvoker3, 3, recorder)
149
+ end
150
+
151
+ it { should == %w(Before1 End-of-the-line! After1) }
152
+ end
153
+ end
154
+ end
155
+
156
+ describe Appsignal::Middleware::Entry do
157
+
158
+ describe "#make_new" do
159
+ let(:object) { mock }
160
+ subject { entry.make_new }
161
+
162
+ it "initializes the passed object" do
163
+ object.should_receive(:new)
164
+ subject
165
+ end
166
+ end
167
+ end
168
+ end