Skip to content

Commit f38f0cf

Browse files
committed
Add monitor instrumentation helpers
Add new "create a transaction" helpers to replace the `monitor_transaction` and `monitor_single_transaction` helpers. These helpers do too much and are confusing to use. We didn't end up using them ourselves anymore in our integrations. These new helpers are based on what we most commonly do in the Ruby gem to instrument blocks of code. These helpers don't accept adding metadata to the transaction using an `env` argument. This can now be set using the instrumentation helpers like `set_action`, `set_params`, `set_custom_data`, etc. If an app wants to track blocks of code with an instrumentation event, `Appsignal.instrument` needs to be called in the `Appsignal.monitor` block. If this helper were to both create a transaction and an instrumentation event the arguments would get complicated and it becomes unclear what it's meant to be used for. These helpers support nested transaction in such a way that it won't break anything, but the namespace and action arguments are ignored when a parent transaction is active.
1 parent 2b5f4d9 commit f38f0cf

File tree

4 files changed

+302
-11
lines changed

4 files changed

+302
-11
lines changed

.changesets/add-monitor-helper.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
bump: minor
3+
type: add
4+
---
5+
6+
Add `Appsignal.monitor` and `Appsignal.monitor_and_stop` instrumentation helpers. These helpers are a replacement for the `Appsignal.monitor_transaction` and `Appsignal.monitor_single_transaction` helpers.
7+
8+
Use these new helpers to create an AppSignal transaction and track any exceptions that occur within the instrumented block. This new helper supports custom namespaces and has a simpler way to set an action name. Use this helper in combination with our other `Appsignal.set_*` helpers to add more metadata to the transaction.
9+
10+
```ruby
11+
# New helper
12+
Appsignal.monitor(
13+
:namespace => "my_namespace",
14+
:action => "MyClass#my_method"
15+
) do
16+
# Track an instrumentation event
17+
Appsignal.instrument("my_event.my_group") do
18+
# Some code
19+
end
20+
end
21+
22+
# Old helper
23+
Appsignal.monitor_transaction(
24+
"process_action.my_group",
25+
:class_name => "MyClass",
26+
:action_name => "my_method"
27+
) do
28+
# Some code
29+
end
30+
```
31+
32+
The `Appsignal.monitor_and_stop` helper can be used in the same scenarios as the `Appsignal.monitor_single_transaction` helper is used. One-off Ruby scripts that are not part of a long running process.

lib/appsignal/helpers/instrumentation.rb

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,140 @@ module Helpers
55
module Instrumentation
66
include Appsignal::Utils::StdoutAndLoggerMessage
77

8+
# Monitor a block of code with AppSignal.
9+
#
10+
# This is a helper to create an AppSignal transaction, track any errors
11+
# that may occur and complete the transaction.
12+
#
13+
# This helper is recommended to be used in Ruby scripts and parts of an
14+
# app not already instrumented by AppSignal's automatic instrumentations.
15+
#
16+
# Use this helper in combination with our {.instrument} helper to track
17+
# instrumentation events.
18+
#
19+
# If AppSignal is not active ({Appsignal.active?}) it will still execute
20+
# the block, but not create a transaction for it.
21+
#
22+
# @example Instrument a block of code
23+
# Appsignal.monitor(
24+
# :namespace => "my_namespace",
25+
# :action => "MyClass#my_method"
26+
# ) do
27+
# # Some code
28+
# end
29+
#
30+
# @example Instrument a block of code with an instrumentation event
31+
# Appsignal.monitor(
32+
# :namespace => "my_namespace",
33+
# :action => "MyClass#my_method"
34+
# ) do
35+
# Appsignal.instrument("some_event.some_group") do
36+
# # Some code
37+
# end
38+
# end
39+
#
40+
# @example Set custom metadata on the transaction
41+
# Appsignal.monitor(
42+
# :namespace => "my_namespace",
43+
# :action => "MyClass#my_method"
44+
# ) do
45+
# # Some code
46+
#
47+
# Appsignal.set_tags(:tag1 => "value1", :tag2 => "value2")
48+
# Appsignal.set_params(:param1 => "value1", :param2 => "value2")
49+
# end
50+
#
51+
# @example Call monitor within monitor (not recommended)
52+
# Appsignal.monitor(
53+
# :namespace => "my_namespace",
54+
# :action => "MyClass#my_method"
55+
# ) do
56+
# # This will _not_ update the namespace and action name
57+
# Appsignal.monitor(
58+
# :namespace => "my_other_namespace",
59+
# :action => "MyOtherClass#my_other_method"
60+
# ) do
61+
# # Some code
62+
#
63+
# # The reported namespace will be "my_namespace"
64+
# # The reported action will be "MyClass#my_method"
65+
# end
66+
# end
67+
#
68+
# @param namespace [String/Symbol] The namespace to set on the new
69+
# transaction.
70+
# Defaults to the 'web' namespace.
71+
# This will update the already active transaction's namespace if
72+
# {.monitor} is called when another transaction is already active.
73+
# @param action [String/Symbol] The action name for the transaction.
74+
# This will update the already active transaction's action if
75+
# {.monitor} is called when another transaction is already active.
76+
# @yield The block to monitor.
77+
# @raise [Exception] Any exception that occurs within the given block is
78+
# re-raised by this method.
79+
# @return [Object] The value of the given block is returned.
80+
# @since 3.11.0
81+
def monitor(
82+
namespace: nil,
83+
action: nil
84+
)
85+
return yield unless active?
86+
87+
has_parent_transaction = Appsignal::Transaction.current?
88+
transaction =
89+
if has_parent_transaction
90+
Appsignal::Transaction.current
91+
else
92+
Appsignal::Transaction.create(namespace || Appsignal::Transaction::HTTP_REQUEST)
93+
end
94+
95+
begin
96+
yield if block_given?
97+
rescue Exception => error # rubocop:disable Lint/RescueException
98+
transaction.set_error(error)
99+
raise error
100+
ensure
101+
if has_parent_transaction
102+
if namespace
103+
callers = caller
104+
Appsignal::Utils::StdoutAndLoggerMessage.warning \
105+
"A parent transaction is active around this 'Appsignal.monitor' call. " \
106+
"The namespace is not updated for the parent transaction." \
107+
"If you want to update the namespace for this parent transaction, " \
108+
"call 'Appsignal.set_namespace' from within the 'Appsignal.monitor' block. " \
109+
"Update the 'Appsignal.monitor' call in: #{callers.first}"
110+
end
111+
if action
112+
callers = caller
113+
Appsignal::Utils::StdoutAndLoggerMessage.warning \
114+
"A parent transaction is active around this 'Appsignal.monitor' call. " \
115+
"The action is not updated for the parent transaction." \
116+
"If you want to update the action for this parent transaction, " \
117+
"call 'Appsignal.set_action' from within the 'Appsignal.monitor' block. " \
118+
"Update the 'Appsignal.monitor' call in: #{callers.first}"
119+
end
120+
else
121+
transaction.set_action_if_nil(action)
122+
Appsignal::Transaction.complete_current!
123+
end
124+
end
125+
end
126+
127+
# Instrument a block of code and stop AppSignal.
128+
#
129+
# Useful for cases such as one-off scripts where there is no long running
130+
# process active and the data needs to be sent after the process exists.
131+
#
132+
# Acts the same way as {.monitor}. See that method for more
133+
# documentation.
134+
#
135+
# @see monitor
136+
def monitor_and_stop(namespace: nil, action: nil)
137+
monitor(:namespace => namespace, :action => action)
138+
ensure
139+
Appsignal.stop("monitor_and_stop")
140+
end
141+
8142
# Creates an AppSignal transaction for the given block.
9143
#
10144
# If AppSignal is not {Appsignal.active?} it will still execute the
@@ -818,14 +952,11 @@ def instrument(
818952
name,
819953
title = nil,
820954
body = nil,
821-
body_format = Appsignal::EventFormatter::DEFAULT
955+
body_format = Appsignal::EventFormatter::DEFAULT,
956+
&block
822957
)
823-
Appsignal::Transaction.current.start_event
824-
yield if block_given?
825-
ensure
826-
Appsignal::Transaction
827-
.current
828-
.finish_event(name, title, body, body_format)
958+
Appsignal::Transaction.current
959+
.instrument(name, title, body, body_format, &block)
829960
end
830961

831962
# Instrumentation helper for SQL queries.

spec/lib/appsignal_spec.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,132 @@
358358
before { start_agent }
359359
around { |example| keep_transactions { example.run } }
360360

