fluent-plugin-prometheus 1.7.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ require 'async'
2
+
3
+ module Fluent::Plugin
4
+ class PrometheusInput
5
+ module AsyncWrapper
6
+ def do_request(host:, port:, secure:)
7
+ endpoint =
8
+ if secure
9
+ context = OpenSSL::SSL::SSLContext.new
10
+ context.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ Async::HTTP::Endpoint.parse("https://#{host}:#{port}", ssl_context: context)
12
+ else
13
+ Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
14
+ end
15
+
16
+ Async::HTTP::Client.open(endpoint) do |client|
17
+ yield(AsyncHttpWrapper.new(client))
18
+ end
19
+ end
20
+
21
+ Response = Struct.new(:code, :body, :headers)
22
+
23
+ class AsyncHttpWrapper
24
+ def initialize(http)
25
+ @http = http
26
+ end
27
+
28
+ def get(path)
29
+ error = nil
30
+ response = Async::Task.current.async {
31
+ begin
32
+ @http.get(path)
33
+ rescue => e # Async::Reactor rescue all error. handle it by itself
34
+ error = e
35
+ end
36
+ }.wait
37
+
38
+ if error
39
+ raise error
40
+ end
41
+
42
+ Response.new(response.status.to_s, response.read || '', response.headers)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -24,14 +24,14 @@ module Fluent::Plugin
24
24
  def configure(conf)
25
25
  super
26
26
  hostname = Socket.gethostname
27
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
28
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
27
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
28
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
29
29
  @base_labels = parse_labels_elements(conf)
30
30
  @base_labels.each do |key, value|
31
31
  unless value.is_a?(String)
32
32
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_monitor"
33
33
  end
34
- @base_labels[key] = expander.expand(value, placeholders)
34
+ @base_labels[key] = expander.expand(value)
35
35
  end
36
36
 
37
37
  if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
@@ -46,19 +46,19 @@ module Fluent::Plugin
46
46
  def start
47
47
  super
48
48
 
