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
@@ -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