fluentd 0.14.8 → 0.14.9

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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CONTRIBUTING.md +6 -1
  4. data/ChangeLog +38 -0
  5. data/Rakefile +21 -0
  6. data/example/out_exec_filter.conf +42 -0
  7. data/lib/fluent/agent.rb +2 -2
  8. data/lib/fluent/command/binlog_reader.rb +1 -1
  9. data/lib/fluent/command/cat.rb +5 -2
  10. data/lib/fluent/compat/output.rb +7 -8
  11. data/lib/fluent/compat/parser.rb +139 -11
  12. data/lib/fluent/config/configure_proxy.rb +2 -11
  13. data/lib/fluent/config/section.rb +7 -0
  14. data/lib/fluent/configurable.rb +1 -3
  15. data/lib/fluent/log.rb +1 -1
  16. data/lib/fluent/plugin/base.rb +17 -0
  17. data/lib/fluent/plugin/filter_parser.rb +108 -0
  18. data/lib/fluent/plugin/filter_record_transformer.rb +4 -7
  19. data/lib/fluent/plugin/filter_stdout.rb +1 -1
  20. data/lib/fluent/plugin/formatter.rb +5 -0
  21. data/lib/fluent/plugin/formatter_msgpack.rb +4 -0
  22. data/lib/fluent/plugin/formatter_stdout.rb +3 -2
  23. data/lib/fluent/plugin/formatter_tsv.rb +34 -0
  24. data/lib/fluent/plugin/in_exec.rb +48 -93
  25. data/lib/fluent/plugin/in_forward.rb +25 -105
  26. data/lib/fluent/plugin/in_http.rb +68 -65
  27. data/lib/fluent/plugin/in_syslog.rb +29 -51
  28. data/lib/fluent/plugin/multi_output.rb +1 -3
  29. data/lib/fluent/plugin/out_exec.rb +58 -71
  30. data/lib/fluent/plugin/out_exec_filter.rb +199 -279
  31. data/lib/fluent/plugin/out_file.rb +155 -80
  32. data/lib/fluent/plugin/out_forward.rb +44 -47
  33. data/lib/fluent/plugin/out_stdout.rb +6 -21
  34. data/lib/fluent/plugin/output.rb +23 -17
  35. data/lib/fluent/plugin/parser.rb +121 -61
  36. data/lib/fluent/plugin/parser_csv.rb +9 -3
  37. data/lib/fluent/plugin/parser_json.rb +37 -35
  38. data/lib/fluent/plugin/parser_ltsv.rb +11 -19
  39. data/lib/fluent/plugin/parser_msgpack.rb +50 -0
  40. data/lib/fluent/plugin/parser_regexp.rb +15 -42
  41. data/lib/fluent/plugin/parser_tsv.rb +8 -3
  42. data/lib/fluent/plugin_helper.rb +8 -1
  43. data/lib/fluent/plugin_helper/child_process.rb +139 -73
  44. data/lib/fluent/plugin_helper/compat_parameters.rb +93 -4
  45. data/lib/fluent/plugin_helper/event_emitter.rb +14 -1
  46. data/lib/fluent/plugin_helper/extract.rb +16 -4
  47. data/lib/fluent/plugin_helper/formatter.rb +9 -11
  48. data/lib/fluent/plugin_helper/inject.rb +4 -0
  49. data/lib/fluent/plugin_helper/parser.rb +3 -3
  50. data/lib/fluent/root_agent.rb +1 -1
  51. data/lib/fluent/test/driver/base.rb +51 -37
  52. data/lib/fluent/test/driver/base_owner.rb +18 -8
  53. data/lib/fluent/test/driver/multi_output.rb +2 -1
  54. data/lib/fluent/test/driver/output.rb +29 -6
  55. data/lib/fluent/test/helpers.rb +3 -1
  56. data/lib/fluent/test/log.rb +4 -0
  57. data/lib/fluent/test/startup_shutdown.rb +13 -0
  58. data/lib/fluent/time.rb +14 -8
  59. data/lib/fluent/version.rb +1 -1
  60. data/test/command/test_binlog_reader.rb +5 -1
  61. data/test/config/test_configurable.rb +173 -0
  62. data/test/config/test_configure_proxy.rb +0 -43
  63. data/test/plugin/test_base.rb +16 -0
  64. data/test/plugin/test_filter_parser.rb +665 -0
  65. data/test/plugin/test_filter_record_transformer.rb +11 -3
  66. data/test/plugin/test_filter_stdout.rb +18 -27
  67. data/test/plugin/test_in_dummy.rb +1 -1
  68. data/test/plugin/test_in_exec.rb +206 -94
  69. data/test/plugin/test_in_forward.rb +310 -327
  70. data/test/plugin/test_in_http.rb +310 -186
  71. data/test/plugin/test_out_exec.rb +223 -68
  72. data/test/plugin/test_out_exec_filter.rb +520 -169
  73. data/test/plugin/test_out_file.rb +620 -177
  74. data/test/plugin/test_out_forward.rb +110 -132
  75. data/test/plugin/test_out_null.rb +1 -1
  76. data/test/plugin/test_out_secondary_file.rb +4 -2
  77. data/test/plugin/test_out_stdout.rb +14 -35
  78. data/test/plugin/test_parser.rb +359 -0
  79. data/test/plugin/test_parser_csv.rb +1 -2
  80. data/test/plugin/test_parser_json.rb +3 -4
  81. data/test/plugin/test_parser_labeled_tsv.rb +1 -2
  82. data/test/plugin/test_parser_none.rb +1 -2
  83. data/test/plugin/test_parser_regexp.rb +8 -4
  84. data/test/plugin/test_parser_tsv.rb +4 -3
  85. data/test/plugin_helper/test_child_process.rb +184 -0
  86. data/test/plugin_helper/test_compat_parameters.rb +88 -1
  87. data/test/plugin_helper/test_extract.rb +0 -1
  88. data/test/plugin_helper/test_formatter.rb +5 -2
  89. data/test/plugin_helper/test_parser.rb +6 -5
  90. data/test/test_output.rb +24 -2
  91. data/test/test_plugin_classes.rb +20 -0
  92. data/test/test_root_agent.rb +139 -0
  93. data/test/test_test_drivers.rb +132 -0
  94. metadata +12 -4
  95. data/test/plugin/test_parser_base.rb +0 -32
