appsignal 1.0.7 → 1.1.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +5 -21
  4. data/Rakefile +2 -0
  5. data/circle.yml +2 -1
  6. data/ext/agent.yml +7 -7
  7. data/ext/appsignal_extension.c +3 -5
  8. data/ext/extconf.rb +6 -15
  9. data/gemfiles/grape.gemfile +7 -0
  10. data/lib/appsignal/config.rb +2 -5
  11. data/lib/appsignal/event_formatter.rb +0 -2
  12. data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +47 -1
  13. data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +29 -0
  14. data/lib/appsignal/event_formatter/moped/query_formatter.rb +7 -6
  15. data/lib/appsignal/hooks.rb +33 -0
  16. data/lib/appsignal/hooks/net_http.rb +1 -1
  17. data/lib/appsignal/hooks/sequel.rb +7 -4
  18. data/lib/appsignal/hooks/sidekiq.rb +10 -19
  19. data/lib/appsignal/integrations/capistrano/appsignal.cap +1 -1
  20. data/lib/appsignal/integrations/delayed_job_plugin.rb +20 -11
  21. data/lib/appsignal/integrations/grape.rb +44 -0
  22. data/lib/appsignal/integrations/mongo_ruby_driver.rb +5 -9
  23. data/lib/appsignal/integrations/railtie.rb +4 -0
  24. data/lib/appsignal/integrations/resque.rb +1 -1
  25. data/lib/appsignal/js_exception_transaction.rb +2 -3
  26. data/lib/appsignal/subscriber.rb +2 -3
  27. data/lib/appsignal/transaction.rb +2 -8
  28. data/lib/appsignal/transmitter.rb +1 -1
  29. data/lib/appsignal/utils.rb +7 -43
  30. data/lib/appsignal/version.rb +1 -1
  31. data/lib/tasks/diag.rake +75 -0
  32. data/spec/lib/appsignal/capistrano3_spec.rb +1 -21
  33. data/spec/lib/appsignal/config_spec.rb +0 -12
  34. data/spec/lib/appsignal/event_formatter/active_record/instantiation_formatter_spec.rb +1 -1
  35. data/spec/lib/appsignal/event_formatter/active_record/sql_formatter_spec.rb +186 -14
  36. data/spec/lib/appsignal/event_formatter/elastic_search/search_formatter_spec.rb +54 -0
  37. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +4 -4
  38. data/spec/lib/appsignal/extension_spec.rb +1 -1
  39. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +49 -14
  40. data/spec/lib/appsignal/hooks/sequel_spec.rb +1 -1
  41. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +29 -62
  42. data/spec/lib/appsignal/hooks_spec.rb +115 -0
  43. data/spec/lib/appsignal/integrations/grape_spec.rb +94 -0
  44. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +5 -8
  45. data/spec/lib/appsignal/integrations/resque_spec.rb +0 -1
  46. data/spec/lib/appsignal/js_exception_transaction_spec.rb +0 -1
  47. data/spec/lib/appsignal/subscriber_spec.rb +5 -23
  48. data/spec/lib/appsignal/transaction_spec.rb +0 -21
  49. data/spec/lib/appsignal/utils_spec.rb +1 -68
  50. data/spec/spec_helper.rb +16 -0
  51. data/spec/support/helpers/env_helpers.rb +0 -1
  52. data/spec/support/stubs/delayed_job.rb +0 -0
  53. metadata +15 -11
  54. data/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter.rb +0 -88
  55. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +0 -13
  56. data/spec/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter_spec.rb +0 -115
  57. data/spec/lib/appsignal/event_formatter/sequel/sql_formatter_spec.rb +0 -22
@@ -4,7 +4,7 @@ describe Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter do
4
4
  let(:klass) { Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter }
5
5
  let(:formatter) { klass.new }
6
6
 
7
- it "should register instantiation.active_record" do
7
+ it "should register request.net_http" do
8
8
  Appsignal::EventFormatter.registered?('instantiation.active_record', klass).should be_true
