appsignal 2.2.1 → 2.3.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/appsignal.gemspec +1 -2
  4. data/ext/agent.yml +11 -11
  5. data/ext/appsignal_extension.c +17 -1
  6. data/lib/appsignal/config.rb +7 -4
  7. data/lib/appsignal/hooks.rb +1 -6
  8. data/lib/appsignal/hooks/action_cable.rb +113 -0
  9. data/lib/appsignal/hooks/active_support_notifications.rb +12 -4
  10. data/lib/appsignal/hooks/shoryuken.rb +11 -11
  11. data/lib/appsignal/hooks/sidekiq.rb +5 -3
  12. data/lib/appsignal/integrations/delayed_job_plugin.rb +5 -1
  13. data/lib/appsignal/integrations/resque_active_job.rb +4 -1
  14. data/lib/appsignal/transaction.rb +40 -13
  15. data/lib/appsignal/version.rb +1 -1
  16. data/spec/lib/appsignal/config_spec.rb +8 -1
  17. data/spec/lib/appsignal/hooks/action_cable_spec.rb +370 -0
  18. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +39 -7
  19. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +179 -34
  20. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +125 -30
  21. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +140 -21
  22. data/spec/lib/appsignal/hooks_spec.rb +0 -21
  23. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +62 -17
  24. data/spec/lib/appsignal/integrations/resque_spec.rb +24 -12
  25. data/spec/lib/appsignal/transaction_spec.rb +230 -91
  26. data/spec/spec_helper.rb +8 -2
  27. data/spec/support/helpers/dependency_helper.rb +4 -0
  28. data/spec/support/helpers/env_helpers.rb +1 -1
  29. data/spec/support/helpers/log_helpers.rb +17 -0
  30. data/spec/support/matchers/contains_log.rb +7 -0
  31. data/spec/support/shared_examples/instrument.rb +2 -2
  32. metadata +13 -20
@@ -1,5 +1,5 @@
1
1
  require "yaml"
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.2.1".freeze
4
+ VERSION = "2.3.0.beta.1".freeze
5
5
  end