@@ -166,6 +166,13 @@ module Fluent
166
166
  varname = subproxy.variable_name
167
167
  elements = (conf.respond_to?(:elements) ? conf.elements : []).select{ |e| e.name == subproxy.name.to_s || e.name == subproxy.alias.to_s }
168
168
  if elements.empty? && subproxy.init?
169
+ if subproxy.argument && !subproxy.defaults.has_key?(subproxy.argument.first)
170
+ raise ArgumentError, "#{name}: init is specified, but default value of argument is missing"
171
+ end
172
+ missing_keys = subproxy.params.keys.select{|param_name| !subproxy.defaults.has_key?(param_name)}
173
+ if !missing_keys.empty?
174
+ raise ArgumentError, "#{name}: init is specified, but there're parameters without default values:#{missing_keys.join(',')}"
175
+ end
169
176
  elements << Fluent::Config::Element.new(subproxy.name.to_s, '', {}, [])
170
177
  end
171
178
 
@@ -71,9 +71,7 @@ module Fluent
71
71
  root.instance_eval{ @params.keys }.each do |param_name|
72
72
  next if param_name.to_s.start_with?('@')
73
73
  varname = "@#{param_name}".to_sym
74
- if (! root[param_name].nil?) || (instance_variable_defined?(varname) && instance_variable_get(varname).nil?)
75
- instance_variable_set(varname, root[param_name])
76
- end
74
+ instance_variable_set(varname, root[param_name])
77
75
  end
78
76
 
79
77
  self
@@ -446,7 +446,7 @@ module Fluent
446
446
 