9
9
  end
10
10
 
@@ -1,23 +1,195 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter do
4
- let(:klass) { Appsignal::EventFormatter::ActiveRecord::SqlFormatter }
5
- let(:formatter) { klass.new }
3
+ if active_record_present?
4
+ require 'active_record'
6
5
 
7
- it "should register sql.active_record" do
8
- Appsignal::EventFormatter.registered?('sql.active_record', klass).should be_true
9
- end
6
+ describe Appsignal::EventFormatter::ActiveRecord::SqlFormatter do
7
+ let(:klass) { Appsignal::EventFormatter::ActiveRecord::SqlFormatter }
8
+ let(:formatter) { klass.new }
9
+ let(:connection_config) { {} }
10
+ before do
11
+ if ActiveRecord::Base.respond_to?(:connection_config)
12
+ # Rails 3.1+
13
+ ActiveRecord::Base.stub(
14
+ :connection_config => connection_config
15
+ )
16
+ else
17
+ # Rails 3.0
18
+ spec = double(:config => connection_config)
19
+ ActiveRecord::Base.stub(
20
+ :connection_pool => double(:spec => spec)
21
+ )
22
+ end
23
+ end
24
+
25
+ pending "should register sql.activerecord" do
26
+ Appsignal::EventFormatter.registered?('sql.active_record', klass).should be_true
27
+ end
28
+
29
+ context "if a connection cannot be established" do
30
+ before do
31
+ ActiveRecord::Base.stub(:connection_config).and_raise(ActiveRecord::ConnectionNotEstablished)
32
+ end
33
+
34
+ it "should log the error and unregister the formatter" do
35
+ Appsignal.logger.should_receive(:error).with(
36
+ 'Error while getting ActiveRecord connection info, unregistering sql.active_record event formatter'
37
+ )
38
+
39
+ lambda {
40
+ formatter
41
+ }.should_not raise_error
10
42
 
11
- describe "#format" do
12
- let(:payload) do
13
- {
14
- name: 'User load',
15
- sql: 'SELECT * FROM users'
16
- }
43
+ Appsignal::EventFormatter.registered?('sql.active_record').should be_false
44
+ end
17
45
  end
18
46
 
