graphql 2.4.16 → 2.5.0
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.
- checksums.yaml +4 -4
- data/lib/graphql/execution/interpreter.rb +0 -3
- data/lib/graphql/query.rb +0 -3
- data/lib/graphql/static_validation/all_rules.rb +1 -1
- data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
- data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
- data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
- data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
- data/lib/graphql/tracing/appsignal_trace.rb +26 -61
- data/lib/graphql/tracing/data_dog_trace.rb +41 -164
- data/lib/graphql/tracing/monitor_trace.rb +285 -0
- data/lib/graphql/tracing/new_relic_trace.rb +34 -166
- data/lib/graphql/tracing/notifications_trace.rb +181 -37
- data/lib/graphql/tracing/perfetto_trace.rb +11 -14
- data/lib/graphql/tracing/prometheus_trace.rb +47 -74
- data/lib/graphql/tracing/scout_trace.rb +25 -59
- data/lib/graphql/tracing/sentry_trace.rb +57 -99
- data/lib/graphql/tracing/statsd_trace.rb +24 -47
- data/lib/graphql/tracing/trace.rb +0 -17
- data/lib/graphql/tracing.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- metadata +4 -2
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -1,49 +1,193 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "graphql/tracing/platform_trace"
|
4
|
-
|
5
3
|
module GraphQL
|
6
4
|
module Tracing
|
7
|
-
# This implementation forwards events to a notification handler
|
8
|
-
# ActiveSupport::Notifications or Dry::Monitor::Notifications)
|
9
|
-
#
|
5
|
+
# This implementation forwards events to a notification handler
|
6
|
+
# (i.e. ActiveSupport::Notifications or Dry::Monitor::Notifications) with a `graphql` suffix.
|
7
|
+
#
|
8
|
+
# @see ActiveSupportNotificationsTrace ActiveSupport::Notifications integration
|
10
9
|
module NotificationsTrace
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
"authorized_lazy" => "authorized_lazy.graphql",
|
34
|
-
"resolve_type" => "resolve_type.graphql",
|
35
|
-
"resolve_type_lazy" => "resolve_type.graphql",
|
36
|
-
}.each do |trace_method, platform_key|
|
37
|
-
module_eval <<-RUBY, __FILE__, __LINE__
|
38
|
-
def #{trace_method}(**metadata, &block)
|
39
|
-
@notifications_engine.instrument("#{platform_key}", metadata) { super(**metadata, &block) }
|
10
|
+
# @api private
|
11
|
+
class Adapter
|
12
|
+
def instrument(keyword, payload, &block)
|
13
|
+
raise "Implement #{self.class}#instrument to measure the block"
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_event(keyword, payload)
|
17
|
+
ev = self.class::Event.new(keyword, payload)
|
18
|
+
ev.start
|
19
|
+
ev
|
20
|
+
end
|
21
|
+
|
22
|
+
class Event
|
23
|
+
def initialize(name, payload)
|
24
|
+
@name = name
|
25
|
+
@payload = payload
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :name, :payload
|
29
|
+
|
30
|
+
def start
|
31
|
+
raise "Implement #{self.class}#start to begin a new event (#{inspect})"
|
40
32
|
end
|
41
|
-
|
33
|
+
|
34
|
+
def finish
|
35
|
+
raise "Implement #{self.class}#finish to end this event (#{inspect})"
|
36
|
+
end
|
37
|
+
end
|
42
38
|
end
|
43
39
|
|
44
|
-
#
|
40
|
+
# @api private
|
41
|
+
class DryMonitorAdapter < Adapter
|
42
|
+
def instrument(...)
|
43
|
+
Dry::Monitor.instrument(...)
|
44
|
+
end
|
45
45
|
|
46
|
-
|
46
|
+
class Event < Adapter::Event
|
47
|
+
def start
|
48
|
+
Dry::Monitor.start(@name, @payload)
|
49
|
+
end
|
50
|
+
|
51
|
+
def finish
|
52
|
+
Dry::Monitor.stop(@name, @payload)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
class ActiveSupportNotificationsAdapter < Adapter
|
59
|
+
def instrument(...)
|
60
|
+
ActiveSupport::Notifications.instrument(...)
|
61
|
+
end
|
62
|
+
|
63
|
+
class Event < Adapter::Event
|
64
|
+
def start
|
65
|
+
@asn_event = ActiveSupport::Notifications.instrumenter.new_event(@name, @payload)
|
66
|
+
@asn_event.start!
|
67
|
+
end
|
68
|
+
|
69
|
+
def finish
|
70
|
+
@asn_event.finish!
|
71
|
+
ActiveSupport::Notifications.publish_event(@asn_event)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param engine [Class] The notifications engine to use, eg `Dry::Monitor` or `ActiveSupport::Notifications`
|
77
|
+
def initialize(engine:, **rest)
|
78
|
+
adapter = if defined?(Dry::Monitor) && engine == Dry::Monitor
|
79
|
+
DryMonitoringAdapter
|
80
|
+
elsif defined?(ActiveSupport::Notifications) && engine == ActiveSupport::Notifications
|
81
|
+
ActiveSupportNotificationsAdapter
|
82
|
+
else
|
83
|
+
engine
|
84
|
+
end
|
85
|
+
@notifications = adapter.new
|
86
|
+
super
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse(**payload)
|
90
|
+
@notifications.instrument("parse.graphql", payload) do
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def lex(**payload)
|
96
|
+
@notifications.instrument("lex.graphql", payload) do
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate(**payload)
|
102
|
+
@notifications.instrument("validate.graphql", payload) do
|
103
|
+
super
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def begin_analyze_multiplex(multiplex, analyzers)
|
108
|
+
begin_notifications_event("analyze.graphql", {multiplex: multiplex, analyzers: analyzers})
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
def end_analyze_multiplex(_multiplex, _analyzers)
|
113
|
+
finish_notifications_event
|
114
|
+
super
|
115
|
+
end
|
116
|
+
|
117
|
+
def execute_multiplex(**payload)
|
118
|
+
@notifications.instrument("execute.graphql", payload) do
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def begin_execute_field(field, object, arguments, query)
|
124
|
+
begin_notifications_event("execute_field.graphql", {field: field, object: object, arguments: arguments, query: query})
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def end_execute_field(_field, _object, _arguments, _query, _result)
|
129
|
+
finish_notifications_event
|
130
|
+
super
|
131
|
+
end
|
132
|
+
|
133
|
+
def dataloader_fiber_yield(source)
|
134
|
+
Fiber[PREVIOUS_EV_KEY] = finish_notifications_event
|
135
|
+
super
|
136
|
+
end
|
137
|
+
|
138
|
+
def dataloader_fiber_resume(source)
|
139
|
+
prev_ev = Fiber[PREVIOUS_EV_KEY]
|
140
|
+
begin_notifications_event(prev_ev.name, prev_ev.payload)
|
141
|
+
super
|
142
|
+
end
|
143
|
+
|
144
|
+
def begin_authorized(type, object, context)
|
145
|
+
begin_notifications_event("authorized.graphql", {type: type, object: object, context: context})
|
146
|
+
super
|
147
|
+
end
|
148
|
+
|
149
|
+
def end_authorized(type, object, context, result)
|
150
|
+
finish_notifications_event
|
151
|
+
super
|
152
|
+
end
|
153
|
+
|
154
|
+
def begin_resolve_type(type, object, context)
|
155
|
+
begin_notifications_event("resolve_type.graphql", {type: type, object: object, context: context})
|
156
|
+
super
|
157
|
+
end
|
158
|
+
|
159
|
+
def end_resolve_type(type, object, context, resolved_type)
|
160
|
+
finish_notifications_event
|
161
|
+
super
|
162
|
+
end
|
163
|
+
|
164
|
+
def begin_dataloader_source(source)
|
165
|
+
begin_notifications_event("dataloader_source.graphql", { source: source })
|
166
|
+
super
|
167
|
+
end
|
168
|
+
|
169
|
+
def end_dataloader_source(source)
|
170
|
+
finish_notifications_event
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
174
|
+
CURRENT_EV_KEY = :__notifications_graphql_trace_event
|
175
|
+
PREVIOUS_EV_KEY = :__notifications_graphql_trace_previous_event
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def begin_notifications_event(name, payload)
|
180
|
+
Fiber[CURRENT_EV_KEY] = @notifications.start_event(name, payload)
|
181
|
+
end
|
182
|
+
|
183
|
+
def finish_notifications_event
|
184
|
+
if ev = Fiber[CURRENT_EV_KEY]
|
185
|
+
ev.finish
|
186
|
+
# Use `false` to prevent grabbing an event from a parent fiber
|
187
|
+
Fiber[CURRENT_EV_KEY] = false
|
188
|
+
ev
|
189
|
+
end
|
190
|
+
end
|
47
191
|
end
|
48
192
|
end
|
49
193
|
end
|
@@ -171,36 +171,36 @@ module GraphQL
|
|
171
171
|
)
|
172
172
|
end
|
173
173
|
|
174
|
-
def
|
174
|
+
def execute_multiplex(multiplex:)
|
175
175
|
if defined?(ActiveSupport::Notifications) && @active_support_notifications_pattern != false
|
176
176
|
subscribe_to_active_support_notifications(@active_support_notifications_pattern)
|
177
177
|
end
|
178
|
-
@operation_name =
|
178
|
+
@operation_name = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(",")
|
179
179
|
@begin_time = Time.now
|
180
180
|
@packets << trace_packet(
|
181
181
|
type: TrackEvent::Type::TYPE_SLICE_BEGIN,
|
182
182
|
track_uuid: fid,
|
183
183
|
name: "Multiplex",
|
184
184
|
debug_annotations: [
|
185
|
-
payload_to_debug("query_string",
|
185
|
+
payload_to_debug("query_string", multiplex.queries.map(&:sanitized_query_string).join("\n\n"))
|
186
186
|
]
|
187
187
|
)
|
188
|
-
super
|
189
|
-
end
|
188
|
+
result = super
|
190
189
|
|
191
|
-
def end_execute_multiplex(m)
|
192
190
|
@packets << trace_packet(
|
193
191
|
type: TrackEvent::Type::TYPE_SLICE_END,
|
194
192
|
track_uuid: fid,
|
195
193
|
)
|
194
|
+
|
195
|
+
result
|
196
|
+
ensure
|
196
197
|
unsubscribe_from_active_support_notifications
|
197
198
|
if @save_profile
|
198
199
|
begin_ts = (@begin_time.to_f * 1000).round
|
199
200
|
end_ts = (Time.now.to_f * 1000).round
|
200
201
|
duration_ms = end_ts - begin_ts
|
201
|
-
|
202
|
+
multiplex.schema.detailed_trace.save_trace(@operation_name, duration_ms, begin_ts, Trace.encode(Trace.new(packet: @packets)))
|
202
203
|
end
|
203
|
-
super
|
204
204
|
end
|
205
205
|
|
206
206
|
def begin_execute_field(field, object, arguments, query)
|
@@ -261,7 +261,7 @@ module GraphQL
|
|
261
261
|
super
|
262
262
|
end
|
263
263
|
|
264
|
-
def
|
264
|
+
def parse(query_string:)
|
265
265
|
@packets << trace_packet(
|
266
266
|
type: TrackEvent::Type::TYPE_SLICE_BEGIN,
|
267
267
|
track_uuid: fid,
|
@@ -269,17 +269,14 @@ module GraphQL
|
|
269
269
|
extra_counter_values: [count_allocations],
|
270
270
|
name: "Parse"
|
271
271
|
)
|
272
|
-
super
|
273
|
-
end
|
274
|
-
|
275
|
-
def end_parse(str)
|
272
|
+
result = super
|
276
273
|
@packets << trace_packet(
|
277
274
|
type: TrackEvent::Type::TYPE_SLICE_END,
|
278
275
|
track_uuid: fid,
|
279
276
|
extra_counter_track_uuids: [@objects_counter_id],
|
280
277
|
extra_counter_values: [count_allocations],
|
281
278
|
)
|
282
|
-
|
279
|
+
result
|
283
280
|
end
|
284
281
|
|
285
282
|
def begin_validate(query, validate)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "graphql/tracing/
|
3
|
+
require "graphql/tracing/monitor_trace"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Tracing
|
@@ -26,93 +26,66 @@ module GraphQL
|
|
26
26
|
#
|
27
27
|
# # Then run:
|
28
28
|
# # bundle exec prometheus_exporter -a lib/graphql_collector.rb
|
29
|
+
PrometheusTrace = MonitorTrace.create_module("prometheus")
|
29
30
|
module PrometheusTrace
|
30
31
|
if defined?(PrometheusExporter::Server)
|
31
32
|
autoload :GraphQLCollector, "graphql/tracing/prometheus_trace/graphql_collector"
|
32
33
|
end
|
33
|
-
include PlatformTrace
|
34
34
|
|
35
|
-
def initialize(client: PrometheusExporter::Client.default, keys_whitelist: [
|
36
|
-
@
|
37
|
-
@
|
38
|
-
@
|
39
|
-
|
40
|
-
super
|
35
|
+
def initialize(client: PrometheusExporter::Client.default, keys_whitelist: [:execute_field], collector_type: "graphql", **rest)
|
36
|
+
@prometheus_client = client
|
37
|
+
@prometheus_keys_whitelist = keys_whitelist.map(&:to_sym) # handle previous string keys
|
38
|
+
@prometheus_collector_type = collector_type
|
39
|
+
setup_prometheus_monitor(**rest)
|
40
|
+
super
|
41
41
|
end
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
module_eval <<-RUBY, __FILE__, __LINE__
|
56
|
-
def #{trace_method}(**data)
|
57
|
-
instrument_prometheus_execution("#{platform_key}", "#{trace_method}") { super }
|
43
|
+
attr_reader :prometheus_collector_type, :prometheus_client, :prometheus_keys_whitelist
|
44
|
+
|
45
|
+
class PrometheusMonitor < MonitorTrace::Monitor
|
46
|
+
def instrument(keyword, object)
|
47
|
+
if active?(keyword)
|
48
|
+
start = gettime
|
49
|
+
result = yield
|
50
|
+
duration = gettime - start
|
51
|
+
send_json(duration, keyword, object)
|
52
|
+
result
|
53
|
+
else
|
54
|
+
yield
|
58
55
|
end
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
# rubocop:enable Development/NoEvalCop
|
63
|
-
|
64
|
-
def platform_execute_field(platform_key, &block)
|
65
|
-
instrument_prometheus_execution(platform_key, "execute_field", &block)
|
66
|
-
end
|
67
|
-
|
68
|
-
def platform_execute_field_lazy(platform_key, &block)
|
69
|
-
instrument_prometheus_execution(platform_key, "execute_field_lazy", &block)
|
70
|
-
end
|
71
|
-
|
72
|
-
def platform_authorized(platform_key, &block)
|
73
|
-
instrument_prometheus_execution(platform_key, "authorized", &block)
|
74
|
-
end
|
75
|
-
|
76
|
-
def platform_authorized_lazy(platform_key, &block)
|
77
|
-
instrument_prometheus_execution(platform_key, "authorized_lazy", &block)
|
78
|
-
end
|
79
|
-
|
80
|
-
def platform_resolve_type(platform_key, &block)
|
81
|
-
instrument_prometheus_execution(platform_key, "resolve_type", &block)
|
82
|
-
end
|
56
|
+
end
|
83
57
|
|
84
|
-
|
85
|
-
|
86
|
-
|
58
|
+
def active?(keyword)
|
59
|
+
@trace.prometheus_keys_whitelist.include?(keyword)
|
60
|
+
end
|
87
61
|
|
88
|
-
|
89
|
-
|
90
|
-
|
62
|
+
def gettime
|
63
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
64
|
+
end
|
91
65
|
|
92
|
-
|
93
|
-
|
94
|
-
|
66
|
+
def send_json(duration, keyword, object)
|
67
|
+
event_name = name_for(keyword, object)
|
68
|
+
@trace.prometheus_client.send_json(
|
69
|
+
type: @trace.prometheus_collector_type,
|
70
|
+
duration: duration,
|
71
|
+
platform_key: event_name,
|
72
|
+
key: keyword
|
73
|
+
)
|
74
|
+
end
|
95
75
|
|
96
|
-
|
97
|
-
"#{type.graphql_name}.resolve_type"
|
98
|
-
end
|
76
|
+
include MonitorTrace::Monitor::GraphQLPrefixNames
|
99
77
|
|
100
|
-
|
78
|
+
class Event < MonitorTrace::Monitor::Event
|
79
|
+
def start
|
80
|
+
@start_time = @monitor.gettime
|
81
|
+
end
|
101
82
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
type: @collector_type,
|
109
|
-
duration: duration,
|
110
|
-
platform_key: platform_key,
|
111
|
-
key: key
|
112
|
-
)
|
113
|
-
result
|
114
|
-
else
|
115
|
-
yield
|
83
|
+
def finish
|
84
|
+
if @monitor.active?(keyword)
|
85
|
+
duration = @monitor.gettime - @start_time
|
86
|
+
@monitor.send_json(duration, keyword, object)
|
87
|
+
end
|
88
|
+
end
|
116
89
|
end
|
117
90
|
end
|
118
91
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "graphql/tracing/
|
3
|
+
require "graphql/tracing/monitor_trace"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Tracing
|
@@ -10,73 +10,39 @@ module GraphQL
|
|
10
10
|
# class MySchema < GraphQL::Schema
|
11
11
|
# trace_with GraphQL::Tracing::ScoutTrace
|
12
12
|
# end
|
13
|
+
ScoutTrace = MonitorTrace.create_module("scout")
|
13
14
|
module ScoutTrace
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def initialize(set_transaction_name: false, **_rest)
|
22
|
-
self.class.include(ScoutApm::Tracer)
|
23
|
-
@set_transaction_name = set_transaction_name
|
24
|
-
super
|
25
|
-
end
|
26
|
-
|
27
|
-
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
|
28
|
-
|
29
|
-
{
|
30
|
-
"lex" => "lex.graphql",
|
31
|
-
"parse" => "parse.graphql",
|
32
|
-
"validate" => "validate.graphql",
|
33
|
-
"analyze_query" => "analyze.graphql",
|
34
|
-
"analyze_multiplex" => "analyze.graphql",
|
35
|
-
"execute_multiplex" => "execute.graphql",
|
36
|
-
"execute_query" => "execute.graphql",
|
37
|
-
"execute_query_lazy" => "execute.graphql",
|
38
|
-
}.each do |trace_method, platform_key|
|
39
|
-
module_eval <<-RUBY, __FILE__, __LINE__
|
40
|
-
def #{trace_method}(**data)
|
41
|
-
#{
|
42
|
-
if trace_method == "execute_query"
|
43
|
-
<<-RUBY
|
44
|
-
set_this_txn_name = data[:query].context[:set_scout_transaction_name]
|
45
|
-
if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name)
|
46
|
-
ScoutApm::Transaction.rename(transaction_name(data[:query]))
|
47
|
-
end
|
48
|
-
RUBY
|
15
|
+
class ScoutMonitor < MonitorTrace::Monitor
|
16
|
+
def instrument(keyword, object)
|
17
|
+
if keyword == :execute
|
18
|
+
query = object.queries.first
|
19
|
+
set_this_txn_name = query.context[:set_scout_transaction_name]
|
20
|
+
if set_this_txn_name == true || (set_this_txn_name.nil? && @set_transaction_name)
|
21
|
+
ScoutApm::Transaction.rename(transaction_name(query))
|
49
22
|
end
|
50
|
-
|
23
|
+
end
|
51
24
|
|
52
|
-
|
53
|
-
|
25
|
+
ScoutApm::Tracer.instrument("GraphQL", name_for(keyword, object), INSTRUMENT_OPTS) do
|
26
|
+
yield
|
54
27
|
end
|
55
28
|
end
|
56
|
-
RUBY
|
57
|
-
end
|
58
|
-
# rubocop:enable Development/NoEvalCop
|
59
|
-
|
60
|
-
def platform_execute_field(platform_key, &block)
|
61
|
-
self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
|
62
|
-
end
|
63
|
-
|
64
|
-
def platform_authorized(platform_key, &block)
|
65
|
-
self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
|
66
|
-
end
|
67
29
|
|
68
|
-
|
30
|
+
INSTRUMENT_OPTS = { scope: true }
|
69
31
|
|
70
|
-
|
71
|
-
field.path
|
72
|
-
end
|
32
|
+
include MonitorTrace::Monitor::GraphQLSuffixNames
|
73
33
|
|
74
|
-
|
75
|
-
|
76
|
-
|
34
|
+
class Event < MonitorTrace::Monitor::Event
|
35
|
+
def start
|
36
|
+
layer = ScoutApm::Layer.new("GraphQL", @monitor.name_for(keyword, object))
|
37
|
+
layer.subscopable!
|
38
|
+
@scout_req = ScoutApm::RequestManager.lookup
|
39
|
+
@scout_req.start_layer(layer)
|
40
|
+
end
|
77
41
|
|
78
|
-
|
79
|
-
|
42
|
+
def finish
|
43
|
+
@scout_req.stop_layer
|
44
|
+
end
|
45
|
+
end
|
80
46
|
end
|
81
47
|
end
|
82
48
|
end
|