447
447
  # This class delegetes some methods which are used in `Fluent::Logger` to a instance variable(`dev`) in `Logger::LogDevice` class
448
448
  # https://github.com/ruby/ruby/blob/7b2d47132ff8ee950b0f978ab772dee868d9f1b0/lib/logger.rb#L661
449
- class LogDeviceIO < Logger::LogDevice
449
+ class LogDeviceIO < ::Logger::LogDevice
450
450
  def flush
451
451
  if @dev.respond_to?(:flush)
452
452
  @dev.flush
@@ -31,6 +31,7 @@ module Fluent
31
31
  def initialize
32
32
  super
33
33
  @_state = State.new(false, false, false, false, false, false, false, false, false)
34
+ @_context_router = nil
34
35
  @under_plugin_development = false
35
36
  end
36
37
 
@@ -45,6 +46,22 @@ module Fluent
45
46
  self
46
47
  end
47
48
 
49
+ def string_safe_encoding(str)
50
+ unless str.valid_encoding?
51
+ log.info "invalid byte sequence is replaced in `#{str}`" if self.respond_to?(:log)
52
+ str = str.scrub('?')
53
+ end
54
+ yield str
55
+ end
56
+
57
+ def context_router=(router)
58
+ @_context_router = router
59
+ end
60
+
61
+ def context_router
62
+ @_context_router
63
+ end
64
+
48
65
  def start
49
66
  @_state.start = true
50
67
  self
@@ -0,0 +1,108 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'fluent/time'
18
+ require 'fluent/config/error'
19
+ require 'fluent/plugin/filter'
20
+ require 'fluent/plugin_helper/parser'
21
+ require 'fluent/plugin_helper/compat_parameters'
22
+
23
+ module Fluent::Plugin
24
+ class ParserFilter < Filter
25
+ Fluent::Plugin.register_filter('parser', self)
26
+
27
+ helpers :parser, :compat_parameters
28
+
29
+ config_param :key_name, :string
30
+ config_param :reserve_data, :bool, default: false
31
+ config_param :reserve_time, :bool, default: false
32
+ config_param :inject_key_prefix, :string, default: nil
33
+ config_param :replace_invalid_sequence, :bool, default: false
34
+ config_param :hash_value_field, :string, default: nil
35
+
36
+ attr_reader :parser
37
+
38
+ def configure(conf)
39
+ compat_parameters_convert(conf, :parser)
40
+
41
+ super
42
+
43
+ @parser = parser_create
44
+ end
45
+
46
+ FAILED_RESULT = [nil, nil].freeze # reduce allocation cost
47
+ REPLACE_CHAR = '?'.freeze
48
+
49
+ def filter_with_time(tag, time, record)
50
+ raw_value = record[@key_name]
51
+ if raw_value.nil?
52
+ router.emit_error_event(tag, time, record, ArgumentError.new("#{@key_name} does not exist"))
53
+ if @reserve_data
54
+ return time, handle_parsed(tag, record, time, {})
55
+ else
56
+ return FAILED_RESULT
57
+ end
58
+ end
59
+ begin
60
+ @parser.parse(raw_value) do |t, values|
61
+ if values
62
+ t = if @reserve_time
63
+ time
64
+ else
65
+ t.nil? ? time : t
66
+ end
67
+ r = handle_parsed(tag, record, t, values)
68
+ return t, r
69
+ else
70
+ router.emit_error_event(tag, time, record, Fluent::Plugin::Parser::ParserError.new("pattern not match with data '#{raw_value}'"))
71
+ if @reserve_data
72
+ t = time
73
+ r = handle_parsed(tag, record, time, {})
74
+ return t, r
75
+ else
76
+ return FAILED_RESULT
77
+ end
78
+ end
79
+ end
80
+ rescue Fluent::Plugin::Parser::ParserError => e
81
+ router.emit_error_event(tag, time, record, e)
82
+ return FAILED_RESULT
83
+ rescue ArgumentError => e
84
+ raise unless @replace_invalid_sequence
85
+ raise unless e.message.index("invalid byte sequence in") == 0
86
+
87
+ raw_value = raw_value.scrub(REPLACE_CHAR)
88
+ retry
89
+ rescue => e
90
+ router.emit_error_event(tag, time, record, Fluent::Plugin::Parser::ParserError.new("parse failed #{e.message}"))
91
+ return FAILED_RESULT
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def handle_parsed(tag, record, t, values)
98
+ if values && @inject_key_prefix
99
+ values = Hash[values.map { |k, v| [@inject_key_prefix + k, v] }]
100
+ end
101
+ r = @hash_value_field ? {@hash_value_field => values} : values
102
+ if @reserve_data
103
+ r = r ? record.merge(r) : record
104
+ end
105
+ r
106
+ end
107
+ end
108
+ end
@@ -38,7 +38,7 @@ module Fluent::Plugin
38
38
  desc 'When set to true, the full Ruby syntax is enabled in the ${...} expression.'
