fluent-plugin-mutate_filter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 289f196651f835bc0e7ea5dab50346802c25c1ed
4
+ data.tar.gz: 3dda295dc953b5ff46804564e689618f29c75d59
5
+ SHA512:
6
+ metadata.gz: ffa1c0787fc79a87f7d39cff9af9064b0a7d4c35d5c9b21057ee54147da95169c00f8231d7399a41866ffde2db5b60979627880e9e897cc94a9b7cfb25accfdb
7
+ data.tar.gz: 9f4231a628362dbb30d6c60a3d639bd5141ddb69c6f744965dad045b5e5c87109090d90c1965f6f98d784c29cfe391862e70671def104d141bcb4d839360dc8e
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ v0.1.0
4
+ -------------
5
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-mutate_filter.gemspec
4
+ gemspec
@@ -0,0 +1,33 @@
1
+ # Fluent::MutateFilter
2
+
3
+ This gem provides the `mutate` filter for Fluentd which is designed to replicate the way `mutate` works in Logstash as much as possible.
4
+
5
+ To be honest, this is a translation of [logstash-filter-mutate](https://github.com/logstash-plugins/logstash-filter-mutate) bordering on copy-paste.
6
+
7
+ ## Requirements
8
+
9
+ * Fluentd v0.12+
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ gem install fluent-plugin-mutate_filter
15
+ ```
16
+
17
+ ## Configuration Options
18
+
19
+ All of the documentation and potential options are documented in the config_params section of the filter module. Below, only a subset of the options are displayed.
20
+
21
+ ```
22
+ <filter *>
23
+ @type mutate
24
+ rename {
25
+ "old_field_name": "new_field_name",
26
+ "old_nest.field_name": "new_nest.field_name"
27
+ }
28
+ replace {
29
+ "new_nest.field_name": "%{old_nest.field_name}"
30
+ }
31
+ </filter>
32
+ ```
33
+
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fluent/plugin/mutate_filter"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "fluent-plugin-mutate_filter"
4
+ spec.version = "0.1.0"
5
+ spec.authors = ["Jonathan Serafini"]
6
+ spec.email = ["jonathan@serafini.ca"]
7
+ spec.summary = %q{A mutate filter for Fluent which functions like Logstash.}
8
+ spec.description = spec.description
9
+ spec.homepage = "http://fuck.off"
10
+
11
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
12
+ spec.executables = []
13
+ spec.require_paths = ["lib"]
14
+
15
+ spec.add_runtime_dependency "fluentd", [">= 0.12.0", "< 0.15.0"]
16
+ spec.add_development_dependency "bundler", "~> 1.11"
17
+ spec.add_development_dependency "rake", "~> 10.0"
18
+ end
@@ -0,0 +1,453 @@
1
+ require 'fluent/filter'
2
+ require 'fluent/plugin_mixin/mutate_event'
3
+
4
+ module Fluent
5
+ class MutateFilter < Filter
6
+ Fluent::Plugin.register_filter('mutate', self)
7
+
8
+ # Treat periods as nested field names
9
+ config_param :expand_nesting, :bool, default: true
10
+
11
+ # Remove any empty hashes or arrays
12
+ config_param :prune_empty, :bool, default: true
13
+
14
+ # Rename one or more fields
15
+ # @example
16
+ # rename {
17
+ # "timestamp": "@timestamp"
18
+ # }
19
+ config_param :rename, :hash, default: Hash.new
20
+
21
+ # Update an existing field with a new value.
22
+ # - If the field does not exist then no action will be taken.
23
+ # - If the new value contains a placeholder %{}, then the value will be
24
+ # expanded to the related event record field.
25
+ # @example
26
+ # update {
27
+ # "message": "%{hostname}: new message"
28
+ # }
29
+ config_param :update, :hash, default: Hash.new
30
+
31
+ # Remove an existing field
32
+ # @example
33
+ # remove [
34
+ # "dummy1", "placeholder1"
35
+ # ]
36
+ config_param :remove, :array, default: Array.new
37
+
38
+ # Replace a field with a new value
39
+ # - If the field does not exist, then it will be created.
40
+ # - If the new value contains a placeholder %{}, then the value will be
41
+ # expanded to the related event record field.
42
+ # @example
43
+ # replace {
44
+ # "new_message": "a new field"
45
+ # }
46
+ config_param :replace, :hash, default: Hash.new
47
+
48
+ # Convert a field's value to a different type, like turning a string to an
49
+ # integer.
50
+ # - If the field value is an array, all members will be converted.
51
+ # - If the field value is a hash, then no action will be taken.
52
+ # - Valid conversion types are integer, float, string, boolean
53
+ # @example
54
+ # convert {
55
+ # "id": "integer",
56
+ # "message": "string"
57
+ # }
58
+ config_param :convert, :hash, default: Hash.new
59
+
60
+ # Convert a string field by applying a regular expression and replacement.
61
+ # - If the field is not a string, then no action will be taken.
62
+ #
63
+ # The configuration takes an array consisting of 3 elements per field/sub.
64
+ #
65
+ # @example
66
+ # gsub [
67
+ # "fieldname", "/", "_",
68
+ # "fieldname2", "[\\?#-]", "."
69
+ # ]
70
+ config_param :gsub, :hash, default: Array.new
71
+
72
+ # Convert a string to it's uppercase equivalent
73
+ # @example
74
+ # uppercase [
75
+ # "field1", "field2"
76
+ # ]
77
+ config_param :uppercase, :array, default: Array.new
78
+
79
+ # Convert a string to it's lowercase equivalent
80
+ # @example
81
+ # lowercase [
82
+ # "field1", "field2"
83
+ # ]
84
+ config_param :lowercase, :array, default: Array.new
85
+
86
+ # Strip whitespace from field.
87
+ # @example
88
+ # strip [
89
+ # "field1"
90
+ # ]
91
+ config_param :strip, :array, default: Array.new
92
+
93
+ # Split a field to an array using a separator character
94
+ # @example
95
+ # split {
96
+ # "field1": ","
97
+ # }
98
+ config_param :split, :hash, default: Hash.new
99
+
100
+ # Join an array using a separator character
101
+ # @example
102
+ # join {
103
+ # "field1": " "
104
+ # }
105
+ config_param :join, :hash, default: Hash.new
106
+
107
+ # Merge two fields of arrays or hashes
108
+ # @example
109
+ # merge {
110
+ # "dest_field": "added_field"
111
+ # }
112
+ config_param :merge, :hash, default: Hash.new
113
+
114
+ # List of all possible mutate actions, in the order that we will apply
115
+ # them. As it stands, this is the order in which Logstash would apply them.
116
+ MUTATE_ACTIONS = %w(
117
+ rename
118
+ update
119
+ replace
120
+ convert
121
+ gsub
122
+ uppercase
123
+ lowercase
124
+ strip
125
+ remove
126
+ split
127
+ join
128
+ merge
129
+ )
130
+
131
+ # Convert valid types
132
+ VALID_CONVERSIONS = %w(string integer float boolean)
133
+
134
+ # Convert helper method prefix
135
+ CONVERT_PREFIX = "convert_".freeze
136
+
137
+ # Convert boolean regex
138
+ TRUE_REGEX = (/^(true|t|yes|y|1)$/i).freeze
139
+ FALSE_REGEX = (/^(false|f|no|n|0)$/i).freeze
140
+
141
+ # Placeholder regex
142
+ TEMPLATE_TAG_REGEXP = /%\{[^}]+\}/
143
+
144
+ # Initialize attributes and parameters
145
+ # @since 0.1.0
146
+ # @return [NilClass]
147
+ def configure(conf)
148
+ super
149
+
150
+ @convert.nil? or @convert.each do |field, type|
151
+ if !VALID_CONVERSIONS.include?(type)
152
+ raise ConfigError,
153
+ "convert #{type} is not one of #{VALID_CONVERSIONS.join(',')}."
154
+ end
155
+ end
156
+
157
+ @gsub_parsed = []
158
+ @gsub.nil? or
159
+ @gsub.each_slice(3) do |field, needle, replacement|
160
+ if [field, needle, replacement].any? {|n| n.nil?}
161
+ raise ConfigError,
162
+ "gsub #{[field,needle,replacement]} requires 3 elements."
163
+ end
164
+
165
+ @gsub_parsed << {
166
+ field: field,
167
+ needle: (needle.index("%{").nil?? Regexp.new(needle): needle),
168
+ replacement: replacement
169
+ }
170
+ end
171
+ end
172
+
173
+ # Filter action which will manipulate records
174
+ # @since 0.1.0
175
+ # @return [Hash] the modified event record
176
+ def filter(tag, time, record)
177
+ # In order to more easily navigate the record, we wrap the record in a
178
+ # delegator. We additionally pass the `expand_nesting` option which
179
+ # determines whether we should treat periods as field separators.
180
+ result = Fluent::PluginMixin::MutateEvent.
181
+ new(record, expand_nesting: @expand_nesting)
182
+
183
+ MUTATE_ACTIONS.each do |action|
184
+ begin
185
+ send(action.to_sym, result) if instance_variable_get("@#{action}")
186
+ rescue => e
187
+ log.warn "failed to mutate #{action} action", error: e
188
+ log.warn_backtrace
189
+ end
190
+ end
191
+
192
+ result.prune if @prune_empty
193
+ result.to_record
194
+ end
195
+
196
+ protected
197
+
198
+ # Expand %{} strings to the related event fields.
199
+ # @since 0.1.0
200
+ # @return [String] the modified string
201
+ def expand_references(event, string)
202
+ new_string = ''
203
+
204
+ position = 0
205
+ matches = string.scan(TEMPLATE_TAG_REGEXP).map{|m| $~}
206
+
207
+ matches.each do |match|
208
+ reference_tag = match[0][2..-2]
209
+ reference_value = event.get(reference_tag.downcase).to_s
210
+ if reference_value.nil?
211
+ @log.error "failed to replace tag", field: reference_tag.downcase
212
+ reference_value = match.to_s
213
+ end
214
+
215
+ start = match.offset(0).first
216
+ new_string << string[position..(start-1)] if start > 0
217
+ new_string << reference_value
218
+ position = match.offset(0).last
219
+ end
220
+
221
+ if position < string.size
222
+ new_string << string[position..-1]
223
+ end
224
+
225
+ new_string
226
+ end
227
+
228
+ # Remove fields from the event hash
229
+ # @since 0.1.0
230
+ def remove(event)
231
+ @remove.each do |field|
232
+ event.remove(field)
233
+ end
234
+ end
235
+
236
+ # Rename fields in the event hash
237
+ # @since 0.1.0
238
+ def rename(event)
239
+ @rename.each do |old, new|
240
+ item = event.get(old)
241
+ next if item.nil?
242
+ event.set(new, item)
243
+ event.remove(old)
244
+ end
245
+ end
246
+
247
+ # Update (existing) fields in the event hash
248
+ # @since 0.1.0
249
+ def update(event)
250
+ @update.each do |field, newvalue|
251
+ newvalue = expand_references(event, newvalue)
252
+ next unless event.include?(field)
253
+ event.set(field, newvalue)
254
+ end
255
+ end
256
+
257
+ # Replace fields in the event hash
258
+ # @since 0.1.0
259
+ def replace(event)
260
+ @replace.each do |field, newvalue|
261
+ newvalue = expand_references(event, newvalue)
262
+ event.set(field, newvalue)
263
+ end
264
+ end
265
+
266
+ # Convert fields to given types in the record hash
267
+ # @since 0.1.0
268
+ def convert(event)
269
+ @convert.each do |field, type|
270
+ converter = method(CONVERT_PREFIX + type)
271
+
272
+ case original = event.get(field)
273
+ when NilClass
274
+ next
275
+ when Hash
276
+ @log.error("cannot convert hash", field: field, value: original)
277
+ when Array
278
+ event.set(field, original.map{|v| converter.call(v)})
279
+ else
280
+ event.set(field, converter.call(original))
281
+ end
282
+ end
283
+ end
284
+
285
+ def convert_string(value)
286
+ value.to_s.force_encoding(Encoding::UTF_8)
287
+ end
288
+
289
+ def convert_integer(value)
290
+ value.to_i
291
+ end
292
+
293
+ def convert_float(value)
294
+ value.to_f
295
+ end
296
+
297
+ def convert_boolean(value)
298
+ return true if value =~ TRUE_REGEX
299
+ return false if value.empty? || value =~ FALSE_REGEX
300
+ @log.error("failed to convert to boolean", value: value)
301
+ end
302
+
303
+ # Convert field values to uppercase in the record hash
304
+ # @since 0.1.0
305
+ def uppercase(event)
306
+ @uppercase.each do |field|
307
+ original = event.get(field)
308
+ result = case original
309
+ when Array
310
+ original.map do |elem|
311
+ (elem.is_a?(String) ? elemen.upcase : elem)
312
+ end
313
+ when String
314
+ original.upcase! || original
315
+ else
316
+ @log.error("can't uppercase field",
317
+ field: field,
318
+ value: original)
319
+ original
320
+ end
321
+ event.set(field, result)
322
+ end
323
+ end
324
+
325
+ # Convert field values to lowercase in the record hash
326
+ # @since 0.1.0
327
+ def lowercase(event)
328
+ @lowercase.each do |field|
329
+ original = event.get(field)
330
+ result = case original
331
+ when Array
332
+ original.map do |elem|
333
+ (elem.is_a?(String) ? elemen.downcase : elem)
334
+ end
335
+ when String
336
+ original.downcase! || original
337
+ else
338
+ @log.error("can't lowercase field",
339
+ field: field,
340
+ value: original)
341
+ original
342
+ end
343
+ event.set(field, result)
344
+ end
345
+ end
346
+
347
+ # Split fields based on delimiters in the record hash
348
+ # @since 0.1.0
349
+ def split(event)
350
+ @split.each do |field, separator|
351
+ value = event.get(field)
352
+ if value.is_a?(String)
353
+ event.set(field, value.split(separator))
354
+ else
355
+ @loger.error("can't split field",
356
+ field: field,
357
+ value: value)
358
+ end
359
+ end
360
+ end
361
+
362
+ # Join fields based on delimiters in the record hash
363
+ # @since 0.1.0
364
+ def join(event)
365
+ @join.each do |field, separator|
366
+ value = event.get(field)
367
+ if value.is_a?(Array)
368
+ event.set(field, value.join(separator))
369
+ end
370
+ end
371
+ end
372
+
373
+ # Strip whitespace surrounding fields in the record hash
374
+ # @since 0.1.0
375
+ def strip(event)
376
+ @strip.each do |field|
377
+ value = event.get(field)
378
+ case value
379
+ when Array
380
+ event.set(field, value.map{|s| s.strip})
381
+ when String
382
+ event.set(field, value.strip)
383
+ end
384
+ end
385
+ end
386
+
387
+ # Merge hashes and arrays in the record hash
388
+ # @since 0.1.0
389
+ def merge(event)
390
+ @merge.each do |dest_field, added_fields|
391
+ dest_field_value = event.get(dest_field)
392
+
393
+ Array(added_fields).each do |added_field|
394
+ added_field_value = event.get(added_field)
395
+
396
+ if dest_field_value.is_a?(Hash) ^ added_field_value.is_a?(Hash)
397
+ @log.error('cannot merge an array and hash',
398
+ dest_field: dest_field,
399
+ added_field: added_field)
400
+ next
401
+ end
402
+
403
+ if dest_field_value.is_a?(Hash)
404
+ event.set(dest_field, dest_field_value.update(added_field_value))
405
+ else
406
+ event.set(dest_field, Array(dest_field_value).
407
+ concat(Array(added_field_value)))
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ # Perform regular expression substitutions in the record hahs
414
+ # @since 0.1.0
415
+ def gsub(event)
416
+ @gsub_parsed.each do |config|
417
+ field = config[:field]
418
+ needle = config[:needle]
419
+ replacement = config[:replacement]
420
+
421
+ value = event.get(field)
422
+ case value
423
+ when Array
424
+ result = value.map do |v|
425
+ if v.is_a?(String)
426
+ gsub_dynamic_fields(event, v, needle, replacement)
427
+ else
428
+ @log.error('cannot gsub non Strings',
429
+ field: field,
430
+ value: v)
431
+ end
432
+ event.set(field, result)
433
+ end
434
+ when String
435
+ v = gsub_dynamic_fields(event, value, needle, replacement)
436
+ event.set(field, v)
437
+ else
438
+ @log.error('cannot gsub non Strings', field: field, value: value)
439
+ end
440
+ end
441
+ end
442
+
443
+ def gsub_dynamic_fields(event, original, needle, replacement)
444
+ replacement = expand_references(event, replacement)
445
+ if needle.is_a?(Regexp)
446
+ original.gsub(needle, replacement)
447
+ else
448
+ original.gsub(Regexp.new(needle), replacement)
449
+ end
450
+ end
451
+ end
452
+ end
453
+
@@ -0,0 +1,78 @@
1
+ module Fluent
2
+ module PluginMixin
3
+ class MutateEvent < SimpleDelegator
4
+ def initialize(record, expand_nesting = true)
5
+ super(record)
6
+ @record = record
7
+ @expand_nesting = expand_nesting
8
+ end
9
+
10
+ def to_record
11
+ @record
12
+ end
13
+
14
+ def dig(*keys)
15
+ item = @record
16
+
17
+ keys.each do |key|
18
+ break if item.nil?
19
+ item = item.is_a?(Hash) ? item[key] : nil
20
+ end
21
+
22
+ item
23
+ end
24
+
25
+ def prune
26
+ delete_proc = proc do |_,v|
27
+ v.delete_if(&delete_proc) if v.respond_to?(:delete_if)
28
+ v.nil? || v.respond_to?(:empty?) && v.empty?
29
+ end
30
+
31
+ @record.delete_if(&delete_proc)
32
+ end
33
+
34
+ def get(key_or_path, &block)
35
+ item = dig(*expand_key(key_or_path))
36
+ block_given? ? yield(item) : item
37
+ end
38
+
39
+ def parent(key_or_path, &block)
40
+ path = expand_key(key_or_path)
41
+ child = path.pop
42
+
43
+ item = dig(*path)
44
+ block_given? ? yield(item, child) : item
45
+ end
46
+
47
+ def set(key_or_path, value)
48
+ path = expand_key(key_or_path)
49
+ child = path.pop
50
+
51
+ item = @record
52
+ path.each do |key|
53
+ item = item[key] ||= {}
54
+ end
55
+ item[child] = value
56
+ end
57
+
58
+ def remove(key_or_path)
59
+ path = expand_key(key_or_path)
60
+ child = path.pop
61
+
62
+ parent(key_or_path) do |item, child|
63
+ item.delete(child) unless item.nil?
64
+ end
65
+ end
66
+
67
+ def include?(key_or_path)
68
+ !get(key_or_path).nil?
69
+ end
70
+
71
+ protected
72
+
73
+ def expand_key(key_or_path)
74
+ @expand_nesting ? key_or_path.split(".") : [key_or_path]
75
+ end
76
+ end
77
+ end
78
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-mutate_filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Serafini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 0.15.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.12.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.15.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.11'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.11'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ description: ''
62
+ email:
63
+ - jonathan@serafini.ca
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".gitignore"
69
+ - CHANGELOG.md
70
+ - Gemfile
71
+ - README.md
72
+ - Rakefile
73
+ - bin/console
74
+ - bin/setup
75
+ - fluent-plugin-mutate_filter.gemspec
76
+ - lib/fluent/plugin/filter_mutate.rb
77
+ - lib/fluent/plugin_mixin/mutate_event.rb
78
+ homepage: http://fuck.off
79
+ licenses: []
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.5.2
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: A mutate filter for Fluent which functions like Logstash.
101
+ test_files: []
102
+ has_rdoc: