appoptics_apm 4.1.2 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -6
- data/Dockerfile_test +4 -2
- data/Rakefile +15 -4
- data/appoptics_apm.gemspec +3 -1
- data/examples/DNT.md +35 -0
- data/examples/carrying_context.rb +220 -0
- data/examples/instrumenting_metal_controller.rb +8 -0
- data/examples/puma_on_heroku_config.rb +17 -0
- data/examples/tracing_async_threads.rb +124 -0
- data/examples/tracing_background_jobs.rb +53 -0
- data/examples/tracing_forked_processes.rb +99 -0
- data/examples/unicorn_on_heroku_config.rb +28 -0
- data/ext/oboe_metal/src/VERSION +1 -1
- data/lib/appoptics_apm.rb +2 -0
- data/lib/appoptics_apm/api/logging.rb +13 -12
- data/lib/appoptics_apm/api/tracing.rb +28 -1
- data/lib/appoptics_apm/base.rb +7 -1
- data/lib/appoptics_apm/config.rb +15 -13
- data/lib/appoptics_apm/frameworks/grape.rb +7 -6
- data/lib/appoptics_apm/inst/rack.rb +102 -69
- data/lib/appoptics_apm/noop/README.md +9 -0
- data/lib/appoptics_apm/noop/context.rb +19 -0
- data/lib/appoptics_apm/support.rb +17 -18
- data/lib/appoptics_apm/version.rb +2 -2
- data/lib/rails/generators/appoptics_apm/templates/appoptics_apm_initializer.rb +8 -0
- data/run_tests_docker.rb +0 -6
- metadata +12 -2
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler.require
|
5
|
+
|
6
|
+
# Make sure oboe is at the bottom of your Gemfile.
|
7
|
+
# This is likely redundant but just in case.
|
8
|
+
require 'oboe'
|
9
|
+
|
10
|
+
# Tracing mode can be 'never' or 'always'
|
11
|
+
AppOpticsAPM::Config[:tracing_mode] = 'always'
|
12
|
+
|
13
|
+
#
|
14
|
+
# Update April 9, 2015 - this is done automagically now
|
15
|
+
# and doesn't have to be called manually
|
16
|
+
#
|
17
|
+
# Load library instrumentation to auto-capture stuff we know about...
|
18
|
+
# e.g. ActiveRecord, Cassandra, Dalli, Redis, Memcache, Mongo
|
19
|
+
# AppOpticsAPM::Ruby.load
|
20
|
+
|
21
|
+
# Some KVs to report to the dashboard
|
22
|
+
report_kvs = {}
|
23
|
+
report_kvs[:command_line_params] = ARGV.to_s
|
24
|
+
report_kvs[:user_id] = `whoami`
|
25
|
+
|
26
|
+
AppOpticsAPM::API.start_trace('my_background_job', nil, report_kvs) do
|
27
|
+
#
|
28
|
+
# Initialization code
|
29
|
+
#
|
30
|
+
|
31
|
+
tasks = get_all_tasks
|
32
|
+
|
33
|
+
tasks.each do |t|
|
34
|
+
# Optional: Here we embed another 'trace' to separate actual
|
35
|
+
# work for each task. In the APPOPTICS dashboard, this will show
|
36
|
+
# up as a large 'my_background_job' parent layer with many
|
37
|
+
# child 'task" layers.
|
38
|
+
AppOpticsAPM::API.trace('task', :task_id => t.id) do
|
39
|
+
t.perform
|
40
|
+
end
|
41
|
+
end
|
42
|
+
#
|
43
|
+
# cleanup code
|
44
|
+
#
|
45
|
+
end
|
46
|
+
|
47
|
+
# Note that we use 'start_trace' in the outer block and 'trace' for
|
48
|
+
# any sub-blocks of code we wish to instrument. The arguments for
|
49
|
+
# both methods vary slightly.
|
50
|
+
#
|
51
|
+
# TODO update location of the following doc
|
52
|
+
# Details in RubyDoc:
|
53
|
+
# https://www.omniref.com/ruby/gems/oboe/2.7.10.1/symbols/AppOpticsAPM::API::Tracing#tab=Methods
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#
|
2
|
+
# This sample demonstrates how to instrument a main loop that
|
3
|
+
# retrieves work and calls fork to do the actual work
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'math'
|
7
|
+
require 'oboe'
|
8
|
+
|
9
|
+
AppOpticsAPM::Config[:tracing_mode] = :always
|
10
|
+
AppOpticsAPM::Config[:verbose] = true
|
11
|
+
|
12
|
+
# The parent process/loop which collects data
|
13
|
+
Kernel.loop do
|
14
|
+
# For each loop, we instrument the work retrieval. These traces
|
15
|
+
# will show up as layer 'get_the_work'.
|
16
|
+
AppOpticsAPM::API.start_trace('get_the_work') do
|
17
|
+
work = get_the_work
|
18
|
+
|
19
|
+
# Loop through work and pass to `do_the_work` method
|
20
|
+
# that spawns a thread each time
|
21
|
+
work.each do |job|
|
22
|
+
fork do
|
23
|
+
# Since the context is copied from the parent process, we clear it
|
24
|
+
# and start a new trace via `AppOpticsAPM::API.start_trace`.
|
25
|
+
AppOpticsAPM::Context.clear
|
26
|
+
result = nil
|
27
|
+
|
28
|
+
AppOpticsAPM::API.start_trace('do_the_work', nil, :job_id => job.id) do
|
29
|
+
result = do_the_work(job)
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# get_the_work
|
40
|
+
#
|
41
|
+
# Method to retrieve work to do
|
42
|
+
#
|
43
|
+
def get_the_work
|
44
|
+
# We'll just return random integers as a
|
45
|
+
# fake work load
|
46
|
+
w = []
|
47
|
+
w << rand(25)
|
48
|
+
w << rand(25)
|
49
|
+
w << rand(25)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# do_the_work
|
54
|
+
#
|
55
|
+
# The work-horse method
|
56
|
+
#
|
57
|
+
def do_the_work(job_to_do)
|
58
|
+
i = job_to_do
|
59
|
+
i * Math::PI
|
60
|
+
end
|
61
|
+
|
62
|
+
#########################################################################
|
63
|
+
# Notes
|
64
|
+
#########################################################################
|
65
|
+
|
66
|
+
# If your parent process only forks a small number of processes per loop (< 5..10),
|
67
|
+
# you may want to mark the child traces as asynchronous and have them directly
|
68
|
+
# linked to the parent tracing context.
|
69
|
+
#
|
70
|
+
# The benefit of this is that instead of having two independent traces (parent
|
71
|
+
# and child), you will have a single view of the parent trace showing the
|
72
|
+
# spawned child process and it's performance in the AppOptics dashboard.
|
73
|
+
#
|
74
|
+
# To do this:
|
75
|
+
# 1. Don't clear the context in the child process
|
76
|
+
# 2. Use `AppOpticsAPM::API.trace` instead
|
77
|
+
# 3. Pass the `Async` flag to mark this child as asynchronous
|
78
|
+
#
|
79
|
+
Kernel.loop do
|
80
|
+
AppOpticsAPM::API.start_trace('get_the_work') do
|
81
|
+
|
82
|
+
work = get_the_work
|
83
|
+
|
84
|
+
work.each do |job|
|
85
|
+
fork do
|
86
|
+
result = nil
|
87
|
+
# 1 Don't clear context
|
88
|
+
# 2 Use `AppOpticsAPM::API.trace` instead
|
89
|
+
# 3 Pass the Async flag
|
90
|
+
AppOpticsAPM::API.trace('do_the_work', {:job_id => job.id, :Async => 1 }) do
|
91
|
+
result = do_the_work(job)
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
sleep 5
|
99
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
|
2
|
+
timeout 15
|
3
|
+
preload_app true
|
4
|
+
|
5
|
+
before_fork do |server, worker|
|
6
|
+
Signal.trap 'TERM' do
|
7
|
+
puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
|
8
|
+
Process.kill 'QUIT', Process.pid
|
9
|
+
end
|
10
|
+
|
11
|
+
defined?(ActiveRecord::Base) and
|
12
|
+
ActiveRecord::Base.connection.disconnect!
|
13
|
+
|
14
|
+
defined?(::AppOpticsAPM) and
|
15
|
+
::AppOpticsAPM.disconnect!
|
16
|
+
end
|
17
|
+
|
18
|
+
after_fork do |server, worker|
|
19
|
+
Signal.trap 'TERM' do
|
20
|
+
puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
|
21
|
+
end
|
22
|
+
|
23
|
+
defined?(ActiveRecord::Base) and
|
24
|
+
ActiveRecord::Base.establish_connection
|
25
|
+
|
26
|
+
defined?(::AppOpticsAPM) and
|
27
|
+
::AppOpticsAPM.reconnect!
|
28
|
+
end
|
data/ext/oboe_metal/src/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.0
|
data/lib/appoptics_apm.rb
CHANGED
@@ -60,6 +60,8 @@ begin
|
|
60
60
|
require 'appoptics_apm/frameworks/sinatra'
|
61
61
|
require 'appoptics_apm/frameworks/padrino'
|
62
62
|
require 'appoptics_apm/frameworks/grape'
|
63
|
+
else
|
64
|
+
require 'appoptics_apm/noop/context'
|
63
65
|
end
|
64
66
|
|
65
67
|
# Load Ruby module last. If there is no framework detected,
|
@@ -40,7 +40,7 @@ module AppOpticsAPM
|
|
40
40
|
#
|
41
41
|
# Returns nothing.
|
42
42
|
def log(layer, label, opts = {}, event=nil)
|
43
|
-
return
|
43
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
44
44
|
|
45
45
|
event ||= AppOpticsAPM::Context.createEvent
|
46
46
|
log_event(layer, label, event, opts)
|
@@ -66,11 +66,11 @@ module AppOpticsAPM
|
|
66
66
|
#
|
67
67
|
# Returns nothing.
|
68
68
|
def log_exception(layer, exn, opts = {})
|
69
|
-
return if !AppOpticsAPM.tracing? || exn.instance_variable_get(:@oboe_logged)
|
69
|
+
return AppOpticsAPM::Context.toString if !AppOpticsAPM.tracing? || exn.instance_variable_get(:@oboe_logged)
|
70
70
|
|
71
71
|
unless exn
|
72
72
|
AppOpticsAPM.logger.debug '[appoptics_apm/debug] log_exception called with nil exception'
|
73
|
-
return
|
73
|
+
return AppOpticsAPM::Context.toString
|
74
74
|
end
|
75
75
|
|
76
76
|
opts.merge!(:ErrorClass => exn.class.name,
|
@@ -180,12 +180,11 @@ module AppOpticsAPM
|
|
180
180
|
#
|
181
181
|
# Returns an xtrace metadata string if we are tracing
|
182
182
|
def log_end(layer, opts = {})
|
183
|
-
return unless AppOpticsAPM.tracing?
|
183
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
184
184
|
|
185
|
-
log_event(layer, :exit, AppOpticsAPM::Context.createEvent, opts)
|
186
|
-
AppOpticsAPM::Context.toString
|
185
|
+
log_event(layer, :exit, AppOpticsAPM::Context.createEvent, opts)
|
187
186
|
ensure
|
188
|
-
# FIXME has_incoming_context commented out, it has importance for JRuby only
|
187
|
+
# FIXME has_incoming_context commented out, it has importance for JRuby only but breaks Ruby tests
|
189
188
|
AppOpticsAPM::Context.clear # unless AppOpticsAPM.has_incoming_context?
|
190
189
|
end
|
191
190
|
|
@@ -206,7 +205,7 @@ module AppOpticsAPM
|
|
206
205
|
#
|
207
206
|
# Returns an xtrace metadata string if we are tracing
|
208
207
|
def log_entry(layer, opts = {}, op = nil)
|
209
|
-
return unless AppOpticsAPM.tracing?
|
208
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
210
209
|
|
211
210
|
AppOpticsAPM.layer_op = op.to_sym if op
|
212
211
|
log_event(layer, :entry, AppOpticsAPM::Context.createEvent, opts)
|
@@ -228,7 +227,7 @@ module AppOpticsAPM
|
|
228
227
|
#
|
229
228
|
# Returns an xtrace metadata string if we are tracing
|
230
229
|
def log_info(layer, opts = {})
|
231
|
-
return unless AppOpticsAPM.tracing?
|
230
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
232
231
|
|
233
232
|
log_event(layer, :info, AppOpticsAPM::Context.createEvent, opts)
|
234
233
|
end
|
@@ -251,7 +250,7 @@ module AppOpticsAPM
|
|
251
250
|
#
|
252
251
|
# Returns an xtrace metadata string if we are tracing
|
253
252
|
def log_exit(layer, opts = {}, op = nil)
|
254
|
-
return unless AppOpticsAPM.tracing?
|
253
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
255
254
|
|
256
255
|
AppOpticsAPM.layer_op = nil if op
|
257
256
|
log_event(layer, :exit, AppOpticsAPM::Context.createEvent, opts)
|
@@ -270,7 +269,7 @@ module AppOpticsAPM
|
|
270
269
|
# * +traces+ - An array with X-Trace strings returned from the requests
|
271
270
|
#
|
272
271
|
def log_multi_exit(layer, traces)
|
273
|
-
return unless AppOpticsAPM.tracing?
|
272
|
+
return AppOpticsAPM::Context.toString unless AppOpticsAPM.tracing?
|
274
273
|
task_id = AppOpticsAPM::XTrace.task_id(AppOpticsAPM::Context.toString)
|
275
274
|
event = AppOpticsAPM::Context.createEvent
|
276
275
|
traces.each do |trace|
|
@@ -289,7 +288,7 @@ module AppOpticsAPM
|
|
289
288
|
# * +opts+ - A hash containing key/value pairs that will be reported along with this event
|
290
289
|
def log_init(layer = :rack, opts = {})
|
291
290
|
context = AppOpticsAPM::Metadata.makeRandom
|
292
|
-
return unless context.isValid
|
291
|
+
return AppOpticsAPM::Context.toString unless context.isValid
|
293
292
|
|
294
293
|
event = context.createEvent
|
295
294
|
event.addInfo(APPOPTICS_STR_LAYER, layer.to_s)
|
@@ -299,6 +298,7 @@ module AppOpticsAPM
|
|
299
298
|
end
|
300
299
|
|
301
300
|
AppOpticsAPM::Reporter.sendStatus(event, context)
|
301
|
+
AppOpticsAPM::Context.toString
|
302
302
|
end
|
303
303
|
|
304
304
|
private
|
@@ -353,6 +353,7 @@ module AppOpticsAPM
|
|
353
353
|
end if !opts.nil? && opts.any?
|
354
354
|
|
355
355
|
AppOpticsAPM::Reporter.sendReport(event)
|
356
|
+
AppOpticsAPM::Context.toString
|
356
357
|
end
|
357
358
|
|
358
359
|
end
|
@@ -51,7 +51,8 @@ module AppOpticsAPM
|
|
51
51
|
#
|
52
52
|
# Returns the result of the block.
|
53
53
|
def trace(layer, opts = {}, protect_op = nil)
|
54
|
-
return if protect_op && AppOpticsAPM.layer_op == protect_op.to_sym
|
54
|
+
return if !AppOpticsAPM.loaded || (protect_op && AppOpticsAPM.layer_op == protect_op.to_sym)
|
55
|
+
|
55
56
|
log_entry(layer, opts, protect_op)
|
56
57
|
begin
|
57
58
|
yield
|
@@ -158,6 +159,32 @@ module AppOpticsAPM
|
|
158
159
|
AppOpticsAPM::Context.clear
|
159
160
|
end
|
160
161
|
end
|
162
|
+
|
163
|
+
# Public: Set a ThreadLocal custom transaction name to be used when sending a trace or metrics for the
|
164
|
+
# current transaction
|
165
|
+
#
|
166
|
+
# In addition to setting a transaction name here there is also a configuration
|
167
|
+
# AppOpticsAPM::Config['transaction_name']['prepend_domain'] which allows to have the domain prepended
|
168
|
+
# to the transaction name
|
169
|
+
#
|
170
|
+
# ===== Arguments
|
171
|
+
# * +name+ - A non-empty string with the custom transaction name
|
172
|
+
#
|
173
|
+
def set_transaction_name(name)
|
174
|
+
if name.is_a?(String) && name.strip != ''
|
175
|
+
AppOpticsAPM.transaction_name = name
|
176
|
+
else
|
177
|
+
AppOpticsAPM.logger.debug "[appoptics_apm/api] Could not set transaction name, provided name is empty or not a String."
|
178
|
+
end
|
179
|
+
AppOpticsAPM.transaction_name
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# this is provided for testing
|
184
|
+
# returns the current transaction name
|
185
|
+
def get_transaction_name
|
186
|
+
AppOpticsAPM.transaction_name
|
187
|
+
end
|
161
188
|
end
|
162
189
|
end
|
163
190
|
end
|
data/lib/appoptics_apm/base.rb
CHANGED
@@ -42,6 +42,12 @@ module AppOpticsAPMBase
|
|
42
42
|
thread_local :sample_rate
|
43
43
|
thread_local :layer
|
44
44
|
thread_local :layer_op
|
45
|
+
|
46
|
+
# transaction_name is used for custom transaction naming
|
47
|
+
# It needs to be globally accessible, but is only set by the request processors of the different frameworks
|
48
|
+
# and read by rack
|
49
|
+
thread_local :transaction_name
|
50
|
+
|
45
51
|
# Semaphore used during the test suite to test
|
46
52
|
# global config options.
|
47
53
|
thread_local :config_lock
|
@@ -54,7 +60,7 @@ module AppOpticsAPMBase
|
|
54
60
|
# X-Trace request header, tracing may have already been started
|
55
61
|
# by Joboe. Such a scenario occurs when the application is being
|
56
62
|
# hosted by a Java container (such as Tomcat or Glassfish) and
|
57
|
-
#
|
63
|
+
# AppOpticsAPM has already initiated tracing. In this case, we shouldn't
|
58
64
|
# pickup the X-Trace context in the X-Trace header and we shouldn't
|
59
65
|
# set the outgoing response X-Trace header or clear context.
|
60
66
|
# Yeah I know. Yuck.
|
data/lib/appoptics_apm/config.rb
CHANGED
@@ -73,31 +73,32 @@ module AppOpticsAPM
|
|
73
73
|
# to create an output similar to the content of the config file
|
74
74
|
#
|
75
75
|
def self.print_config
|
76
|
-
|
76
|
+
AppOpticsAPM.logger.warn "# General configurations"
|
77
77
|
non_instrumentation = @@config.keys - @@instrumentation
|
78
78
|
non_instrumentation.each do |config|
|
79
|
-
|
79
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:#{config}] = #{@@config[config]}"
|
80
80
|
end
|
81
81
|
|
82
|
-
|
83
|
-
|
82
|
+
AppOpticsAPM.logger.warn "\n# Instrumentation specific configurations"
|
83
|
+
AppOpticsAPM.logger.warn "# Enabled/Disabled Instrumentation"
|
84
84
|
@@instrumentation.each do |config|
|
85
|
-
|
85
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:#{config}][:enabled] = #{@@config[config][:enabled]}"
|
86
86
|
end
|
87
87
|
|
88
|
-
|
88
|
+
AppOpticsAPM.logger.warn "\n# Enabled/Disabled Backtrace Collection"
|
89
89
|
@@instrumentation.each do |config|
|
90
|
-
|
90
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:#{config}][:collect_backtraces] = #{@@config[config][:collect_backtraces]}"
|
91
|
+
AppOpticsAPM.logger.warn
|
91
92
|
end
|
92
93
|
|
93
|
-
|
94
|
+
AppOpticsAPM.logger.warn "\n# Logging of outgoing HTTP query args"
|
94
95
|
@@instrumentation.each do |config|
|
95
|
-
|
96
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:#{config}][:log_args] = #{@@config[config][:log_args]}"
|
96
97
|
end
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
99
|
+
AppOpticsAPM.logger.warn "\n# Bunny Controller and Action"
|
100
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:bunnyconsumer][:controller] = #{@@config[:bunnyconsumer][:controller].inspect}"
|
101
|
+
AppOpticsAPM.logger.warn "AppOpticsAPM::Config[:bunnyconsumer][:action] = #{@@config[:bunnyconsumer][:action].inspect}"
|
101
102
|
nil
|
102
103
|
end
|
103
104
|
|
@@ -112,7 +113,8 @@ module AppOpticsAPM
|
|
112
113
|
@@instrumentation.each do |k|
|
113
114
|
@@config[k] = {}
|
114
115
|
end
|
115
|
-
|
116
|
+
@@config[:transaction_name] = {}
|
117
|
+
load(File.join(File.dirname(File.dirname(__FILE__)),
|
116
118
|
'rails/generators/appoptics_apm/templates/appoptics_apm_initializer.rb'))
|
117
119
|
end
|
118
120
|
# rubocop:enable Metrics/AbcSize
|
@@ -51,6 +51,8 @@ module AppOpticsAPM
|
|
51
51
|
def error_response_with_appoptics(error = {})
|
52
52
|
status, headers, body = error_response_without_appoptics(error)
|
53
53
|
|
54
|
+
xtrace = AppOpticsAPM::Context.toString
|
55
|
+
|
54
56
|
if AppOpticsAPM.tracing?
|
55
57
|
# Since Grape uses throw/catch and not Exceptions, we manually log
|
56
58
|
# the error here.
|
@@ -58,18 +60,17 @@ module AppOpticsAPM
|
|
58
60
|
kvs[:ErrorClass] = 'GrapeError'
|
59
61
|
kvs[:ErrorMsg] = error[:message] ? error[:message] : "No message given."
|
60
62
|
kvs[:Backtrace] = ::AppOpticsAPM::API.backtrace if AppOpticsAPM::Config[:grape][:collect_backtraces]
|
61
|
-
|
62
|
-
::AppOpticsAPM::API.log(nil, 'error', kvs)
|
63
|
+
::AppOpticsAPM::API.log('rack', 'error', kvs)
|
63
64
|
|
64
65
|
# Since calls to error() are handled similar to abort in Grape. We
|
65
66
|
# manually log the rack exit here since the original code won't
|
66
67
|
# be returned to
|
67
68
|
xtrace = AppOpticsAPM::API.log_end('rack', :Status => status)
|
69
|
+
end
|
68
70
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
71
|
+
if headers && AppOpticsAPM::XTrace.valid?(xtrace)
|
72
|
+
unless defined?(JRUBY_VERSION) && AppOpticsAPM.is_continued_trace?
|
73
|
+
headers['X-Trace'] = xtrace if headers.is_a?(Hash)
|
73
74
|
end
|
74
75
|
end
|
75
76
|
|