39
39
  config_param :enable_ruby, :bool, default: false
40
40
  desc 'Use original value type.'
41
- config_param :auto_typecast, :bool, default: false # false for lower version compatibility
41
+ config_param :auto_typecast, :bool, default: true
42
42
 
43
43
  def configure(conf)
44
44
  super
@@ -95,10 +95,9 @@ module Fluent::Plugin
95
95
  last_record = nil
96
96
  es.each do |time, record|
97
97
  last_record = record # for debug log
98
- placeholder_values.merge!({
99
- 'time' => @placeholder_expander.time_value(time),
100
- 'record' => record,
101
- })
98
+ placeholder_values['time'] = @placeholder_expander.time_value(time)
99
+ placeholder_values['record'] = record
100
+
102
101
  new_record = reform(record, placeholder_values)
103
102
  if @renew_time_key && new_record.has_key?(@renew_time_key)
104
103
  time = Fluent::EventTime.from_time(Time.at(new_record[@renew_time_key].to_f))
@@ -316,8 +315,6 @@ module Fluent::Plugin
316
315
 
317
316
  class CleanroomExpander
318
317
  def expand(__str_to_eval__, tag, time, record, tag_parts, tag_prefix, tag_suffix, hostname)
319
- tags = tag_parts # for old version compatibility
320
- _ = tags # to suppress "unused variable" warning for tags
321
318
  Thread.current[:record_transformer_record] = record # for old version compatibility
322
319
  instance_eval(__str_to_eval__)
323
320
  end
@@ -34,7 +34,7 @@ module Fluent::Plugin
34
34
  def configure(conf)
35
35
  compat_parameters_convert(conf, :inject, :formatter)
36
36
  super
37
- @formatter = formatter_create(conf: @config.elements('format').first, default_type: DEFAULT_FORMAT_TYPE)
37
+ @formatter = formatter_create
38
38
  end
39
39
 
40
40
  def filter_stream(tag, es)
@@ -26,6 +26,11 @@ module Fluent
26
26
 
27
27
  configured_in :format
28
28
 
29
+ PARSER_TYPES = [:text_per_line, :text, :binary]
30
+ def formatter_type
31
+ :text_per_line
32
+ end
33
+
29
34
  def format(tag, time, record)
30
35
  raise NotImplementedError, "Implement this method in child class"
31
36
  end
@@ -21,6 +21,10 @@ module Fluent
21
21
  class MessagePackFormatter < Formatter
22
22
  Plugin.register_formatter('msgpack', self)
23
23
 
24
+ def formatter_type
25
+ :binary
26
+ end
27
+
24
28
  def format(tag, time, record)
25
29
  record.to_msgpack
26
30
  end
@@ -21,6 +21,8 @@ module Fluent
21
21
  class StdoutFormatter < Formatter
22
22
  Plugin.register_formatter('stdout', self)
23
23
 
24
+ TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%9N %z'
25
+
24
26
  config_param :output_type, :string, default: 'json'
25
27
 
