fluent-plugin-norikra 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ module Fluent::NorikraPlugin
2
+ class ConfigSection
3
+ attr_accessor :target, :target_matcher, :auto_field, :filter_params, :field_definitions, :query_generators
4
+
5
+ def initialize(section, enable_auto_query=true)
6
+ @target = nil
7
+ @target_matcher = nil
8
+ if section.name == 'default'
9
+ # nil
10
+ elsif section.name == 'target'
11
+ # unescaped target name (tag style with dots)
12
+ @target = section.arg
13
+ @target_matcher = Fluent::GlobMatchPattern.new(section.arg)
14
+ else
15
+ raise ArgumentError, "invalid section for this class, #{section.name}: ConfigSection"
16
+ end
17
+
18
+ @auto_field = Fluent::Config.bool_value(section['auto_field'])
19
+
20
+ @filter_params = {
21
+ :include => section['include'],
22
+ :include_regexp => section['include_regexp'],
23
+ :exclude => section['exclude'],
24
+ :exclude_regexp => section['exclude_regexp']
25
+ }
26
+ @field_definitions = {
27
+ :string => (section['field_string'] || '').split(','),
28
+ :boolean => (section['field_boolean'] || '').split(','),
29
+ :integer => (section['field_integer'] || '').split(','),
30
+ :float => (section['field_float'] || '').split(','),
31
+ }
32
+
33
+ @query_generators = []
34
+ section.elements.each do |element|
35
+ if element.name == 'query' && enable_auto_query
36
+ opt = {}
37
+ if element.has_key?('fetch_interval')
38
+ opt['fetch_interval'] = Fluent::Config.time_value(element['fetch_interval'])
39
+ end
40
+ @query_generators.push(QueryGenerator.new(element['name'], element['group'], element['expression'], element['tag'], opt))
41
+ end
42
+ end
43
+ end
44
+
45
+ def +(other)
46
+ if other.nil?
47
+ other = self.class.new(Fluent::Config::Element.new('target', 'dummy', {}, []))
48
+ end
49
+ r = self.class.new(Fluent::Config::Element.new('target', (other.target ? other.target : self.target), {}, []))
50
+ r.auto_field = (other.auto_field.nil? ? self.auto_field : other.auto_field)
51
+
52
+ others_filter = {}
53
+ other.filter_params.keys.each do |k|
54
+ others_filter[k] = other.filter_params[k] if other.filter_params[k]
55
+ end
56
+ r.filter_params = self.filter_params.merge(others_filter)
57
+ r.field_definitions = {
58
+ :string => self.field_definitions[:string] + other.field_definitions[:string],
59
+ :boolean => self.field_definitions[:boolean] + other.field_definitions[:boolean],
60
+ :integer => self.field_definitions[:integer] + other.field_definitions[:integer],
61
+ :float => self.field_definitions[:float] + other.field_definitions[:float],
62
+ }
63
+ r.query_generators = self.query_generators + other.query_generators
64
+ r
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ module Fluent::NorikraPlugin
2
+ class FetchRequest
3
+ METHODS = [:event, :sweep]
4
+ TAG_TYPES = ['query_name', 'field', 'string']
5
+
6
+ attr_accessor :method, :target, :interval, :tag_generator, :tag_prefix
7
+ attr_accessor :time
8
+
9
+ def initialize(method, target, interval, tag_type, tag_arg, tag_prefix)
10
+ raise ArgumentError, "unknown method '#{method}'" unless METHODS.include?(method.to_sym)
11
+
12
+ @method = method.to_sym
13
+ @target = target
14
+ @interval = interval.to_i
15
+
16
+ raise ArgumentError, "unknown tag type specifier '#{tag_type}'" unless TAG_TYPES.include?(tag_type.to_s)
17
+ raw_tag_prefix = tag_prefix.to_s
18
+ if (! raw_tag_prefix.empty?) && (! raw_tag_prefix.end_with?('.')) # tag_prefix specified, and ends without dot
19
+ raw_tag_prefix += '.'
20
+ end
21
+
22
+ @tag_generator = case tag_type.to_s
23
+ when 'query_name' then lambda{|query_name,record| raw_tag_prefix + query_name}
24
+ when 'field' then lambda{|query_name,record| raw_tag_prefix + record[tag_arg]}
25
+ when 'string' then lambda{|query_name,record| raw_tag_prefix + tag_arg}
26
+ else
27
+ raise "bug"
28
+ end
29
+ @time = Time.now + 1 # should be fetched soon ( 1sec later )
30
+ end
31
+
32
+ def <=>(other)
33
+ self.time <=> other.time
34
+ end
35
+
36
+ def next!
37
+ @time = Time.now + @interval
38
+ end
39
+
40
+ # returns hash: { tag => [[time, record], ...], ... }
41
+ def fetch(client)
42
+ # events { query_name => [[time, record], ...], ... }
43
+ events = case @method
44
+ when :event then event(client)
45
+ when :sweep then sweep(client)
46
+ else
47
+ raise "BUG: unknown method: #{@method}"
48
+ end
49
+
50
+ output = {}
51
+
52
+ events.keys.each do |query_name|
53
+ events[query_name].each do |time, record|
54
+ tag = @tag_generator.call(query_name, record)
55
+ output[tag] ||= []
56
+ output[tag] << [time, record]
57
+ end
58
+ end
59
+
60
+ output
61
+ end
62
+
63
+ def event(client)
64
+ events = client.event(@target) # [[time(int from epoch), event], ...]
65
+ {@target => events}
66
+ end
67
+
68
+ def sweep(client)
69
+ client.sweep(@target) # {query_name => event_array, ...}
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,109 @@
1
+ require_relative 'fetch_request'
2
+
3
+ module Fluent::NorikraPlugin
4
+ module InputMixin
5
+ # <fetch>
6
+ # method event
7
+ # target QUERY_NAME
8
+ # interval 5s
9
+ # tag query_name
10
+ # # tag field FIELDNAME
11
+ # # tag string FIXED_STRING
12
+ # tag_prefix norikra.event # actual tag: norikra.event.QUERYNAME
13
+ # </fetch>
14
+ # <fetch>
15
+ # method sweep
16
+ # target QUERY_GROUP # or unspecified => default
17
+ # interval 60s
18
+ # tag field group_by_key
19
+ # tag_prefix norikra.query
20
+ # </fetch>
21
+
22
+ def setup_input(conf)
23
+ @fetch_queue = []
24
+
25
+ conf.elements.each do |e|
26
+ next unless e.name == 'fetch'
27
+ method = e['method']
28
+ target = e['target']
29
+ interval_str = e['interval']
30
+ tag = e['tag']
31
+ unless method && interval_str && tag
32
+ raise Fluent::ConfigError, "<fetch> must be specified with method/interval/tag"
33
+ end
34
+ if method == 'event' and target.nil?
35
+ raise Fluent::ConfigError, "<fetch> method 'event' requires 'target' for fetch target query name"
36
+ end
37
+
38
+ interval = Fluent::Config.time_value(interval_str)
39
+ tag_type, tag_arg = tag.split(/ /, 2)
40
+ req = FetchRequest.new(method, target, interval, tag_type, tag_arg, e['tag_prefix'])
41
+
42
+ @fetch_queue << req
43
+ end
44
+
45
+ @fetch_queue_mutex = Mutex.new
46
+ end
47
+
48
+ def start_input
49
+ @fetch_worker_running = true
50
+ @fetch_thread = Thread.new(&method(:fetch_worker))
51
+ end
52
+
53
+ def stop_input
54
+ @fetch_worker_running = false
55
+ end
56
+
57
+ def shutdown_input
58
+ # @fetch_thread.kill
59
+ @fetch_thread.join
60
+ end
61
+
62
+ def insert_fetch_queue(request)
63
+ @fetch_queue_mutex.synchronize do
64
+ request.next! if request.time < Time.now
65
+ # if @fetch_queue.size > 0
66
+ # next_pos = @fetch_queue.bsearch{|req| req.time > request.time}
67
+ # @fetch_queue.insert(next_pos, request)
68
+ # else
69
+ # @fetch_queue.push(request)
70
+ # end
71
+ @fetch_queue.push(request)
72
+ @fetch_queue.sort!
73
+ end
74
+ rescue => e
75
+ $log.error "unknown log encountered", :error_class => e.class, :message => e.message
76
+ end
77
+
78
+ def fetch_worker
79
+ while sleep(1)
80
+ break unless @fetch_worker_running
81
+ next unless fetchable?
82
+ next if @fetch_queue.first.nil? || @fetch_queue.first.time > Time.now
83
+
84
+ now = Time.now
85
+ while @fetch_queue.first.time <= now
86
+ req = @fetch_queue.shift
87
+
88
+ begin
89
+ data = req.fetch(client())
90
+ rescue => e
91
+ $log.error "failed to fetch", :norikra => "#{@host}:#{@port}", :method => req.method, :target => req.target, :error => e.class, :message => e.message
92
+ end
93
+
94
+ data.each do |tag, event_array|
95
+ event_array.each do |time,event|
96
+ begin
97
+ Fluent::Engine.emit(tag, time, event)
98
+ rescue => e
99
+ $log.error "failed to emit event from norikra query", :norikra => "#{@host}:#{@port}", :error => e.class, :message => e.message, :tag => tag, :record => event
100
+ end
101
+ end
102
+ end
103
+
104
+ insert_fetch_queue(req)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,213 @@
1
+ require_relative 'config_section'
2
+ require_relative 'query'
3
+ require_relative 'query_generator'
4
+ require_relative 'record_filter'
5
+ require_relative 'target'
6
+
7
+ require_relative 'fetch_request'
8
+
9
+ module Fluent::NorikraPlugin
10
+ module OutputMixin
11
+ def setup_output(conf, enable_auto_query)
12
+ @enable_auto_query = enable_auto_query
13
+
14
+ @target_generator = case
15
+ when @target_string
16
+ lambda {|tag,record| @target_string}
17
+ when @target_map_key
18
+ lambda {|tag,record| record[@target_map_key]}
19
+ when @target_map_tag
20
+ lambda {|tag,record| tag.gsub(/^#{@remove_tag_prefix}(\.)?/, '')}
21
+ else
22
+ raise Fluent::ConfigError, "no one way specified to decide target"
23
+ end
24
+
25
+ # target map already prepared (opened, and related queries registered)
26
+ @target_map = {} # 'target' => instance of Fluent::NorikraPlugin::Target
27
+
28
+ # for conversion from query_name to tag
29
+ @query_map = {} # 'query_name' => instance of Fluent::NorikraPlugin::Query
30
+
31
+ @default_target = ConfigSection.new(Fluent::Config::Element.new('default', nil, {}, []), @enable_auto_query)
32
+ @config_targets = {}
33
+
34
+ conf.elements.each do |element|
35
+ case element.name
36
+ when 'default'
37
+ @default_target = ConfigSection.new(element, @enable_auto_query)
38
+ when 'target'
39
+ c = ConfigSection.new(element, @enable_auto_query)
40
+ @config_targets[c.target] = c
41
+ end
42
+ end
43
+
44
+ @target_mutex = Mutex.new
45
+ end
46
+
47
+ def start_output
48
+ @register_worker_running = true
49
+ @register_queue = []
50
+ @registered_targets = {}
51
+ @register_thread = Thread.new(&method(:register_worker))
52
+ end
53
+
54
+ def stop_output
55
+ @register_worker_running = false
56
+ end
57
+
58
+ def shutdown_output
59
+ # @register_thread.kill
60
+ @register_thread.join
61
+ end
62
+
63
+ def prepared?(target_names)
64
+ fetchable? && target_names.reduce(true){|r,t| r && @target_map.values.any?{|target| target.escaped_name == t}}
65
+ end
66
+
67
+ def fetch_event_registration(query)
68
+ return if query.tag.nil? || query.tag.empty?
69
+ req = FetchRequest.new(:event, query.name, query.interval, 'string', query.tag, nil)
70
+ insert_fetch_queue(req)
71
+ end
72
+
73
+ def register_worker
74
+ while sleep(0.25)
75
+ break unless @register_worker_running
76
+ next unless fetchable?
77
+
78
+ c = client()
79
+
80
+ targets = @register_queue.shift(10)
81
+ targets.each do |t|
82
+ next if @target_map[t.name]
83
+
84
+ $log.debug "Preparing norikra target #{t.name} on #{@host}:#{@port}"
85
+ if prepare_target(c, t)
86
+ $log.debug "success to prepare target #{t.name} on #{@host}:#{@port}"
87
+
88
+ if @enable_auto_query
89
+ raise "bug" unless self.respond_to?(:insert_fetch_queue)
90
+
91
+ t.queries.each do |query|
92
+ @query_map[query.name] = query
93
+ fetch_event_registration(query)
94
+ end
95
+ end
96
+ @target_map[t.name] = t
97
+ @registered_targets.delete(t.name)
98
+ else
99
+ $log.error "Failed to prepare norikra data for target:#{t.name}"
100
+ @norikra_started.push(t)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def prepare_target(client, target)
107
+ # target open and reserve fields
108
+ $log.debug "Going to prepare about target"
109
+ begin
110
+ unless client.targets.include?(target.escaped_name)
111
+ $log.debug "opening target #{target.escaped_name}"
112
+ client.open(target.escaped_name, target.reserve_fields, target.auto_field)
113
+ $log.debug "opening target #{target.escaped_name}, done."
114
+ end
115
+
116
+ reserving = target.reserve_fields
117
+ reserved = []
118
+ client.fields(target.escaped_name).each do |field|
119
+ if reserving[field['name']]
120
+ reserved.push(field['name'])
121
+ if reserving[field['name']] != field['type']
122
+ $log.warn "field type mismatch, reserving:#{reserving[field['name']]} but reserved:#{field['type']}"
123
+ end
124
+ end
125
+ end
126
+
127
+ reserving.each do |fieldname,type|
128
+ client.reserve(target.escaped_name, fieldname, type) unless reserved.include?(fieldname)
129
+ end
130
+ rescue => e
131
+ $log.error "failed to prepare target:#{target.escaped_name}", :norikra => "#{@host}:#{@port}", :error => e.class, :message => e.message
132
+ return false
133
+ end
134
+
135
+ # query registration
136
+ begin
137
+ registered = Hash[client.queries.map{|q| [q['name'], q['expression']]}]
138
+ target.queries.each do |query|
139
+ if registered.has_key?(query.name) # query already registered
140
+ if registered[query.name] != query.expression
141
+ $log.warn "query name and expression mismatch, check norikra server status. target query name:#{query.name}"
142
+ end
143
+ next
144
+ end
145
+ client.register(query.name, query.group, query.expression)
146
+
147
+ @query_map[query.name] = query
148
+ fetch_event_registration(query)
149
+ end
150
+ rescue => e
151
+ $log.warn "failed to register query", :norikra => "#{@host}:#{@port}", :error => e.class, :message => e.message
152
+ end
153
+ end
154
+
155
+ def format_stream(tag, es)
156
+ tobe_registered_target_names = []
157
+
158
+ out = ''
159
+
160
+ es.each do |time,record|
161
+ target = @target_generator.call(tag, record)
162
+
163
+ tgt = @target_mutex.synchronize do
164
+ t = @target_map[target]
165
+ unless t
166
+ unless tobe_registered_target_names.include?(target)
167
+ conf = @config_targets[target]
168
+ unless conf
169
+ @config_targets.values.each do |c|
170
+ if c.target_matcher.match(target)
171
+ conf = c
172
+ break
173
+ end
174
+ end
175
+ end
176
+ t = Target.new(target, @default_target + conf)
177
+ @registered_targets[target] = t
178
+ @register_queue.push(t)
179
+ tobe_registered_target_names.push(target)
180
+ end
181
+ t = @registered_targets[target]
182
+ end
183
+ t
184
+ end
185
+
186
+ event = tgt.filter(record)
187
+
188
+ out << [tgt.escaped_name,event].to_msgpack
189
+ end
190
+
191
+ out
192
+ end
193
+
194
+ def write(chunk)
195
+ events_map = {} # target => [event]
196
+ chunk.msgpack_each do |target, event|
197
+ events_map[target] ||= []
198
+ events_map[target].push(event)
199
+ end
200
+
201
+ unless prepared?(events_map.keys)
202
+ raise RuntimeError, "norikra server is not ready for this targets: #{events_map.keys.join(',')}"
203
+ end
204
+
205
+ c = client()
206
+
207
+ events_map.each do |target, events|
208
+ c.send(target, events)
209
+ end
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,13 @@
1
+ module Fluent::NorikraPlugin
2
+ class Query
3
+ attr_accessor :name, :group, :expression, :tag, :interval
4
+
5
+ def initialize(name, group, expression, tag, interval)
6
+ @name = name
7
+ @group = group
8
+ @expression = expression
9
+ @tag = tag
10
+ @interval = interval
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ module Fluent::NorikraPlugin
2
+ class QueryGenerator
3
+ attr_reader :fetch_interval
4
+
5
+ def initialize(name_template, group, expression_template, tag_template, opts={})
6
+ @name_template = name_template || ''
7
+ @group = group
8
+ @expression_template = expression_template || ''
9
+ @tag_template = tag_template || ''
10
+ if @name_template.empty? || @expression_template.empty?
11
+ raise Fluent::ConfigError, "query's name/expression must be specified"
12
+ end
13
+ @fetch_interval = case
14
+ when opts['fetch_interval']
15
+ Fluent::Config.time_value(opts['fetch_interval'])
16
+ when @expression_template =~ /\.win:time_batch\(([^\)]+)\)/
17
+ y,mon,w,d,h,m,s,msec = self.class.parse_time_period($1)
18
+ (h * 3600 + m * 60 + s) / 5
19
+ else
20
+ 60
21
+ end
22
+ end
23
+
24
+ def generate(name, escaped)
25
+ Fluent::NorikraPlugin::Query.new(
26
+ self.class.replace_target(name, @name_template),
27
+ @group,
28
+ self.class.replace_target(escaped, @expression_template),
29
+ self.class.replace_target(name, @tag_template),
30
+ @fetch_interval
31
+ )
32
+ end
33
+
34
+ def self.replace_target(t, str)
35
+ str.gsub('${target}', t)
36
+ end
37
+
38
+ def self.parse_time_period(string)
39
+ #### http://esper.codehaus.org/esper-4.9.0/doc/reference/en-US/html/epl_clauses.html#epl-syntax-time-periods
40
+ # time-period : [year-part] [month-part] [week-part] [day-part] [hour-part] [minute-part] [seconds-part] [milliseconds-part]
41
+ # year-part : (number|variable_name) ("years" | "year")
42
+ # month-part : (number|variable_name) ("months" | "month")
43
+ # week-part : (number|variable_name) ("weeks" | "week")
44
+ # day-part : (number|variable_name) ("days" | "day")
45
+ # hour-part : (number|variable_name) ("hours" | "hour")
46
+ # minute-part : (number|variable_name) ("minutes" | "minute" | "min")
47
+ # seconds-part : (number|variable_name) ("seconds" | "second" | "sec")
48
+ # milliseconds-part : (number|variable_name) ("milliseconds" | "millisecond" | "msec")
49
+ m = /^\s*(\d+ years?)? ?(\d+ months?)? ?(\d+ weeks?)? ?(\d+ days?)? ?(\d+ hours?)? ?(\d+ (?:min|minute|minutes))? ?(\d+ (?:sec|second|seconds))? ?(\d+ (?:msec|millisecond|milliseconds))?/.match(string)
50
+ years = (m[1] || '').split(' ',2).first.to_i
51
+ months = (m[2] || '').split(' ',2).first.to_i
52
+ weeks = (m[3] || '').split(' ',2).first.to_i
53
+ days = (m[4] || '').split(' ',2).first.to_i
54
+ hours = (m[5] || '').split(' ',2).first.to_i
55
+ minutes = (m[6] || '').split(' ',2).first.to_i
56
+ seconds = (m[7] || '').split(' ',2).first.to_i
57
+ msecs = (m[8] || '').split(' ',2).first.to_i
58
+ return [years, months, weeks, days, hours, minutes, seconds, msecs]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ module Fluent::NorikraPlugin
2
+ class RecordFilter
3
+ attr_reader :default_policy, :include_fields, :include_regexp, :exclude_fields, :exclude_regexp
4
+
5
+ def initialize(include='', include_regexp='', exclude='', exclude_regexp='')
6
+ include ||= ''
7
+ include_regexp ||= ''
8
+ exclude ||= ''
9
+ exclude_regexp ||= ''
10
+
11
+ @default_policy = nil
12
+ if include == '*' && exclude == '*'
13
+ raise Fluent::ConfigError, "invalid configuration, both of 'include' and 'exclude' are '*'"
14
+ end
15
+ if include.empty? && include_regexp.empty? && exclude.empty? && exclude_regexp.empty? # assuming "include *"
16
+ @default_policy = :include
17
+ elsif exclude.empty? && exclude_regexp.empty? || exclude == '*' # assuming "exclude *"
18
+ @default_policy = :exclude
19
+ elsif include.empty? && include_regexp.empty? || include == '*' # assuming "include *"
20
+ @default_policy = :include
21
+ else
22
+ raise Fluent::ConfigError, "unknown default policy. specify 'include *' or 'exclude *'"
23
+ end
24
+
25
+ @include_fields = nil
26
+ @include_regexp = nil
27
+ @exclude_fields = nil
28
+ @exclude_regexp = nil
29
+
30
+ if @default_policy == :exclude
31
+ @include_fields = include.split(',')
32
+ @include_regexp = Regexp.new(include_regexp) unless include_regexp.empty?
33
+ if @include_fields.empty? && @include_regexp.nil?
34
+ raise Fluent::ConfigError, "no one fields specified. specify 'include' or 'include_regexp'"
35
+ end
36
+ else
37
+ @exclude_fields = exclude.split(',')
38
+ @exclude_regexp = Regexp.new(exclude_regexp) unless exclude_regexp.empty?
39
+ end
40
+ end
41
+
42
+ def filter(record)
43
+ if @default_policy == :include
44
+ if @exclude_fields.empty? && @exclude_regexp.nil?
45
+ record
46
+ else
47
+ record = record.dup
48
+ record.keys.each do |f|
49
+ record.delete(f) if @exclude_fields.include?(f) || @exclude_regexp && @exclude_regexp.match(f)
50
+ end
51
+ record
52
+ end
53
+ else # default policy exclude
54
+ data = {}
55
+ record.keys.each do |f|
56
+ data[f] = record[f] if @include_fields.include?(f) || @include_regexp && @include_regexp.match(f)
57
+ end
58
+ data
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,47 @@
1
+ module Fluent::NorikraPlugin
2
+ class Target
3
+ attr_accessor :name, :auto_field, :fields, :queries
4
+ attr_reader :escaped_name
5
+
6
+ def self.escape(src)
7
+ if src.nil? || src.empty?
8
+ return 'FluentdGenerated'
9
+ end
10
+
11
+ dst = src.gsub(/[^_a-zA-Z0-9]/, '_')
12
+ unless dst =~ /^[a-zA-Z]([_a-zA-Z0-9]*[a-zA-Z0-9])?$/
13
+ unless dst =~ /^[a-zA-Z]/
14
+ dst = 'Fluentd' + dst
15
+ end
16
+ unless dst =~ /[a-zA-Z0-9]$/
17
+ dst = dst + 'Generated'
18
+ end
19
+ end
20
+ dst
21
+ end
22
+
23
+ def initialize(target, config)
24
+ @name = target
25
+ @escaped_name = self.class.escape(@name)
26
+ @auto_field = config.auto_field.nil? ? true : config.auto_field
27
+
28
+ @filter = RecordFilter.new(*([:include, :include_regexp, :exclude, :exclude_regexp].map{|s| config.filter_params[s]}))
29
+ @fields = config.field_definitions
30
+ @queries = config.query_generators.map{|g| g.generate(@name, @escaped_name)}
31
+ end
32
+
33
+ def filter(record)
34
+ @filter.filter(record)
35
+ end
36
+
37
+ def reserve_fields
38
+ f = {}
39
+ @fields.keys.each do |type_sym|
40
+ @fields[type_sym].each do |fieldname|
41
+ f[fieldname] = type_sym.to_s
42
+ end
43
+ end
44
+ f
45
+ end
46
+ end
47
+ end