fluent-plugin-mutate_filter 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,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: