fluent-plugin-norikra 0.0.9 → 0.1.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.
@@ -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