361+
describe ".monitor" do
362+
it "creates a transaction" do
363+
expect do
364+
Appsignal.monitor
365+
end.to(change { created_transactions.count }.by(1))
366+
367+
transaction = last_transaction
368+
expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
369+
expect(transaction).to_not have_action
370+
expect(transaction).to_not have_error
371+
expect(transaction).to_not include_events
372+
expect(transaction).to_not have_queue_start
373+
expect(transaction).to be_completed
374+
end
375+
376+
it "returns the block's return value" do
377+
expect(Appsignal.monitor { :return_value }).to eq(:return_value)
378+
end
379+
380+
it "sets a custom namespace via the namespace argument" do
381+
Appsignal.monitor(:namespace => "custom")
382+
383+
expect(last_transaction).to have_namespace("custom")
384+
end
385+
386+
it "doesn't overwrite custom namespace set in the block" do
387+
Appsignal.monitor(:namespace => "custom") do
388+
Appsignal.set_namespace("more custom")
389+
end
390+
391+
expect(last_transaction).to have_namespace("more custom")
392+
end
393+
394+
it "sets a custom action via the action argument" do
395+
Appsignal.monitor(:action => "custom")
396+
397+
expect(last_transaction).to have_action("custom")
398+
end
399+
400+
it "doesn't overwrite custom action set in the block" do
401+
Appsignal.monitor(:action => "custom") do
402+
Appsignal.set_action("more custom")
403+
end
404+
405+
expect(last_transaction).to have_action("more custom")
406+
end
407+
408+
it "reports exceptions that occur in the block" do
409+
expect do
410+
Appsignal.monitor do
411+
raise ExampleException, "error message"
412+
end
413+
end.to raise_error(ExampleException, "error message")
414+
415+
expect(last_transaction).to have_error("ExampleException", "error message")
416+
end
417+
418+
context "with already active transction" do
419+
let(:err_stream) { std_stream }
420+
let(:stderr) { err_stream.read }
421+
let(:transaction) { http_request_transaction }
422+
before do
423+
set_current_transaction(transaction)
424+
transaction.set_action("My action")
425+
end
426+
427+
it "doesn't create a new transaction" do
428+
expect do
429+
Appsignal.monitor
430+
end.to_not(change { created_transactions.count })
431+
end
432+
433+
it "does not overwrite the parent transaction's namespace" do
434+
logs =
435+
capture_logs do
436+
capture_std_streams(std_stream, err_stream) do
437+
Appsignal.monitor(:namespace => "custom")
438+
end
439+
end
440+
441+
expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
442+
warning =
443+
"A parent transaction is active around this 'Appsignal.monitor' call. " \
444+
"The namespace is not updated"
445+
expect(logs).to contains_log(:warn, warning)
446+
expect(stderr).to include("appsignal WARNING: #{warning}")
447+
end
448+
449+
it "does not overwrite the parent transaction's action" do
450+
logs =
451+
capture_logs do
452+
capture_std_streams(std_stream, err_stream) do
453+
Appsignal.monitor(:action => "custom")
454+
end
455+
end
456+
457+
expect(transaction).to have_action("My action")
458+
warning =
459+
"A parent transaction is active around this 'Appsignal.monitor' call. " \
460+
"The action is not updated"
461+
expect(logs).to contains_log(:warn, warning)
462+
expect(stderr).to include("appsignal WARNING: #{warning}")
463+
end
464+
465+
it "doesn't complete the parent transaction" do
466+
Appsignal.monitor
467+
468+
expect(transaction).to_not be_completed
469+
end
470+
end
471+
end
472+
473+
describe ".monitor_and_stop" do
474+
it "calls Appsignal.stop after the block" do
475+
allow(Appsignal).to receive(:stop)
476+
Appsignal.monitor_and_stop(:namespace => "custom", :action => "My Action")
477+
478+
transaction = last_transaction
479+
expect(transaction).to have_namespace("custom")
480+
expect(transaction).to have_action("My Action")
481+
expect(transaction).to be_completed
482+
483+
expect(Appsignal).to have_received(:stop).with("monitor_and_stop")
484+
end
485+
end
486+
361487
describe ".monitor_transaction" do
362488
context "with a successful call" do
363489
it "instruments and completes for a background job" do

spec/support/matchers/transaction.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,20 @@ def format_breadcrumb(action, category, message, metadata, time)
180180

181181
RSpec::Matchers.define :have_queue_start do |queue_start_time|
182182
match(:notify_expectation_failures => true) do |transaction|
183+
actual_start = transaction.ext.queue_start
183184
if queue_start_time
184-
expect(transaction.ext.queue_start).to eq(queue_start_time)
185+
expect(actual_start).to eq(queue_start_time)
185186
else
186-
expect(transaction.ext.queue_start).to_not be_nil
187+
expect(actual_start).to_not be_nil
187188
end
188189
end
189190

190191
match_when_negated(:notify_expectation_failures => true) do |transaction|
192+
actual_start = transaction.ext.queue_start
191193
if queue_start_time
192-
expect(transaction.ext.queue_start).to_not eq(queue_start_time)
194+
expect(actual_start).to_not eq(queue_start_time)
193195
else
194-
expect(transaction.ext.queue_start).to be_nil
196+
expect(actual_start).to be_nil
195197
end
196198
end
197199
end

0 commit comments

Comments
 (0)