appsignal 1.0.7 → 1.1.0.beta.1

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