@@ -84,8 +84,9 @@ describe Appsignal::Config do
84
84
  expect(config.config_hash).to eq(
85
85
  :debug => false,
86
86
  :log => "file",
87
- :ignore_errors => [],
88
87
  :ignore_actions => [],
88
+ :ignore_errors => [],
89
+ :ignore_namespaces => [],
89
90
  :filter_parameters => [],
90
91
  :instrument_net_http => true,
91
92
  :instrument_redis => true,
@@ -279,6 +280,8 @@ describe Appsignal::Config do
279
280
  ENV["APPSIGNAL_APP_NAME"] = "App name"
280
281
  ENV["APPSIGNAL_DEBUG"] = "true"
281
282
  ENV["APPSIGNAL_IGNORE_ACTIONS"] = "action1,action2"
283
+ ENV["APPSIGNAL_IGNORE_ERRORS"] = "VerySpecificError,AnotherError"
284
+ ENV["APPSIGNAL_IGNORE_NAMESPACES"] = "admin,private_namespace"
282
285
  ENV["APPSIGNAL_INSTRUMENT_NET_HTTP"] = "false"
283
286
  ENV["APPSIGNAL_INSTRUMENT_REDIS"] = "false"
284
287
  ENV["APPSIGNAL_INSTRUMENT_SEQUEL"] = "false"
@@ -294,6 +297,8 @@ describe Appsignal::Config do
294
297
  expect(config[:name]).to eq "App name"
295
298
  expect(config[:debug]).to be_truthy
296
299
  expect(config[:ignore_actions]).to eq %w(action1 action2)
300
+ expect(config[:ignore_errors]).to eq %w(VerySpecificError AnotherError)
301
+ expect(config[:ignore_namespaces]).to eq %w(admin private_namespace)
297
302
  expect(config[:instrument_net_http]).to be_falsey
298
303
  expect(config[:instrument_redis]).to be_falsey
299
304
  expect(config[:instrument_sequel]).to be_falsey
@@ -366,6 +371,7 @@ describe Appsignal::Config do
366
371
  config[:http_proxy] = "http://localhost"
367
372
  config[:ignore_actions] = %w(action1 action2)
368
373
  config[:ignore_errors] = %w(VerySpecificError AnotherError)
374
+ config[:ignore_namespaces] = %w(admin private_namespace)
369
375
  config[:log] = "stdout"
370
376
  config[:log_path] = "/tmp"
371
377
  config[:hostname] = "app1.local"
@@ -391,6 +397,7 @@ describe Appsignal::Config do
391
397
  expect(ENV["_APPSIGNAL_HTTP_PROXY"]).to eq "http://localhost"
392
398
  expect(ENV["_APPSIGNAL_IGNORE_ACTIONS"]).to eq "action1,action2"
393
399
  expect(ENV["_APPSIGNAL_IGNORE_ERRORS"]).to eq "VerySpecificError,AnotherError"
400
+ expect(ENV["_APPSIGNAL_IGNORE_NAMESPACES"]).to eq "admin,private_namespace"
394
401
  expect(ENV["_APPSIGNAL_FILTER_PARAMETERS"]).to eq "password,confirm_password"
395
402
  expect(ENV["_APPSIGNAL_SEND_PARAMS"]).to eq "true"
396
403
  expect(ENV["_APPSIGNAL_RUNNING_IN_CONTAINER"]).to eq "false"
@@ -0,0 +1,370 @@
1
+ describe Appsignal::Hooks::ActionCableHook do
2
+ if DependencyHelper.action_cable_present?
3
+ context "with ActionCable" do
4
+ require "action_cable/engine"
5
+
6
+ describe ".dependencies_present?" do
7
+ subject { described_class.new.dependencies_present? }
8
+
9
+ it "returns true" do
10
+ is_expected.to be_truthy
11
+ end
12
+ end
13
+
14
+ describe ActionCable::Channel::Base do
15
+ let(:transaction) do
16
+ Appsignal::Transaction.new(
17
+ transaction_id,
18
+ Appsignal::Transaction::ACTION_CABLE,
19
+ ActionDispatch::Request.new(env)
20
+ )
21
+ end
22
+ let(:channel) do
23
+ Class.new(ActionCable::Channel::Base) do
24
+ def speak(_data)
25
+ end
26
+
27
+ def self.to_s
28
+ "MyChannel"
29
+ end
30
+ end
31
+ end
32
+ let(:log) { StringIO.new }
33
+ let(:server) do
34
+ ActionCable::Server::Base.new.tap do |s|
35
+ s.config.logger = ActiveSupport::Logger.new(log)
36
+ end
37
+ end
38
+ let(:connection) { ActionCable::Connection::Base.new(server, env) }
39
+ let(:identifier) { { :channel => "MyChannel" }.to_json }
40
+ let(:params) { {} }
41
+ let(:request_id) { SecureRandom.uuid }
42
+ let(:transaction_id) { request_id }
43
+ let(:env) do
44
+ http_request_env_with_data("action_dispatch.request_id" => request_id, :params => params)
45
+ end
46
+ let(:instance) { channel.new(connection, identifier, params) }
47
+ subject { transaction.to_h }
48
+ before do
49
+ start_agent
50
+ expect(Appsignal.active?).to be_truthy
51
+ transaction
52
+
53
+ expect(Appsignal::Transaction).to receive(:create)
54
+ .with(transaction_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request))
55
+ .and_return(transaction)
56
+ allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
57
+ # Make sure sample data is added
58
+ expect(transaction.ext).to receive(:finish).and_return(true)
59
+ # Stub complete call, stops it from being cleared in the extension
60
+ # And allows us to call `#to_h` on it after it's been completed.
61
+ expect(transaction.ext).to receive(:complete)
62
+
63
+ # Stub transmit call for subscribe/unsubscribe tests
64
+ allow(connection).to receive(:websocket)
65
+ .and_return(instance_double("ActionCable::Connection::WebSocket", :transmit => nil))
66
+ end
67
+
68
+ describe "#perform_action" do
69
+ it "creates a transaction for an action" do
70
+ instance.perform_action("message" => "foo", "action" => "speak")
71
+
72
+ expect(subject).to include(
73
+ "action" => "MyChannel#speak",
74
+ "error" => nil,
75
+ "id" => transaction_id,
76
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
77
+ "metadata" => {
78
+ "method" => "websocket",
79
+ "path" => "/blog"
80
+ }
81
+ )
82
+ expect(subject["events"].first).to include(
83
+ "allocation_count" => kind_of(Integer),
84
+ "body" => "",
85
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
86
+ "child_allocation_count" => kind_of(Integer),
87
+ "child_duration" => kind_of(Float),
88
+ "child_gc_duration" => kind_of(Float),
89
+ "count" => 1,
90
+ "gc_duration" => kind_of(Float),
91
+ "start" => kind_of(Float),
92
+ "duration" => kind_of(Float),
93
+ "name" => "perform_action.action_cable",
94
+ "title" => ""
95
+ )
96
+ expect(subject["sample_data"]).to include(
97
+ "params" => {
98
+ "action" => "speak",
99
+ "message" => "foo"
100
+ }
101
+ )
102
+ end
103
+
104
+ context "without request_id (standalone server)" do
105
+ let(:request_id) { nil }
106
+ let(:transaction_id) { SecureRandom.uuid }
107
+ let(:action_transaction) do
108
+ Appsignal::Transaction.new(
109
+ transaction_id,
110
+ Appsignal::Transaction::ACTION_CABLE,
111
+ ActionDispatch::Request.new(env)
112
+ )
113
+ end
114
+ before do
115
+ # Stub future (private AppSignal) transaction id generated by the hook.
116
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
117
+ end
118
+
119
+ it "uses its own internal request_id set by the subscribed callback" do
120
+ # Subscribe action, sets the request_id
121
+ instance.subscribe_to_channel
122
+ expect(transaction.to_h["id"]).to eq(transaction_id)
123
+
124
+ # Expect another transaction for the action.
125
+ # This transaction will use the same request_id as the
126
+ # transaction id used to subscribe to the channel.
127
+ expect(Appsignal::Transaction).to receive(:create).with(
128
+ transaction_id,
129
+ Appsignal::Transaction::ACTION_CABLE,
130
+ kind_of(ActionDispatch::Request)
131
+ ).and_return(action_transaction)
132
+ allow(Appsignal::Transaction).to receive(:current).and_return(action_transaction)
133
+ # Stub complete call, stops it from being cleared in the extension
134
+ # And allows us to call `#to_h` on it after it's been completed.
135
+ expect(action_transaction.ext).to receive(:complete)
136
+
137
+ instance.perform_action("message" => "foo", "action" => "speak")
138
+ expect(action_transaction.to_h["id"]).to eq(transaction_id)
139
+ end
140
+ end
141
+
142
+ context "with an error in the action" do
143
+ let(:channel) do
144
+ Class.new(ActionCable::Channel::Base) do
145
+ def speak(_data)
146
+ raise VerySpecificError, "oh no!"
147
+ end
148
+
149
+ def self.to_s
150
+ "MyChannel"
151
+ end
152
+ end
153
+ end
154
+
155
+ it "registers an error on the transaction" do
156
+ expect do
157
+ instance.perform_action("message" => "foo", "action" => "speak")
158
+ end.to raise_error(VerySpecificError)
159
+
160
+ expect(subject).to include(
161
+ "action" => "MyChannel#speak",
162
+ "id" => transaction_id,
163
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
164
+ "metadata" => {
165
+ "method" => "websocket",
166
+ "path" => "/blog"
167
+ }
168
+ )
169
+ expect(subject["error"]).to include(
170
+ "backtrace" => kind_of(String),
171
+ "name" => "VerySpecificError",
172
+ "message" => "oh no!"
173
+ )
174
+ expect(subject["sample_data"]).to include(
175
+ "params" => {
176
+ "action" => "speak",
177
+ "message" => "foo"
178
+ }
179
+ )
180
+ end
181
+ end
182
+ end
183
+
184
+ describe "subscribe callback" do
185
+ let(:params) { { "internal" => true } }
186
+
187
+ it "creates a transaction for a subscription" do
188
+ instance.subscribe_to_channel
189
+
190
+ expect(subject).to include(
191
+ "action" => "MyChannel#subscribed",
192
+ "error" => nil,
193
+ "id" => transaction_id,
194
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
195
+ "metadata" => {
196
+ "method" => "websocket",
197
+ "path" => "/blog"
198
+ }
199
+ )
200
+ expect(subject["events"].first).to include(
201
+ "allocation_count" => kind_of(Integer),
202
+ "body" => "",
203
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
204
+ "child_allocation_count" => kind_of(Integer),
205
+ "child_duration" => kind_of(Float),
206
+ "child_gc_duration" => kind_of(Float),
207
+ "count" => 1,
208
+ "gc_duration" => kind_of(Float),
209
+ "start" => kind_of(Float),
210
+ "duration" => kind_of(Float),
211
+ "name" => "subscribed.action_cable",
212
+ "title" => ""
213
+ )
214
+ expect(subject["sample_data"]).to include(
215
+ "params" => { "internal" => "true" }
216
+ )
217
+ end
218
+
219
+ context "without request_id (standalone server)" do
220
+ let(:request_id) { nil }
221
+ let(:transaction_id) { SecureRandom.uuid }
222
+ before do
223
+ allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
224
+ instance.subscribe_to_channel
225
+ end
226
+
227
+ it "uses its own internal request_id" do
228
+ expect(subject["id"]).to eq(transaction_id)
229
+ end
230
+ end
231
+
232
+ context "with an error in the callback" do
233
+ let(:channel) do
234
+ Class.new(ActionCable::Channel::Base) do
235
+ def subscribed
236
+ raise VerySpecificError, "oh no!"
237
+ end
238
+
239
+ def self.to_s
240
+ "MyChannel"
241
+ end
242
+ end
243
+ end
244
+
245
+ it "registers an error on the transaction" do
246
+ expect do
247
+ instance.subscribe_to_channel
248
+ end.to raise_error(VerySpecificError)
249
+
250
+ expect(subject).to include(
251
+ "action" => "MyChannel#subscribed",
252
+ "id" => transaction_id,
253
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
254
+ "metadata" => {
255
+ "method" => "websocket",
256
+ "path" => "/blog"
257
+ }
258
+ )
259
+ expect(subject["error"]).to include(
260
+ "backtrace" => kind_of(String),
261
+ "name" => "VerySpecificError",
262
+ "message" => "oh no!"
263
+ )
264
+ expect(subject["sample_data"]).to include(
265
+ "params" => { "internal" => "true" }
266
+ )
267
+ end
268
+ end
269
+ end
270
+
271
+ describe "unsubscribe callback" do
272
+ let(:params) { { "internal" => true } }
273
+
274
+ it "creates a transaction for a subscription" do
275
+ instance.unsubscribe_from_channel
276
+
277
+ expect(subject).to include(
278
+ "action" => "MyChannel#unsubscribed",
279
+ "error" => nil,
280
+ "id" => transaction_id,
281
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
282
+ "metadata" => {
283
+ "method" => "websocket",
284
+ "path" => "/blog"
285
+ }
286
+ )
287
+ expect(subject["events"].first).to include(
288
+ "allocation_count" => kind_of(Integer),
289
+ "body" => "",
290
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
291
+ "child_allocation_count" => kind_of(Integer),
292
+ "child_duration" => kind_of(Float),
293
+ "child_gc_duration" => kind_of(Float),
294
+ "count" => 1,
295
+ "gc_duration" => kind_of(Float),
296
+ "start" => kind_of(Float),
297
+ "duration" => kind_of(Float),
298
+ "name" => "unsubscribed.action_cable",
299
+ "title" => ""
300
+ )
301
+ expect(subject["sample_data"]).to include(
302
+ "params" => { "internal" => "true" }
303
+ )
304
+ end
305
+
306
+ context "without request_id (standalone server)" do
307
+ let(:request_id) { nil }
308
+ let(:transaction_id) { SecureRandom.uuid }
309
+ before do
310
+ allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
311
+ instance.unsubscribe_from_channel
312
+ end
313
+
314
+ it "uses its own internal request_id" do
315
+ expect(subject["id"]).to eq(transaction_id)
316
+ end
317
+ end
318
+
319
+ context "with an error in the callback" do
320
+ let(:channel) do
321
+ Class.new(ActionCable::Channel::Base) do
322
+ def unsubscribed
323
+ raise VerySpecificError, "oh no!"
324
+ end
325
+
326
+ def self.to_s
327
+ "MyChannel"
328
+ end
329
+ end
330
+ end
331
+
332
+ it "registers an error on the transaction" do
333
+ expect do
334
+ instance.unsubscribe_from_channel
335
+ end.to raise_error(VerySpecificError)
336
+
337
+ expect(subject).to include(
338
+ "action" => "MyChannel#unsubscribed",
339
+ "id" => transaction_id,
340
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
341
+ "metadata" => {
342
+ "method" => "websocket",
343
+ "path" => "/blog"
344
+ }
345
+ )
346
+ expect(subject["error"]).to include(
347
+ "backtrace" => kind_of(String),
348
+ "name" => "VerySpecificError",
349
+ "message" => "oh no!"
350
+ )
351
+ expect(subject["sample_data"]).to include(
352
+ "params" => { "internal" => "true" }
353
+ )
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
359
+ else
360
+ context "without ActionCable" do
361
+ describe ".dependencies_present?" do
362
+ subject { described_class.new.dependencies_present? }
363
+
364
+ it "returns false" do
365
+ is_expected.to be_falsy
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -1,45 +1,77 @@
1
1
  describe Appsignal::Hooks::ActiveSupportNotificationsHook do