26
28
  def configure(conf)
@@ -36,8 +38,7 @@ module Fluent
36
38
  end
37
39
 
38
40
  def format(tag, time, record)
39
- header = "#{Time.now.localtime} #{tag}: "
40
- "#{header}#{@sub_formatter.format(tag, time, record)}"
41
+ "#{Time.at(time).localtime.strftime(TIME_FORMAT)} #{tag}: #{@sub_formatter.format(tag, time, record).chomp}\n"
41
42
  end
42
43
 
43
44
  def stop
@@ -0,0 +1,34 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'fluent/plugin/formatter'
18
+
19
+ module Fluent
20
+ module Plugin
21
+ class TSVFormatter < Formatter
22
+ Plugin.register_formatter('tsv', self)
23
+
24
+ desc 'Field names included in each lines'
25
+ config_param :keys, :array, value_type: :string
26
+ desc 'The delimiter character (or string) of TSV values'
27
+ config_param :delimiter, :string, default: "\t"
28
+
29
+ def format(tag, time, record)
30
+ @keys.map{|k| record[k].to_s }.join(@delimiter)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -14,136 +14,91 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'strptime'
18
- require 'yajl'
19
-
20
17
  require 'fluent/plugin/input'
21
- require 'fluent/time'
22
- require 'fluent/timezone'
23
- require 'fluent/config/error'
18
+ require 'yajl'
24
19
 
25
20
  module Fluent::Plugin
26
21
  class ExecInput < Fluent::Plugin::Input
27
22
  Fluent::Plugin.register_input('exec', self)
28
23
 
29
- helpers :child_process
30
-
31
- def initialize
32
- super
33
- require 'fluent/plugin/exec_util'
34
- end
24
+ helpers :compat_parameters, :extract, :parser, :child_process
35
25
 
36
26
  desc 'The command (program) to execute.'
37
27
  config_param :command, :string
38
- desc 'The format used to map the program output to the incoming event.(tsv,json,msgpack)'
39
- config_param :format, :string, default: 'tsv'
40
- desc 'Specify the comma-separated keys when using the tsv format.'
41
- config_param :keys, default: [] do |val|
42
- val.split(',')
28
+
29
+ config_section :parse do
30
+ config_set_default :@type, 'tsv'
31
+ config_set_default :time_type, :float
32
+ config_set_default :time_key, nil
33
+ config_set_default :estimate_current_event, false
34
+ end
35
+
36
+ config_section :extract do
37
+ config_set_default :time_type, :float
43
38
  end
39
+
44
40
  desc 'Tag of the output events.'
45
41
  config_param :tag, :string, default: nil
46
- desc 'The key to use as the event tag instead of the value in the event record. '
47
- config_param :tag_key, :string, default: nil
48
- desc 'The key to use as the event time instead of the value in the event record.'
49
- config_param :time_key, :string, default: nil
50
- desc 'The format of the event time used for the time_key parameter.'
51
- config_param :time_format, :string, default: nil
52
42
  desc 'The interval time between periodic program runs.'
53
43
  config_param :run_interval, :time, default: nil
44
+ desc 'The default block size to read if parser requires partial read.'
45
+ config_param :read_block_size, :size, default: 10240 # 10k
54
46
 
55
- def configure(conf)
56
- super
57
-
58
- if conf['localtime']
59
- @localtime = true
60
- elsif conf['utc']
61
- @localtime = false
62
- end
63
-
64
- if conf['timezone']
65
- @timezone = conf['timezone']
66
- Fluent::Timezone.validate!(@timezone)
67
- end
68
-
69
- if !@tag && !@tag_key
70
- raise Fleunt::ConfigError, "'tag' or 'tag_key' option is required on exec input"
71
- end
47
+ attr_reader :parser
72
48
 