49
- @buffer_newest_timekey = @registry.gauge(
49
+ @buffer_newest_timekey = get_gauge(
50
50
  :fluentd_status_buffer_newest_timekey,
51
51
  'Newest timekey in buffer.')
52
- @buffer_oldest_timekey = @registry.gauge(
52
+ @buffer_oldest_timekey = get_gauge(
53
53
  :fluentd_status_buffer_oldest_timekey,
54
54
  'Oldest timekey in buffer.')
55
- buffer_queue_length = @registry.gauge(
55
+ buffer_queue_length = get_gauge(
56
56
  :fluentd_status_buffer_queue_length,
57
57
  'Current buffer queue length.')
58
- buffer_total_queued_size = @registry.gauge(
58
+ buffer_total_queued_size = get_gauge(
59
59
  :fluentd_status_buffer_total_bytes,
60
60
  'Current total size of queued buffers.')
61
- retry_counts = @registry.gauge(
61
+ retry_counts = get_gauge(
62
62
  :fluentd_status_retry_count,
63
63
  'Current retry counts.')
64
64
 
@@ -76,14 +76,14 @@ module Fluent::Plugin
76
76
 
77
77
  @monitor_info.each do |name, metric|
78
78
  if info[name]
79
- metric.set(label, info[name])
79
+ metric.set(info[name], labels: label)
80
80
  end
81
81
  end
82
82
 
83
83
  timekeys = info["buffer_timekeys"]
84
84
  if timekeys && !timekeys.empty?
85
- @buffer_newest_timekey.set(label, timekeys.max)
86
- @buffer_oldest_timekey.set(label, timekeys.min)
85
+ @buffer_newest_timekey.set(timekeys.max, labels: label)
86
+ @buffer_oldest_timekey.set(timekeys.min, labels: label)
87
87
  end
88
88
  end
89
89
  end
@@ -95,5 +95,13 @@ module Fluent::Plugin
95
95
  type: plugin_info["type"],
96
96
  )
97
97
  end
98
+
99
+ def get_gauge(name, docstring)
100
+ if @registry.exist?(name)
101
+ @registry.get(name)
102
+ else
103
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :plugin_category, :type])
104
+ end
105
+ end
98
106
  end
99
107
  end
@@ -1,15 +1,16 @@
1
- require 'fluent/input'
1
+ require 'fluent/plugin/input'
2
2
  require 'fluent/plugin/in_monitor_agent'
3
3
  require 'fluent/plugin/prometheus'
4
4
 
5
5
  module Fluent::Plugin
6
- class PrometheusOutputMonitorInput < Fluent::Input
6
+ class PrometheusOutputMonitorInput < Fluent::Plugin::Input
7
7
  Fluent::Plugin.register_input('prometheus_output_monitor', self)
8
8
  include Fluent::Plugin::PrometheusLabelParser
9
9
 
10
10
  helpers :timer
11
11
 
12
12
  config_param :interval, :time, default: 5
13
+ config_param :gauge_all, :bool, default: true
13
14
  attr_reader :registry
14
15
 
15
16
  MONITOR_IVARS = [
@@ -43,22 +44,19 @@ module Fluent::Plugin
43
44
  def configure(conf)
44
45
  super
45
46
  hostname = Socket.gethostname
46
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
47
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
47
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
48
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
48
49
  @base_labels = parse_labels_elements(conf)
49
50
  @base_labels.each do |key, value|
50
51
  unless value.is_a?(String)
51
52
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_output_monitor"
52
53
  end
53
- @base_labels[key] = expander.expand(value, placeholders)
54
+ @base_labels[key] = expander.expand(value)
54
55
  end
55
56
 
56
- if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
57
- # from v0.14.6
58
- @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
59
- else
60
- @monitor_agent = Fluent::MonitorAgentInput.new
61
- end
57
+ @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
58
+
59
+ @gauge_or_counter = @gauge_all ? :gauge : :counter
62
60
  end
63
61
 
64
62
  def start
@@ -66,57 +64,57 @@ module Fluent::Plugin
66
64
 
67
65
  @metrics = {
68
66
  # Buffer metrics
69
- buffer_total_queued_size: @registry.gauge(
67
+ buffer_total_queued_size: get_gauge(
70
68
  :fluentd_output_status_buffer_total_bytes,
71
69
  'Current total size of stage and queue buffers.'),
72
- buffer_stage_length: @registry.gauge(
70
+ buffer_stage_length: get_gauge(
73
71
  :fluentd_output_status_buffer_stage_length,
74
72
  'Current length of stage buffers.'),
75
- buffer_stage_byte_size: @registry.gauge(
73
+ buffer_stage_byte_size: get_gauge(
76
74
  :fluentd_output_status_buffer_stage_byte_size,
77
75
  'Current total size of stage buffers.'),
78
- buffer_queue_length: @registry.gauge(
76
+ buffer_queue_length: get_gauge(
79
77
  :fluentd_output_status_buffer_queue_length,
80
78
  'Current length of queue buffers.'),
81
- buffer_queue_byte_size: @registry.gauge(
79
+ buffer_queue_byte_size: get_gauge(
82
80
  :fluentd_output_status_queue_byte_size,
83
81
  'Current total size of queue buffers.'),
84
- buffer_available_buffer_space_ratios: @registry.gauge(
82
+ buffer_available_buffer_space_ratios: get_gauge(
85
83
  :fluentd_output_status_buffer_available_space_ratio,
86
84
  'Ratio of available space in buffer.'),
87
- buffer_newest_timekey: @registry.gauge(
85
+ buffer_newest_timekey: get_gauge(
88
86
  :fluentd_output_status_buffer_newest_timekey,
89
87
  'Newest timekey in buffer.'),
90
- buffer_oldest_timekey: @registry.gauge(
88
+ buffer_oldest_timekey: get_gauge(
91
89
  :fluentd_output_status_buffer_oldest_timekey,
92
90
  'Oldest timekey in buffer.'),
93
91
 
94
92
  # Output metrics
95
- retry_counts: @registry.gauge(
93
+ retry_counts: get_gauge_or_counter(
96
94
  :fluentd_output_status_retry_count,
97
95
  'Current retry counts.'),
98
- num_errors: @registry.gauge(
96
+ num_errors: get_gauge_or_counter(
99
97
  :fluentd_output_status_num_errors,
100
98
  'Current number of errors.'),
101
- emit_count: @registry.gauge(
99
+ emit_count: get_gauge_or_counter(
102
100
  :fluentd_output_status_emit_count,
103
101
  'Current emit counts.'),
104
- emit_records: @registry.gauge(
102
+ emit_records: get_gauge_or_counter(
105
103
  :fluentd_output_status_emit_records,
106
104
  'Current emit records.'),
107
- write_count: @registry.gauge(
105
+ write_count: get_gauge_or_counter(
108
106
  :fluentd_output_status_write_count,
109
107
  'Current write counts.'),
110
- rollback_count: @registry.gauge(
108
+ rollback_count: get_gauge(
111
109
  :fluentd_output_status_rollback_count,
112
110
  'Current rollback counts.'),
113
- flush_time_count: @registry.gauge(
111
+ flush_time_count: get_gauge_or_counter(
114
112
  :fluentd_output_status_flush_time_count,
115
113
  'Total flush time.'),
116
- slow_flush_count: @registry.gauge(
114
+ slow_flush_count: get_gauge_or_counter(
117
115
  :fluentd_output_status_slow_flush_count,
118
116
  'Current slow flush counts.'),
119
- retry_wait: @registry.gauge(
117
+ retry_wait: get_gauge(
120
118
  :fluentd_output_status_retry_wait,
121
119
  'Current retry wait'),
122
120
  }
@@ -146,7 +144,16 @@ module Fluent::Plugin
146
144
 
147
145
  # output metrics
148
146
  'retry_count' => @metrics[:retry_counts],
147
+ # Needed since Fluentd v1.14 due to metrics extensions.
148
+ 'num_errors' => @metrics[:num_errors],
149
+ 'write_count' => @metrics[:write_count],
150
+ 'emit_count' => @metrics[:emit_count],
151
+ 'emit_records' => @metrics[:emit_records],
152
+ 'rollback_count' => @metrics[:rollback_count],
153
+ 'flush_time_count' => @metrics[:flush_time_count],
154
+ 'slow_flush_count' => @metrics[:slow_flush_count],
149
155
  }
156
+ # No needed for Fluentd v1.14 but leave as-is for backward compatibility.
150
157
  instance_vars_info = {
151
158
  num_errors: @metrics[:num_errors],
152
159
  write_count: @metrics[:write_count],
@@ -162,14 +169,22 @@ module Fluent::Plugin
162
169
 
163
170
  monitor_info.each do |name, metric|
164
171
  if info[name]
165
- metric.set(label, info[name])
172
+ if metric.is_a?(::Prometheus::Client::Gauge)
173
+ metric.set(info[name], labels: label)
174
+ elsif metric.is_a?(::Prometheus::Client::Counter)
175
+ metric.increment(by: info[name] - metric.get(labels: label), labels: label)
176
+ end
166
177
  end
167
178
  end
168
179
 
169
180
  if info['instance_variables']
170
181
  instance_vars_info.each do |name, metric|
171
182
  if info['instance_variables'][name]
172
- metric.set(label, info['instance_variables'][name])
183
+ if metric.is_a?(::Prometheus::Client::Gauge)
184
+ metric.set(info['instance_variables'][name], labels: label)
185
+ elsif metric.is_a?(::Prometheus::Client::Counter)
186
+ metric.increment(by: info['instance_variables'][name] - metric.get(labels: label), labels: label)
187
+ end
173
188
  end
174
189
  end
175
190
  end
@@ -187,7 +202,7 @@ module Fluent::Plugin
187
202
  if next_time && start_time
188
203
  wait = next_time - start_time
189
204
  end
190
- @metrics[:retry_wait].set(label, wait.to_f)
205
+ @metrics[:retry_wait].set(wait.to_f, labels: label)
191
206
  end
192
207
  end
193
208
  end
@@ -198,5 +213,21 @@ module Fluent::Plugin
198
213
  type: plugin_info["type"],
199
214
  )
200
215
  end
216
+
217
+ def get_gauge(name, docstring)
218
+ if @registry.exist?(name)
219
+ @registry.get(name)
220
+ else
221
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type])
222
+ end
223
+ end
224
+
225
+ def get_gauge_or_counter(name, docstring)
226
+ if @registry.exist?(name)
227
+ @registry.get(name)
228
+ else
229
+ @registry.public_send(@gauge_or_counter, name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type])
230
+ end
231
+ end
201
232
  end
202
233
  end
@@ -28,32 +28,27 @@ module Fluent::Plugin
28
28
  def configure(conf)
29
29
  super
30
30
  hostname = Socket.gethostname
31
- expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
32
- placeholders = expander.prepare_placeholders({'hostname' => hostname, 'worker_id' => fluentd_worker_id})
31
+ expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
32
+ expander = expander_builder.build({ 'hostname' => hostname, 'worker_id' => fluentd_worker_id })
33
33
  @base_labels = parse_labels_elements(conf)
34
34
  @base_labels.each do |key, value|
35
35
  unless value.is_a?(String)
36
36
  raise Fluent::ConfigError, "record accessor syntax is not available in prometheus_tail_monitor"
37
37
  end
38
- @base_labels[key] = expander.expand(value, placeholders)
38
+ @base_labels[key] = expander.expand(value)
39
39
  end
40
40
 
41
- if defined?(Fluent::Plugin) && defined?(Fluent::Plugin::MonitorAgentInput)
42
- # from v0.14.6
43
- @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
44
- else
45
- @monitor_agent = Fluent::MonitorAgentInput.new
46
- end
41
+ @monitor_agent = Fluent::Plugin::MonitorAgentInput.new
47
42
  end
48
43
 
49
44
  def start
50
45
  super
51
46
 
52
47
  @metrics = {
53
- position: @registry.gauge(
48
+ position: get_gauge(
54
49
  :fluentd_tail_file_position,
55
50
  'Current position of file.'),
56
- inode: @registry.gauge(
51
+ inode: get_gauge(
57
52
  :fluentd_tail_file_inode,
58
53
  'Current inode of file.'),
59
54
  }
@@ -78,8 +73,8 @@ module Fluent::Plugin
78
73
  # Very fragile implementation
79
74
  pe = watcher.instance_variable_get(:@pe)
80
75
  label = labels(info, watcher.path)
81
- @metrics[:inode].set(label, pe.read_inode)
82
- @metrics[:position].set(label, pe.read_pos)
76
+ @metrics[:inode].set(pe.read_inode, labels: label)
77
+ @metrics[:position].set(pe.read_pos, labels: label)
83
78
  end
84
79
  end
85
80
  end
@@ -91,5 +86,13 @@ module Fluent::Plugin
91
86
  path: path,
92
87
  )
93
88
  end
89
+
90
+ def get_gauge(name, docstring)
91
+ if @registry.exist?(name)
92
+ @registry.get(name)
93
+ else
94
+ @registry.gauge(name, docstring: docstring, labels: @base_labels.keys + [:plugin_id, :type, :path])
95
+ end
96
+ end
94
97
  end
95
98
  end
@@ -1,6 +1,6 @@
1
1
  require 'prometheus/client'
2
2
  require 'prometheus/client/formats/text'
3
- require 'fluent/plugin/filter_record_transformer'
3
+ require 'fluent/plugin/prometheus/placeholder_expander'
4
4
 
5
5
  module Fluent
6
6
  module Plugin
@@ -82,14 +82,25 @@ module Fluent
82
82
  end
83
83
 
84
84
  def self.placeholder_expander(log)
85
- # Use internal class in order to expand placeholder
86
- Fluent::Plugin::RecordTransformerFilter::PlaceholderExpander.new(log: log)
85
+ Fluent::Plugin::Prometheus::ExpandBuilder.new(log: log)
86
+ end
87
+
88
+ def stringify_keys(hash_to_stringify)
89
+ # Adapted from: https://www.jvt.me/posts/2019/09/07/ruby-hash-keys-string-symbol/
90
+ hash_to_stringify.map do |k,v|
91
+ value_or_hash = if v.instance_of? Hash
92
+ stringify_keys(v)
93
+ else
94
+ v
95
+ end
96
+ [k.to_s, value_or_hash]
97
+ end.to_h
87
98
  end
88
99
 
89
100
  def configure(conf)
90
101
  super
91
102
  @placeholder_values = {}
92
- @placeholder_expander = Fluent::Plugin::Prometheus.placeholder_expander(log)
103
+ @placeholder_expander_builder = Fluent::Plugin::Prometheus.placeholder_expander(log)
93
104
  @hostname = Socket.gethostname
94
105
  end
95
106
 
@@ -100,11 +111,12 @@ module Fluent
100
111
  'worker_id' => fluentd_worker_id,
101
112
  }
102
113
 
114
+ record = stringify_keys(record)
103
115
  placeholders = record.merge(@placeholder_values[tag])
104
- placeholders = @placeholder_expander.prepare_placeholders(placeholders)
116
+ expander = @placeholder_expander_builder.build(placeholders)
105
117
  metrics.each do |metric|
106
118
  begin
107
- metric.instrument(record, @placeholder_expander, placeholders)
119
+ metric.instrument(record, expander)
108
120
  rescue => e
109
121
  log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
110
122
  router.emit_error_event(tag, time, record, e)
@@ -120,11 +132,12 @@ module Fluent
120
132
  }
121
133
 
122
134
  es.each do |time, record|
135
+ record = stringify_keys(record)
123
136
  placeholders = record.merge(placeholder_values)
124
- placeholders = @placeholder_expander.prepare_placeholders(placeholders)
137
+ expander = @placeholder_expander_builder.build(placeholders)
125
138
  metrics.each do |metric|
126
139
  begin
127
- metric.instrument(record, @placeholder_expander, placeholders)
140
+ metric.instrument(record, expander)
128
141
  rescue => e
129
142
  log.warn "prometheus: failed to instrument a metric.", error_class: e.class, error: e, tag: tag, name: metric.name
130
143
  router.emit_error_event(tag, time, record, e)
@@ -154,11 +167,11 @@ module Fluent
154
167
  @base_labels = labels.merge(@base_labels)
155
168
  end
156
169
 
157
- def labels(record, expander, placeholders)
170
+ def labels(record, expander)
158
171
  label = {}
159
172
  @base_labels.each do |k, v|
160
173
  if v.is_a?(String)
161
- label[k] = expander.expand(v, placeholders)
174
+ label[k] = expander.expand(v)
162
175
  else
163
176
  label[k] = v.call(record)
164
177
  end
@@ -189,20 +202,20 @@ module Fluent
189
202
  end
190
203
 
191
204
  begin
192
- @gauge = registry.gauge(element['name'].to_sym, element['desc'])
205
+ @gauge = registry.gauge(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
193
206
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
194
207
  @gauge = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :gauge, element['desc'])
195
208
  end
196
209
  end
197
210
 
198
- def instrument(record, expander, placeholders)
211
+ def instrument(record, expander)
199
212
  if @key.is_a?(String)
200
213
  value = record[@key]
201
214
  else
202
215
  value = @key.call(record)
203
216
  end
204
217
  if value
205
- @gauge.set(labels(record, expander, placeholders), value)
218
+ @gauge.set(value, labels: labels(record, expander))
206
219
  end
207
220
  end
208
221
  end
@@ -211,13 +224,13 @@ module Fluent
211
224
  def initialize(element, registry, labels)
212
225
  super
213
226
  begin
214
- @counter = registry.counter(element['name'].to_sym, element['desc'])
227
+ @counter = registry.counter(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
215
228
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
216
229
  @counter = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :counter, element['desc'])
217
230
  end
218
231
  end
219
232
 
220
- def instrument(record, expander, placeholders)
233
+ def instrument(record, expander)
221
234
  # use record value of the key if key is specified, otherwise just increment
222
235
  if @key.nil?
223
236
  value = 1
@@ -230,7 +243,7 @@ module Fluent
230
243
  # ignore if record value is nil
231
244
  return if value.nil?
232
245
 
233
- @counter.increment(labels(record, expander, placeholders), value)
246
+ @counter.increment(by: value, labels: labels(record, expander))
234
247
  end
235
248
  end
236
249
 
@@ -242,20 +255,20 @@ module Fluent
242
255
  end
243
256
 
244
257
  begin
245
- @summary = registry.summary(element['name'].to_sym, element['desc'])
258
+ @summary = registry.summary(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
246
259
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
247
260
  @summary = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :summary, element['desc'])
248
261
  end
249
262
  end
250
263
 
251
- def instrument(record, expander, placeholders)
264
+ def instrument(record, expander)
252
265
  if @key.is_a?(String)
253
266
  value = record[@key]
254
267
  else
255
268
  value = @key.call(record)
256
269
  end
257
270
  if value
258
- @summary.observe(labels(record, expander, placeholders), value)
271
+ @summary.observe(value, labels: labels(record, expander))
259
272
  end
260
273
  end
261
274
  end
@@ -272,23 +285,23 @@ module Fluent
272
285
  buckets = element['buckets'].split(/,/).map(&:strip).map do |e|
273
286
  e[/\A\d+.\d+\Z/] ? e.to_f : e.to_i
274
287
  end
275
- @histogram = registry.histogram(element['name'].to_sym, element['desc'], {}, buckets)
288
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys, buckets: buckets)
276
289
  else
277
- @histogram = registry.histogram(element['name'].to_sym, element['desc'])
290
+ @histogram = registry.histogram(element['name'].to_sym, docstring: element['desc'], labels: @base_labels.keys)
278
291
  end
279
292
  rescue ::Prometheus::Client::Registry::AlreadyRegisteredError
280
293
  @histogram = Fluent::Plugin::Prometheus::Metric.get(registry, element['name'].to_sym, :histogram, element['desc'])
281
294
  end
282
295
  end
283
296
 
284
- def instrument(record, expander, placeholders)
297
+ def instrument(record, expander)
285
298
  if @key.is_a?(String)
286
299
  value = record[@key]
287
300
  else
288
301
  value = @key.call(record)
289
302
  end
290
303
  if value
291
- @histogram.observe(labels(record, expander, placeholders), value)
304
+ @histogram.observe(value, labels: labels(record, expander))
292
305
  end
293
306
  end
294
307
  end