2
2
  if active_support_present?
3
+ let(:notifier) { ActiveSupport::Notifications::Fanout.new }
4
+ let(:as) { ActiveSupport::Notifications }
3
5
  before :context do
4
6
  start_agent
5
7
  end
6
8
  before do
9
+ as.notifier = notifier
7
10
  Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
8
11
  end
9
12
 
10
- let(:notifier) { ActiveSupport::Notifications::Fanout.new }
11
- let(:instrumenter) { ActiveSupport::Notifications::Instrumenter.new(notifier) }
12
-
13
13
  describe "#dependencies_present?" do
14
14
  subject { described_class.new.dependencies_present? }
15
15
 
16
16
  it { is_expected.to be_truthy }
17
17
  end
18
18
 
19
- it "should instrument an AS notifications instrument call with a block" do
19
+ it "instruments an ActiveSupport::Notifications.instrument event" do
20
20
  expect(Appsignal::Transaction.current).to receive(:start_event)
21
21
  .at_least(:once)
22
22
  expect(Appsignal::Transaction.current).to receive(:finish_event)
23
23
  .at_least(:once)
24
24
  .with("sql.active_record", nil, "SQL", 1)
25
25
 
26
- return_value = instrumenter.instrument("sql.active_record", :sql => "SQL") do
26
+ return_value = as.instrument("sql.active_record", :sql => "SQL") do
27
27
  "value"