73
- if @time_key
74
- if @time_format
75
- f = @time_format
76
- @time_parse_proc =
77
- begin
78
- strptime = Strptime.new(f)
79
- Proc.new { |str| Fluent::EventTime.from_time(strptime.exec(str)) }
80
- rescue
81
- Proc.new {|str| Fluent::EventTime.from_time(Time.strptime(str, f)) }
82
- end
83
- else
84
- @time_parse_proc = Proc.new {|str| Fluent::EventTime.from_time(Time.at(str.to_f)) }
49
+ def configure(conf)
50
+ compat_parameters_convert(conf, :extract, :parser)
51
+ ['parse', 'extract'].each do |subsection_name|
52
+ if subsection = conf.elements(subsection_name).first
53
+ if subsection.has_key?('time_format')
54
+ subsection['time_type'] ||= 'string'
55
+ end
85
56
  end
86
57
  end
87
58
 
88
- @parser = setup_parser(conf)
89
- end
59
+ super
90
60
 
91
- def setup_parser(conf)
92
- case @format
93
- when 'tsv'
94
- if @keys.empty?
95
- raise Fluent::ConfigError, "keys option is required on exec input for tsv format"
96
- end
97
- Fluent::ExecUtil::TSVParser.new(@keys, method(:on_message))
98
- when 'json'
99
- Fluent::ExecUtil::JSONParser.new(method(:on_message))
100
- when 'msgpack'
101
- Fluent::ExecUtil::MessagePackParser.new(method(:on_message))
102
- else
103
- Fluent::ExecUtil::TextParserWrapperParser.new(conf, method(:on_message))
61
+ if !@tag && (!@extract_config || !@extract_config.tag_key)
62
+ raise Fluent::ConfigError, "'tag' or 'tag_key' option is required on exec input"
104
63
  end
64
+ @parser = parser_create
105
65
  end
106
66
 
107
67
  def start
108
68
  super
109
69
 
110
70
  if @run_interval
111
- child_process_execute(:exec_input, @command, interval: @run_interval, mode: [:read]) do |io|
112
- run(io)
113
- end
71
+ child_process_execute(:exec_input, @command, interval: @run_interval, mode: [:read], &method(:run))
114
72
  else
115
- child_process_execute(:exec_input, @command, immediate: true, mode: [:read]) do |io|
116
- run(io)
117
- end
73
+ child_process_execute(:exec_input, @command, immediate: true, mode: [:read], &method(:run))
118
74
  end
119
75
  end
120
76
 
121
77
  def run(io)
122
- @parser.call(io)
123
- end
124
-
125
- private
126
-
127
- def on_message(record, parsed_time = nil)
128
- if val = record.delete(@tag_key)
129
- tag = val
130
- else
131
- tag = @tag
132
- end
133
-
134
- if parsed_time
135
- time = parsed_time
136
- else
137
- if val = record.delete(@time_key)
138
- time = @time_parse_proc.call(val)
139
- else
140
- time = Fluent::EventTime.now
78
+ case
79
+ when @parser.implement?(:parse_io)
80
+ @parser.parse_io(io, &method(:on_record))
81
+ when @parser.implement?(:parse_partial_data)
82
+ until io.eof?
83
+ @parser.parse_partial_data(io.readpartial(@read_block_size), &method(:on_record))
84
+ end
85
+ when @parser.parser_type == :text_per_line
86
+ io.each_line do |line|
87
+ @parser.parse(line.chomp, &method(:on_record))
141
88
  end
89
+ else
90
+ @parser.parse(io.read, &method(:on_record))
142
91
  end
92
+ end
143
93
 
94
+ def on_record(time, record)
95
+ tag = extract_tag_from_record(record)
96
+ tag ||= @tag
97
+ time ||= extract_time_from_record(record) || Fluent::EventTime.now
144
98
  router.emit(tag, time, record)
145
99
  rescue => e
146
- log.error "exec failed to emit", error: e, tag: tag, record: Yajl.dump(record)
100
+ log.error "exec failed to emit", tag: tag, record: Yajl.dump(record), error: e
101
+ router.emit_error_event(tag, time, record, e) if tag && time && record
147
102
  end
148
103
  end
149
104
  end