19
- subject { formatter.format(payload) }
47
+ describe "#format" do
48
+ let(:name) { 'Model load' }
49
+ let(:payload) { {:sql => sql, :name => name} }
50
+ subject { formatter.format(payload) }
51
+
52
+ context "with backtick table names" do
53
+ before { formatter.stub(:adapter_uses_double_quoted_table_names => false) }
54
+
55
+ context "single quoted data value" do
56
+ let(:sql) { "SELECT `table`.* FROM `table` WHERE `id` = 'secret' ORDER BY `table`.`id` ASC LIMIT 1" }
57
+
58
+ it { should == ['Model load', "SELECT `table`.* FROM `table` WHERE `id` = ? ORDER BY `table`.`id` ASC LIMIT ?"] }
59
+
60
+ context "with escaped single quotes in the string" do
61
+ let(:sql) { "`id` = 'this is a \'big\' secret'" }
62
+
63
+ it { should == ['Model load', "`id` = ?"] }
64
+ end
65
+ end
66
+
67
+ context "double quoted data value" do
68
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` = "secret"' }
69
+
70
+ it { should == ['Model load', 'SELECT `table`.* FROM `table` WHERE `id` = ?'] }
71
+
72
+ context "with escaped double quotes in the string" do
73
+ let(:sql) { '`id` = "this is a \"big\" secret"' }
74
+
75
+ it { should == ['Model load', "`id` = ?"] }
76
+ end
77
+ end
78
+
79
+ context "numeric parameter" do
80
+ context "integer" do
81
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` = 1' }
82
+
83
+ it { should == ['Model load', 'SELECT `table`.* FROM `table` WHERE `id` = ?'] }
84
+ end
85
+
86
+ context "float" do
87
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `value` = 10.0' }
88
+
89
+ it { should == ['Model load', 'SELECT `table`.* FROM `table` WHERE `value` = ?'] }
90
+ end
91
+ end
92
+
93
+ context "in operator with values" do
94
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` IN (1, 2)' }
95
+
96
+ it { should == ['Model load', 'SELECT `table`.* FROM `table` WHERE `id` IN (?)'] }
97
+ end
98
+
99
+ context "in operator with inner query" do
100
+ let(:sql) { 'SELECT `table`.* FROM `table` WHERE `id` IN (SELECT `id` from `other_table` WHERE `value` = 10.0)' }
101
+
102
+ it { should == ['Model load', 'SELECT `table`.* FROM `table` WHERE `id` IN (SELECT `id` from `other_table` WHERE `value` = ?)'] }
103
+ end
104
+ end
105
+
106
+ context "with double quote style table names" do
107
+ let(:connection_config) { {:adapter => 'postgresql'} }
108
+
109
+ context "single quoted data value" do
110
+ let(:sql) { "SELECT \"table\".* FROM \"table\" WHERE \"id\" = 'secret' ORDER BY \"table\".\"id\" ASC LIMIT 1" }
111
+
112
+ it { should == ['Model load', "SELECT \"table\".* FROM \"table\" WHERE \"id\" = ? ORDER BY \"table\".\"id\" ASC LIMIT ?"] }
20
113
 
21
- it { should == ['User load', 'SELECT * FROM users', 1] }
114
+ context "with an escaped single quote" do
115
+ let(:sql) { "\"id\" = 'this is a \'big\' secret'" }
116
+
117
+ it { should == ['Model load', "\"id\" = ?"] }
118
+ end
119
+ end
120
+
121
+ context "numeric parameter" do
122
+ context "integer" do
123
+ let(:sql) { 'SELECT "table".* FROM "table" WHERE "id"=1' }
124
+
125
+ it { should == ['Model load', 'SELECT "table".* FROM "table" WHERE "id"=?'] }
126
+ end
127
+
128
+ context "float" do
129
+ let(:sql) { 'SELECT "table".* FROM "table" WHERE "value"=10.0' }
130
+
131
+ it { should == ['Model load', 'SELECT "table".* FROM "table" WHERE "value"=?'] }
132
+ end
133
+ end
134
+ end
135
+
136
+ context "return nil for schema queries" do
137
+ let(:name) { 'SCHEMA' }
138
+ let(:sql) { 'SET client_min_messages TO 22' }
139
+
140
+ it { should be_nil }
141
+ end
142
+
143
+ context "with a a frozen sql string" do
144
+ let(:sql) { "SELECT `table`.* FROM `table` WHERE `id` = 'secret'".freeze }
145
+
146
+ it { should == ['Model load', "SELECT `table`.* FROM `table` WHERE `id` = ?"] }
147
+ end
148
+ end
149
+
150
+ describe "#schema_query?" do
151
+ let(:payload) { {} }
152
+ subject { formatter.send(:schema_query?, payload) }
153
+
154
+ it { should be_false }
155
+
156
+ context "when name is schema" do
157
+ let(:payload) { {:name => 'SCHEMA'} }
158
+
159
+ it { should be_true }
160
+ end
161
+ end
162
+
163
+ context "connection config" do
164
+ describe "#connection_config" do
165
+ let(:connection_config) { {:adapter => 'adapter'} }
166
+
167
+ subject { formatter.send(:connection_config) }
168
+
169
+ it { should == {:adapter => 'adapter'} }
170
+ end
171
+
172
+ describe "#adapter_uses_double_quoted_table_names" do
173
+ subject { formatter.adapter_uses_double_quoted_table_names }
174
+
175
+ context "when using mysql" do
176
+ let(:connection_config) { {:adapter => 'mysql'} }
177
+
178
+ it { should be_false }
179
+ end
180
+
181
+ context "when using postgresql" do
182
+ let(:connection_config) { {:adapter => 'postgresql'} }
183
+
184
+ it { should be_true }
185
+ end
186
+
187
+ context "when using sqlite" do
188
+ let(:connection_config) { {:adapter => 'sqlite'} }
189
+
190
+ it { should be_true }
191
+ end
192
+ end
193
+ end
22
194
  end
23
195
  end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Appsignal::EventFormatter::ElasticSearch::SearchFormatter do
4
+ let(:klass) { Appsignal::EventFormatter::ElasticSearch::SearchFormatter }
5
+ let(:formatter) { klass.new }
6
+
7
+ it "should register query.moped" do
8
+ expect(
9
+ Appsignal::EventFormatter.registered?('search.elasticsearch', klass)
10
+ ).to be_true
11
+ end
12
+
13
+ describe "#format" do
14
+ let(:payload) do
15
+ {
16
+ :name => 'Search',
17
+ :klass => 'User',
18
+ :search => {:index => 'users', :type => 'user', :q => 'John Doe'}
19
+ }
20
+ end
21
+
22
+ it "should return a payload with name and sanitized body" do
23
+ expect( formatter.format(payload) ).to eql([
24
+ "Search: User",
25
+ "{:index=>\"users\", :type=>\"user\", :q=>\"?\"}"
26
+ ])
27
+ end
28
+ end
29
+
30
+ describe "#sanitized_search" do
31
+ let(:search) do
32
+ {
33
+ :index => 'users',
34
+ :type => 'user',
35
+ :q => 'John Doe',
36
+ :other => 'Other'
37
+ }
38
+ end
39
+
40
+ it "should sanitize non-whitelisted params" do
41
+ expect(
42
+ formatter.sanitized_search(search)
43
+ ).to eql({:index => 'users', :type => 'user', :q => '?', :other => '?'})
44
+ end
45
+
46
+ it "should return nil string when search is nil" do
47
+ expect( formatter.sanitized_search(nil) ).to be_nil
48
+ end
49
+
50
+ it "should return nil string when search is not a hash" do
51
+ expect( formatter.sanitized_search([]) ).to be_nil
52
+ end
53
+ end
54
+ end
@@ -22,12 +22,12 @@ describe Appsignal::EventFormatter::Moped::QueryFormatter do
22
22
  let(:op) do
23
23
  double(
24
24
  :full_collection_name => 'database.collection',
25
- :selector => {'query' => {'_id' => 'abc'}},
25
+ :selector => {'_id' => 'abc'},
26
26
  :class => double(:to_s => 'Moped::Protocol::Command')
27
27
  )
28
28
  end
29
29
 
30
- it { should == ['Command', '{:database=>"database.collection", :selector=>{"query"=>"?"}}'] }
30
+ it { should == ['Command', '{:database=>"database.collection", :selector=>{"_id"=>"?"}}'] }
31
31
  end
32
32
 
33
33
  context "Moped::Protocol::Query" do
@@ -80,13 +80,13 @@ describe Appsignal::EventFormatter::Moped::QueryFormatter do
80
80
  double(
81
81
  :full_collection_name => 'database.collection',
82
82
  :selector => {'_id' => 'abc'},
83
- :update => {'user.name' => 'James Bond'},
83
+ :update => {'name' => 'James Bond'},
84
84
  :flags => [],
85
85
  :class => double(:to_s => 'Moped::Protocol::Update')
86
86
  )
87
87
  end
88
88
 
89
- it { should == ['Update', '{:database=>"database.collection", :selector=>{"_id"=>"?"}, :update=>{"user.?"=>"?"}, :flags=>[]}'] }
89
+ it { should == ['Update', '{:database=>"database.collection", :selector=>{"_id"=>"?"}, :update=>{"name"=>"?"}, :flags=>[]}'] }
90
90
  end
91
91
 
92
92
  context "Moped::Protocol::KillCursors" do
@@ -46,7 +46,7 @@ describe "extension loading and operation" do
46
46
  end
47
47
 
48
48
  it "should have a finish_event method" do
49
- subject.finish_event(1, 'name', 'title', 'body', 0)
49
+ subject.finish_event(1, 'name', 'title', 'body')
50
50
  end
51
51
 
52
52
  it "should have a set_transaction_error method" do
@@ -28,17 +28,18 @@ describe Appsignal::Hooks::DelayedJobHook do
28
28
  describe ".invoke_with_instrumentation" do
29
29
  let(:plugin) { Appsignal::Hooks::DelayedJobPlugin }
30
30
  let(:time) { Time.parse('01-01-2001 10:01:00UTC') }
31
- let(:job) do
32
- double(
31
+ let(:job_data) do
32
+ {
33
33
  :id => 123,
34
34
  :name => 'TestClass#perform',
35
35
  :priority => 1,
36
36
  :attempts => 1,
37
37
  :queue => 'default',
38
38
  :created_at => time - 60_000,
39
- :payload_object => double
40
- )
39
+ :payload_object => double(:args => ['argument']),
40
+ }
41
41
  end
42
+ let(:job) { double(job_data) }
42
43
  let(:invoked_block) { Proc.new { } }
43
44
  let(:error) { StandardError.new }
44
45
 
@@ -54,6 +55,7 @@ describe Appsignal::Hooks::DelayedJobHook do
54
55
  :queue => 'default',
55
56
  :id => '123'
56
57
  },
58
+ :params => ['argument'],
57
59
  :queue_start => time - 60_000,
58
60
  )
59
61
 
@@ -63,18 +65,19 @@ describe Appsignal::Hooks::DelayedJobHook do
63
65
  end
64
66
 
65
67
  context "with custom name call" do
66
- let(:job) do
67
- double(
68
+ let(:job_data) do
69
+ {
68
70
  :payload_object => double(
69
- :appsignal_name => 'CustomClass#perform'
71
+ :appsignal_name => 'CustomClass#perform',
72
+ :args => ['argument']
70
73
  ),
71
- :id => '123',
72
- :name => 'TestClass#perform',
73
- :priority => 1,
74
- :attempts => 1,
75
- :queue => 'default',
76
- :created_at => time - 60_000
77
- )
74
+ :id => '123',
75
+ :name => 'TestClass#perform',
76
+ :priority => 1,
77
+ :attempts => 1,
78
+ :queue => 'default',
79
+ :created_at => time - 60_000
80
+ }
78
81
  end
79
82
  it "should wrap in a transaction with the correct params" do
80
83
  Appsignal.should_receive(:monitor_transaction).with(
@@ -87,6 +90,7 @@ describe Appsignal::Hooks::DelayedJobHook do
87
90
  :queue => 'default',
88
91
  :id => '123'
89
92
  },
93
+ :params => ['argument'],
90
94
  :queue_start => time - 60_000
91
95
  )
92
96
 
@@ -95,6 +99,37 @@ describe Appsignal::Hooks::DelayedJobHook do
95
99
  end
96
100
  end
97
101
  end
102
+
103
+ if active_job_present?
104
+ require 'active_job'
105
+
106
+ context "when wrapped by ActiveJob" do
107
+ before do
108
+ job_data[:args] = ['argument']
109
+ end
110
+ let(:job) { ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(job_data) }
111
+
112
+ it "should wrap in a transaction with the correct params" do
113
+ Appsignal.should_receive(:monitor_transaction).with(
114
+ 'perform_job.delayed_job',
115
+ :class => 'TestClass',
116
+ :method => 'perform',
117
+ :metadata => {
118
+ :priority => 1,
119
+ :attempts => 1,
120
+ :queue => 'default',
121
+ :id => '123'
122
+ },
123
+ :params => ['argument'],
124
+ :queue_start => time - 60_000,
125
+ )
126
+
127
+ Timecop.freeze(time) do
128
+ plugin.invoke_with_instrumentation(job, invoked_block)
129
+ end
130
+ end
131
+ end
132
+ end
98
133
  end
99
134
 
100
135
  context "with an erroring call" do
@@ -15,7 +15,7 @@ describe "Sequel integration", if: sequel_present? do
15
15
  .at_least(:once)
16
16
  expect( Appsignal::Extension ).to receive(:finish_event)
17
17
  .at_least(:once)
18
- .with(kind_of(Integer), "sql.sequel", "", kind_of(String), 1)
18
+ .with(kind_of(Integer), "sql.sequel", "", "")
19
19
 
20
20
  db['SELECT 1'].all
21
21
  end
@@ -36,21 +36,35 @@ describe Appsignal::Hooks::SidekiqPlugin do
36
36
  )
37
37
  end
38
38
 
39
- it "reports the correct job class for a ActiveJob wrapped job" do
40
- item['wrapped'] = 'ActiveJobClass'
41
- Appsignal.should_receive(:monitor_transaction).with(
42
- 'perform_job.sidekiq',
43
- :class => 'ActiveJobClass',
44
- :method => 'perform',
45
- :metadata => {
46
- 'retry_count' => "0",
47
- 'queue' => 'default',
48
- 'extra' => 'data',
49
- 'wrapped' => 'ActiveJobClass'
50
- },
51
- :params => ['Model', "1"],
52
- :queue_start => Time.parse('01-01-2001 10:00:00UTC')
53
- )
39
+ context "when wrapped by ActiveJob" do
40
+ let(:item) {{
41
+ "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
42
+ "wrapped" => "TestClass",
43
+ "queue" => "default",
44
+ "args"=> [{
45
+ "job_class" => "TestJob",
46
+ "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
47
+ "queue_name" => "default",
48
+ "arguments" => ['Model', 1],
49
+ }],
50
+ "retry" => true,
51
+ "jid" => "efb140489485999d32b5504c",
52
+ "created_at" => Time.parse('01-01-2001 10:00:00UTC').to_f,
53
+ "enqueued_at" => Time.parse('01-01-2001 10:00:00UTC').to_f
54
+ }}
55
+
56
+ it "should wrap in a transaction with the correct params" do
57
+ Appsignal.should_receive(:monitor_transaction).with(
58
+ 'perform_job.sidekiq',
59
+ :class => 'TestClass',
60
+ :method => 'perform',
61
+ :metadata => {
62
+ 'queue' => 'default'
63
+ },
64
+ :params => ['Model', "1"],
65
+ :queue_start => Time.parse('01-01-2001 10:00:00UTC').to_f
66
+ )
67
+ end
54
68
  end
55
69
 
56
70
  after do
@@ -92,53 +106,6 @@ describe Appsignal::Hooks::SidekiqPlugin do
92
106
  plugin.formatted_metadata(item).should == {'foo' => 'bar'}
93
107
  end
94
108
  end
95
-
96
- describe "#format_args" do
97
- let(:object) { Object.new }
98
- let(:args) do
99
- [
100
- 'Model',
101
- 1,
102
- object
103
- ]
104
- end
105
-
106
- it "should format the arguments" do
107
- plugin.format_args(args).should == ['Model', '1', object.inspect]
108
- end
109
- end
110
-
111
- describe "#truncate" do
112
- let(:very_long_text) do
113
- "a" * 400
114
- end
115
-
116
- it "should truncate the text to 200 chars max" do
117
- plugin.truncate(very_long_text).should == "#{'a' * 197}..."
118
- end
119
- end
120
-
121
- describe "#string_or_inspect" do
122
- context "when string" do
123
- it "should return the string" do
124
- plugin.string_or_inspect('foo').should == 'foo'
125
- end
126
- end
127
-
128
- context "when integer" do
129
- it "should return the string" do
130
- plugin.string_or_inspect(1).should == '1'
131
- end
132
- end
133
-
134
- context "when object" do
135
- let(:object) { Object.new }
136
-
137
- it "should return the string" do
138
- plugin.string_or_inspect(object).should == object.inspect
139
- end
140
- end
141
- end
142
109
  end
143
110
 
144
111
  describe Appsignal::Hooks::SidekiqHook do