apminsight 1.0.1 → 1.8.2
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.
- data/VERSION +1 -1
- data/apm-agent.gemspec +64 -0
- data/conf/apminsight.conf +15 -24
- data/lib/agent/am_objectholder.rb +25 -19
- data/lib/agent/api/custom_tracker.rb +79 -0
- data/lib/agent/configuration/am_configuration.rb +249 -37
- data/lib/agent/handler/custom_api_handler.rb +40 -0
- data/lib/agent/handler/sequence_book.rb +118 -0
- data/lib/agent/handler/tracker_handler.rb +58 -0
- data/lib/agent/logging/am_logger.rb +13 -9
- data/lib/agent/metrics/am_metricsformatter.rb +117 -59
- data/lib/agent/metrics/am_metricsparser.rb +195 -468
- data/lib/agent/metrics/am_metricstore.rb +7 -6
- data/lib/agent/metrics/exception_record.rb +24 -0
- data/lib/agent/server/am_agent.rb +42 -17
- data/lib/agent/server/am_connector.rb +65 -21
- data/lib/agent/server/instrument/action_view.rb +64 -0
- data/lib/agent/server/instrument/active_record.rb +52 -0
- data/lib/agent/server/instrument/am_apm.rb +107 -97
- data/lib/agent/server/instrument/am_instrumenter.rb +54 -42
- data/lib/agent/server/instrument/environment.rb +42 -0
- data/lib/agent/server/instrument/rails.rb +56 -0
- data/lib/agent/server/instrument/sinatra.rb +97 -0
- data/lib/agent/server/worker/am_worker.rb +93 -49
- data/lib/agent/trackers/database_tracker.rb +107 -0
- data/lib/agent/trackers/default_tracker.rb +57 -0
- data/lib/agent/trackers/root_tracker.rb +43 -0
- data/lib/agent/util/am_constants.rb +46 -3
- data/lib/agent/util/am_util.rb +64 -1
- data/lib/agent/util/transaction_util.rb +35 -0
- data/lib/agent/version.rb +13 -0
- data/lib/apminsight.rb +4 -1
- metadata +114 -76
@@ -1,43 +1,55 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
class APMInstrumenter
|
5
|
-
@t =nil;
|
6
|
-
def initialize
|
7
|
-
@obj=ManageEngine::APMObjectHolder.instance
|
8
|
-
end
|
1
|
+
##
|
2
|
+
## This is Rails framework specific tracking mechanism, they are moved to multiple files for separate tracking
|
3
|
+
##
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
5
|
+
#require 'agent/am_objectholder'
|
6
|
+
#require 'socket'
|
7
|
+
#module ManageEngine
|
8
|
+
# class APMInstrumenter
|
9
|
+
# @t =nil;
|
10
|
+
# def initialize
|
11
|
+
# @obj=ManageEngine::APMObjectHolder.instance
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# def doSubscribe
|
15
|
+
# @obj=ManageEngine::APMObjectHolder.instance
|
16
|
+
# @obj.log.debug "[ instrumenter ] [ Subscriber for Agent ]"
|
17
|
+
# @subscriber = ActiveSupport::Notifications.subscribe do |name, start, finish, id, payload|
|
18
|
+
# if(ManageEngine::APMObjectHolder.instance.config.agent_enabled)
|
19
|
+
# #rt = (finish-start).to_i
|
20
|
+
# ManageEngine::APMWorker.getInstance.start
|
21
|
+
# ManageEngine::APMObjectHolder.instance.log.debug "[ Notifications for Agent ] #{Thread.current} #{id} #{name} - #{payload[:path]}"
|
22
|
+
# #trace= caller;
|
23
|
+
# #puts ">>> Threadlocal var : #{Thread.current[:apminsight]}"
|
24
|
+
# if name=="sql.active_record"
|
25
|
+
# #Thread.current[:apminsight] = "#{Thread.current[:apminsight]} + #{payload[:sql]}"
|
26
|
+
# if payload[:name] != "SCHEMA"
|
27
|
+
# @obj.log.debug ">>>>>>>> SQL: #{payload[:sql]}"
|
28
|
+
# end
|
29
|
+
# @obj.log.debug "~~~~~ SQL Payload: #{payload}"
|
30
|
+
# end
|
31
|
+
# id = "#{Thread.current}"
|
32
|
+
# stats = Hash.new
|
33
|
+
# stats["name"] = name;
|
34
|
+
# stats["start"] = start.to_f * 1000;
|
35
|
+
# stats["end"] = finish.to_f * 1000;
|
36
|
+
# stats["id"] = id;
|
37
|
+
# stats["payload"] = payload;
|
38
|
+
# if (name=="sql.active_record" && (finish.to_f - start.to_f)>=(ManageEngine::APMObjectHolder.instance.config.sql_trace_t).to_f)
|
39
|
+
# stats["trace"] = caller(20); # Taking stacktrace of depth 20
|
40
|
+
# end
|
41
|
+
# stats["ctime"] =ManageEngine::APMObjectHolder.instance.util.currenttimemillis;
|
42
|
+
# ManageEngine::APMObjectHolder.instance.collector.updateTransaction(id,stats);
|
43
|
+
# else
|
44
|
+
# ActiveSupport::Notifications.unsubscribe @subscriber
|
45
|
+
# @obj.log.info "[ instrumenter ] [ RETURNING NO METRICS] "
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def doUnSubscribe
|
51
|
+
# ActiveSupport::Notifications.unsubscribe @subscriber
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
#end #class
|
55
|
+
#end#module
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'agent/server/instrument/rails'
|
2
|
+
require 'agent/server/instrument/sinatra'
|
3
|
+
require 'agent/server/instrument/active_record'
|
4
|
+
require 'agent/server/instrument/action_view'
|
5
|
+
|
6
|
+
module ManageEngine
|
7
|
+
class Environment
|
8
|
+
|
9
|
+
SUPPORTED_FRAMEWORKS = [
|
10
|
+
ManageEngine::Instrumentation::RailsFramework.new,
|
11
|
+
ManageEngine::Instrumentation::SinatraFramework.new
|
12
|
+
]
|
13
|
+
|
14
|
+
DATABASE_INTERCEPTORS = [
|
15
|
+
ManageEngine::Instrumentation::ActiveRecordSQL.new
|
16
|
+
]
|
17
|
+
|
18
|
+
OTHER_INTERCEPTORS = [
|
19
|
+
ManageEngine::Instrumentation::ActionView.new
|
20
|
+
]
|
21
|
+
|
22
|
+
def detect_and_instrument
|
23
|
+
@framework ||= SUPPORTED_FRAMEWORKS.detect{ |framework| framework.present? }
|
24
|
+
if (@framework != nil)
|
25
|
+
@framework.instrument
|
26
|
+
end
|
27
|
+
|
28
|
+
DATABASE_INTERCEPTORS.each do |interceptor|
|
29
|
+
if (interceptor.present?)
|
30
|
+
interceptor.instrument
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
OTHER_INTERCEPTORS.each do |interceptor|
|
35
|
+
if (interceptor.present?)
|
36
|
+
interceptor.instrument
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'agent/handler/tracker_handler'
|
2
|
+
|
3
|
+
module ManageEngine
|
4
|
+
module Instrumentation
|
5
|
+
class RailsFramework
|
6
|
+
|
7
|
+
def present?
|
8
|
+
defined?(::Rails) && defined?(::ActionController)
|
9
|
+
end
|
10
|
+
|
11
|
+
def version
|
12
|
+
Rails::VERSION::STRING
|
13
|
+
end
|
14
|
+
|
15
|
+
def env
|
16
|
+
if Rails::VERSION::MAJOR >= 3
|
17
|
+
::Rails.env
|
18
|
+
else
|
19
|
+
RAILS_ENV.dup
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
'Rails'
|
25
|
+
end
|
26
|
+
|
27
|
+
def instrument
|
28
|
+
@obj = ManageEngine::APMObjectHolder.instance
|
29
|
+
@obj.log.info "Instrumenting ActionController.. Rails Version: #{version}"
|
30
|
+
@railsTracker = nil
|
31
|
+
|
32
|
+
ActiveSupport::Notifications.subscribe('start_processing.action_controller') do |name, start, finish, id, payload|
|
33
|
+
path = payload[:path].partition("?")[0]
|
34
|
+
@railsTracker = ManageEngine::Tracker::RootTracker.new("#{payload[:controller]}.#{payload[:action]}", start.to_f * 1000)
|
35
|
+
@railsTracker.url=(path)
|
36
|
+
@railsTracker = ManageEngine::Agent::TrackerHandler.invokeTracker(@railsTracker)
|
37
|
+
end # subscribe
|
38
|
+
|
39
|
+
|
40
|
+
ActiveSupport::Notifications.subscribe('process_action.action_controller') do |name, start, finish, id, payload|
|
41
|
+
if @railsTracker != nil
|
42
|
+
@railsTracker.finish(finish.to_f * 1000)
|
43
|
+
exception = payload[:exception_object]
|
44
|
+
if exception != nil
|
45
|
+
@railsTracker.setError(exception)
|
46
|
+
@railsTracker.setStatus(500) # By default, set 500 as status for error txns
|
47
|
+
end
|
48
|
+
ManageEngine::Agent::TrackerHandler.exitTracker(@railsTracker)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end # def instrument
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'agent/am_objectholder'
|
2
|
+
require 'agent/trackers/root_tracker'
|
3
|
+
|
4
|
+
module ManageEngine
|
5
|
+
module Instrumentation
|
6
|
+
class SinatraFramework
|
7
|
+
|
8
|
+
def present?
|
9
|
+
defined?(::Sinatra) && defined?(::Sinatra::Base)
|
10
|
+
end
|
11
|
+
|
12
|
+
def version
|
13
|
+
::Sinatra::VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
def env
|
17
|
+
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
'Sinatra'
|
22
|
+
end
|
23
|
+
|
24
|
+
def instrument
|
25
|
+
ManageEngine::APMObjectHolder.instance.log.info "Instrumenting Sinatra framework. Version: #{version}"
|
26
|
+
::Sinatra::Base.class_eval do
|
27
|
+
include ManageEngine::Instrumentation::APMInsightSinatra
|
28
|
+
|
29
|
+
alias original_route_eval route_eval
|
30
|
+
alias route_eval apminsight_route_eval
|
31
|
+
|
32
|
+
# alias sinatra_exception_handler! handle_exception!
|
33
|
+
# alias handle_exception! apminsight_exception_handler!
|
34
|
+
|
35
|
+
end # class_eval
|
36
|
+
end # def instrument
|
37
|
+
|
38
|
+
end # class Sinatra
|
39
|
+
|
40
|
+
module APMInsightSinatra
|
41
|
+
|
42
|
+
def apminsight_route_eval(*args, &block)
|
43
|
+
|
44
|
+
# http://www.rubydoc.info/github/rack/rack/master/Rack/Request
|
45
|
+
url = (env.has_key?('sinatra.route') ? env['sinatra.route'] : @request.path).dup
|
46
|
+
@obj = ManageEngine::APMObjectHolder.instance
|
47
|
+
|
48
|
+
sinatraTracker = ManageEngine::Tracker::RootTracker.new(url)
|
49
|
+
sinatraTracker.url=(url)
|
50
|
+
sinatraTracker = ManageEngine::Agent::TrackerHandler.invokeTracker(sinatraTracker)
|
51
|
+
|
52
|
+
# TODO: capture all additional details @request.query_string @request.params
|
53
|
+
|
54
|
+
begin
|
55
|
+
original_route_eval(*args, &block)
|
56
|
+
|
57
|
+
rescue Exception => e # On application error, above method throws exception
|
58
|
+
if (sinatraTracker != nil)
|
59
|
+
sinatraTracker.setError(e)
|
60
|
+
sinatraTracker.setStatus(500) # By default, set 500 as status for error txns
|
61
|
+
end
|
62
|
+
raise e
|
63
|
+
|
64
|
+
ensure
|
65
|
+
if sinatraTracker != nil
|
66
|
+
sinatraTracker.finish
|
67
|
+
end
|
68
|
+
ManageEngine::Agent::TrackerHandler.exitTracker(sinatraTracker)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
# def apminsight_exception_handler!(*args, &block)
|
74
|
+
# begin
|
75
|
+
# sinatra_exception_handler!(*args, &block)
|
76
|
+
# ensure
|
77
|
+
# tracker = Thread.current[:apminsight]
|
78
|
+
# puts "tracker is #{(tracker == nil)}"
|
79
|
+
# if tracker != nil
|
80
|
+
# tracker.error(args[0]) # Other way, env[sinatra.error]
|
81
|
+
# tracker.status(@response.status)
|
82
|
+
# finishTracker tracker
|
83
|
+
# end #if
|
84
|
+
# end#begin
|
85
|
+
# end#def
|
86
|
+
#
|
87
|
+
# def finishTracker(tracker)
|
88
|
+
# tracker.finish
|
89
|
+
# #ManageEngine::APMObjectHolder.instance.collector.updateTransaction(id,stats)
|
90
|
+
# puts tracker.to_s
|
91
|
+
# Thread.current[:apminsight] = nil
|
92
|
+
# end
|
93
|
+
|
94
|
+
end # module SinatraFramework
|
95
|
+
|
96
|
+
end # module Instrumentation
|
97
|
+
end
|
@@ -7,46 +7,63 @@ class APMWorker
|
|
7
7
|
@status = 'not_init'
|
8
8
|
@id = 0
|
9
9
|
attr_accessor :id
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
def start
|
16
|
-
@obj = ManageEngine::APMObjectHolder.instance
|
17
|
-
|
18
|
-
if @status=="working"
|
19
|
-
@obj.log.debug "woker thread already started"
|
20
|
-
elsif @status == "initialized"
|
21
|
-
@obj.log.info "start worker thread for - #{Process.pid} :: #{@status} "
|
22
|
-
#@obj.log.info "Starting APMWorker Thread #{Process.pid} "
|
23
|
-
@apm = Thread.new do
|
24
|
-
@status = 'working'
|
25
|
-
while !@obj.shutdown do
|
26
|
-
checkforagentstatus
|
27
|
-
updateConfig
|
28
|
-
dc
|
29
|
-
sleep (@obj.config.connect_interval).to_i
|
30
|
-
end#w
|
31
|
-
@status= "end"
|
32
|
-
@obj.log.debug "Worker thread ends"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
10
|
+
def initialize
|
11
|
+
@status = "initialized"
|
12
|
+
@id = Process.pid
|
13
|
+
end
|
36
14
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
15
|
+
def start
|
16
|
+
@obj = ManageEngine::APMObjectHolder.instance
|
17
|
+
|
18
|
+
if @status=="working"
|
19
|
+
@obj.log.debug "woker thread already started"
|
20
|
+
elsif @status == "initialized"
|
21
|
+
@obj.log.info "start worker thread for - #{Process.pid} :: #{@status} "
|
22
|
+
#@obj.log.info "Starting APMWorker Thread #{Process.pid} "
|
23
|
+
@apm = Thread.new do
|
24
|
+
@status = 'working'
|
25
|
+
while !@obj.shutdown do
|
26
|
+
checkforagentstatus
|
27
|
+
updateConfig
|
28
|
+
dc
|
29
|
+
sleep (@obj.config.connect_interval).to_i
|
30
|
+
end#w
|
31
|
+
@status= "end"
|
32
|
+
@obj.log.debug "Worker thread ends"
|
42
33
|
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.getInstance
|
38
|
+
if(@work==nil || @work.id!=Process.pid)
|
39
|
+
@work = ManageEngine::APMWorker.new
|
40
|
+
end
|
41
|
+
return @work
|
42
|
+
end
|
43
43
|
|
44
44
|
def updateConfig
|
45
45
|
if(@obj.config.lastupdatedtime!=File.mtime(@obj.constants.apm_conf).to_i)
|
46
|
-
|
46
|
+
@obj.log.info "Configuration File Changed... So Updating Configuration."
|
47
|
+
agent_config_data = @obj.config.getAgentConfigData
|
47
48
|
@obj.config.lastupdatedtime=File.mtime(@obj.constants.apm_conf).to_i
|
48
49
|
@obj.config.configureFile
|
49
50
|
@obj.config.assignConfig
|
51
|
+
new_agent_config_data = @obj.config.getAgentConfigData
|
52
|
+
sendUpdate = "false"
|
53
|
+
agent_config_data.each do|key,value|
|
54
|
+
if key != "last.modified.time"
|
55
|
+
newValue = new_agent_config_data[key]
|
56
|
+
if value != newValue
|
57
|
+
sendUpdate = "true"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
if sendUpdate == "true"
|
62
|
+
@obj.log.info "sending update to server #{new_agent_config_data}"
|
63
|
+
data1 = Hash.new
|
64
|
+
data1["custom_config_info"]=new_agent_config_data
|
65
|
+
resp = @obj.connector.post @obj.constants.connect_config_update_uri+@obj.config.instance_id,data1
|
66
|
+
end
|
50
67
|
end
|
51
68
|
end
|
52
69
|
|
@@ -57,12 +74,12 @@ class APMWorker
|
|
57
74
|
@obj.log.info "Agent in Disabled State."
|
58
75
|
if prevState
|
59
76
|
@obj.log.info "Agent in Disabled State. Going to unsubscribe"
|
60
|
-
@obj.instrumenter.doUnSubscribe
|
77
|
+
# @obj.instrumenter.doUnSubscribe
|
61
78
|
end
|
62
79
|
else
|
63
80
|
if !prevState
|
64
81
|
@obj.log.info "Agent in Active State."
|
65
|
-
@obj.instrumenter.doSubscribe
|
82
|
+
# @obj.instrumenter.doSubscribe
|
66
83
|
end
|
67
84
|
end
|
68
85
|
end
|
@@ -126,14 +143,18 @@ class APMWorker
|
|
126
143
|
tdata.concat(val[0])
|
127
144
|
when 2
|
128
145
|
tdata.concat(val[0])
|
129
|
-
trdata.
|
146
|
+
if (trdata.size < @obj.config.trace_overflow_t)
|
147
|
+
trdata.concat(val[1])
|
148
|
+
end
|
130
149
|
end
|
131
150
|
end
|
132
151
|
result.push(merge(tdata))
|
133
152
|
resp = @obj.connector.post @obj.constants.connect_data_uri+@obj.config.instance_id,result
|
153
|
+
@obj.log.info "#{tdata.size} metric(s) dispatched."
|
134
154
|
if trdata.size>0
|
135
155
|
result[2]=trdata;
|
136
156
|
resp = @obj.connector.post @obj.constants.connect_trace_uri+@obj.config.instance_id,result
|
157
|
+
@obj.log.info "#{trdata.size} trace(s) dispatched."
|
137
158
|
end
|
138
159
|
end
|
139
160
|
|
@@ -149,7 +170,7 @@ class APMWorker
|
|
149
170
|
def send_save data
|
150
171
|
begin
|
151
172
|
if FileTest.exist?(@obj.constants.agent_lock)
|
152
|
-
if Time.now.to_i - File.mtime(@obj.constants.agent_lock).to_i
|
173
|
+
if Time.now.to_i - File.mtime(@obj.constants.agent_lock).to_i >= (@obj.config.connect_interval).to_i
|
153
174
|
@obj.log.debug "worker send signal"
|
154
175
|
senddata data
|
155
176
|
else
|
@@ -172,7 +193,11 @@ class APMWorker
|
|
172
193
|
f.flock(File::LOCK_EX)
|
173
194
|
begin
|
174
195
|
f.each_line do |line|
|
175
|
-
|
196
|
+
begin
|
197
|
+
data.push(JSON.parse(line))
|
198
|
+
rescue Exception=>ex
|
199
|
+
@obj.log.logException "Error Parsing data, Skipping line #{line}", ex
|
200
|
+
end
|
176
201
|
end
|
177
202
|
f.truncate 0
|
178
203
|
rescue Exception=>e
|
@@ -223,18 +248,36 @@ class APMWorker
|
|
223
248
|
|
224
249
|
|
225
250
|
def mapdx res,dat
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
251
|
+
begin
|
252
|
+
rtData = res[0];
|
253
|
+
rtData[0] = rtData[0]+dat[0][0];
|
254
|
+
if dat[0][1]<rtData[1]
|
255
|
+
rtData[1]=dat[0][1]
|
256
|
+
end
|
257
|
+
if dat[0][2]>rtData[2]
|
258
|
+
rtData[2]=dat[0][2]
|
259
|
+
end
|
260
|
+
rtData[3] = rtData[3]+dat[0][3]
|
261
|
+
rtData[5] = rtData[5]+dat[0][5]
|
262
|
+
rtData[6] = rtData[6]+dat[0][6]
|
263
|
+
rtData[7] = rtData[7]+dat[0][7]
|
264
|
+
rtData[4] = rtData[3] != 0 ? (rtData[5].to_f + (rtData[6].to_f/2).to_f).to_f/rtData[3].to_f : 0
|
265
|
+
res[0] = rtData
|
266
|
+
|
267
|
+
resExcepData = res[1][@obj.constants.mf_logmetric]
|
268
|
+
excepData = dat[1][@obj.constants.mf_logmetric]
|
269
|
+
if (resExcepData == nil)
|
270
|
+
resExcepData = excepData
|
271
|
+
else
|
272
|
+
if (excepData != nil)
|
273
|
+
resExcepData = resExcepData.merge(excepData){|key, oldval, newval| newval + oldval}
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
res[1][@obj.constants.mf_logmetric] = resExcepData != nil ? resExcepData : Hash.new
|
278
|
+
rescue Exception=>e
|
279
|
+
@obj.log.logException "Exception while merging data",e
|
280
|
+
end
|
238
281
|
res
|
239
282
|
end
|
240
283
|
|
@@ -247,6 +290,7 @@ class APMWorker
|
|
247
290
|
res[2]=dat[2]
|
248
291
|
end
|
249
292
|
res[3] = res[3]+dat[3]
|
293
|
+
res[4] = res[4]+dat[4]
|
250
294
|
res
|
251
295
|
end
|
252
296
|
|