28
28
  end
29
29
 
30
30
  expect(return_value).to eq "value"
31
31
  end
32
32
 
33
- it "should not instrument events whose name starts with a bang" do
33
+ it "does not instrument events whose name starts with a bang" do
34
34
  expect(Appsignal::Transaction.current).not_to receive(:start_event)
35
35
  expect(Appsignal::Transaction.current).not_to receive(:finish_event)
36
36
 
37
- return_value = instrumenter.instrument("!sql.active_record", :sql => "SQL") do
37
+ return_value = as.instrument("!sql.active_record", :sql => "SQL") do
38
38
  "value"
39
39
  end
40
40
 
41
41
  expect(return_value).to eq "value"
42
42
  end
43
+
44
+ context "when an error is raised in an instrumented block" do
45
+ it "instruments an ActiveSupport::Notifications.instrument event" do
46
+ expect(Appsignal::Transaction.current).to receive(:start_event)
47
+ .at_least(:once)
48
+ expect(Appsignal::Transaction.current).to receive(:finish_event)
49
+ .at_least(:once)
50
+ .with("sql.active_record", nil, "SQL", 1)
51
+
52
+ expect do
53
+ as.instrument("sql.active_record", :sql => "SQL") do
54
+ raise VerySpecificError, "foo"
55
+ end
56
+ end.to raise_error(VerySpecificError, "foo")
57
+ end
58
+ end
59
+
60
+ context "when a message is thrown in an instrumented block" do
61
+ it "instruments an ActiveSupport::Notifications.instrument event" do
62
+ expect(Appsignal::Transaction.current).to receive(:start_event)
63
+ .at_least(:once)
64
+ expect(Appsignal::Transaction.current).to receive(:finish_event)
65
+ .at_least(:once)
66
+ .with("sql.active_record", nil, "SQL", 1)
67
+
68
+ expect do
69
+ as.instrument("sql.active_record", :sql => "SQL") do
70
+ throw :foo
71
+ end
72
+ end.to throw_symbol(:foo)
73
+ end
74
+ end
43
75
  else
44
76
  describe "#dependencies_present?" do
45
77
  subject { described_class.new.dependencies_present? }