logstash-filter-mutate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MmZhMDNmZDM5MGVmMGEwNTM4N2ZlMTlmZmIyNmZkODU5MmRiNzliMA==
5
+ data.tar.gz: !binary |-
6
+ MjE0Mzc3M2QyOTczN2Q3NDIyM2EyZGUwMmEzZTM5M2FjOWZhNjc3Mw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NGVlZWVmNjcwNWI0NTA2YWEzOGZiZmJmYjUzNGU0ODQ3NjFhMTFlZjBlZGNm
10
+ MTE4NjAxYzU2YTUwY2JjMGE2NmNmZjUzNWU5ZmI0MWQzMjM3ZGMyYWZmMGI3
11
+ YTU5NTAwNjcyOGNjZjE1ZDI2OTMxY2ExOTA3MzRjMzNlZTUyOWE=
12
+ data.tar.gz: !binary |-
13
+ M2JiODBlMzk1YTdhYzJjNGZmOGZhYzllNTQxZTNlNWIwYzNhYzE2YmZmMDA1
14
+ ZDJmYjk2ZWU5M2JiNzA0OWQyOGE0OTBjNGRiMDdjMTM2NGRlMTUyNzY4Zjkz
15
+ YzNlOTJjZTlkYzYxZDZlN2RmZTFjZDIxMzYwNzYxNDg3ODQ0M2I=
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,413 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+
5
+ # The mutate filter allows you to perform general mutations on fields. You
6
+ # can rename, remove, replace, and modify fields in your events.
7
+ #
8
+ # TODO(sissel): Support regexp replacements like String#gsub ?
9
+ class LogStash::Filters::Mutate < LogStash::Filters::Base
10
+ config_name "mutate"
11
+ milestone 3
12
+
13
+ # Rename one or more fields.
14
+ #
15
+ # Example:
16
+ #
17
+ # filter {
18
+ # mutate {
19
+ # # Renames the 'HOSTORIP' field to 'client_ip'
20
+ # rename => { "HOSTORIP" => "client_ip" }
21
+ # }
22
+ # }
23
+ config :rename, :validate => :hash
24
+
25
+ # Remove one or more fields.
26
+ #
27
+ # Example:
28
+ #
29
+ # filter {
30
+ # mutate {
31
+ # remove => [ "client" ] # Removes the 'client' field
32
+ # }
33
+ # }
34
+ #
35
+ # This option is deprecated, instead use remove_field option available in all
36
+ # filters.
37
+ config :remove, :validate => :array, :deprecated => true
38
+
39
+ # Replace a field with a new value. The new value can include %{foo} strings
40
+ # to help you build a new value from other parts of the event.
41
+ #
42
+ # Example:
43
+ #
44
+ # filter {
45
+ # mutate {
46
+ # replace => { "message" => "%{source_host}: My new message" }
47
+ # }
48
+ # }
49
+ config :replace, :validate => :hash
50
+
51
+ # Update an existing field with a new value. If the field does not exist,
52
+ # then no action will be taken.
53
+ #
54
+ # Example:
55
+ #
56
+ # filter {
57
+ # mutate {
58
+ # update => { "sample" => "My new message" }
59
+ # }
60
+ # }
61
+ config :update, :validate => :hash
62
+
63
+ # Convert a field's value to a different type, like turning a string to an
64
+ # integer. If the field value is an array, all members will be converted.
65
+ # If the field is a hash, no action will be taken.
66
+ #
67
+ # Valid conversion targets are: integer, float, string.
68
+ #
69
+ # Example:
70
+ #
71
+ # filter {
72
+ # mutate {
73
+ # convert => { "fieldname" => "integer" }
74
+ # }
75
+ # }
76
+ config :convert, :validate => :hash
77
+
78
+ # Convert a string field by applying a regular expression and a replacement.
79
+ # If the field is not a string, no action will be taken.
80
+ #
81
+ # This configuration takes an array consisting of 3 elements per
82
+ # field/substitution.
83
+ #
84
+ # Be aware of escaping any backslash in the config file.
85
+ #
86
+ # Example:
87
+ #
88
+ # filter {
89
+ # mutate {
90
+ # gsub => [
91
+ # # replace all forward slashes with underscore
92
+ # "fieldname", "/", "_",
93
+ #
94
+ # # replace backslashes, question marks, hashes, and minuses with
95
+ # # dot
96
+ # "fieldname2", "[\\?#-]", "."
97
+ # ]
98
+ # }
99
+ # }
100
+ #
101
+ config :gsub, :validate => :array
102
+
103
+ # Convert a string to its uppercase equivalent.
104
+ #
105
+ # Example:
106
+ #
107
+ # filter {
108
+ # mutate {
109
+ # uppercase => [ "fieldname" ]
110
+ # }
111
+ # }
112
+ config :uppercase, :validate => :array
113
+
114
+ # Convert a string to its lowercase equivalent.
115
+ #
116
+ # Example:
117
+ #
118
+ # filter {
119
+ # mutate {
120
+ # lowercase => [ "fieldname" ]
121
+ # }
122
+ # }
123
+ config :lowercase, :validate => :array
124
+
125
+ # Split a field to an array using a separator character. Only works on string
126
+ # fields.
127
+ #
128
+ # Example:
129
+ #
130
+ # filter {
131
+ # mutate {
132
+ # split => { "fieldname" => "," }
133
+ # }
134
+ # }
135
+ config :split, :validate => :hash
136
+
137
+ # Join an array with a separator character. Does nothing on non-array fields.
138
+ #
139
+ # Example:
140
+ #
141
+ # filter {
142
+ # mutate {
143
+ # join => { "fieldname" => "," }
144
+ # }
145
+ # }
146
+ config :join, :validate => :hash
147
+
148
+ # Strip whitespace from field. NOTE: this only works on leading and trailing whitespace.
149
+ #
150
+ # Example:
151
+ #
152
+ # filter {
153
+ # mutate {
154
+ # strip => ["field1", "field2"]
155
+ # }
156
+ # }
157
+ config :strip, :validate => :array
158
+
159
+ # Merge two fields of arrays or hashes.
160
+ # String fields will be automatically be converted into an array, so:
161
+ # array + string will work
162
+ # string + string will result in an 2 entry array in dest_field
163
+ # array and hash will not work
164
+ #
165
+ # Example:
166
+ #
167
+ # filter {
168
+ # mutate {
169
+ # merge => { "dest_field" => "added_field" }
170
+ # }
171
+ # }
172
+ config :merge, :validate => :hash
173
+
174
+ public
175
+ def register
176
+ valid_conversions = %w(string integer float)
177
+ # TODO(sissel): Validate conversion requests if provided.
178
+ @convert.nil? or @convert.each do |field, type|
179
+ if !valid_conversions.include?(type)
180
+ raise LogStash::ConfigurationError, I18n.t("logstash.agent.configuration.invalid_plugin_register",
181
+ :plugin => "filter", :type => "mutate",
182
+ :error => "Invalid conversion type '#{type}', expected one of '#{valid_conversions.join(',')}'")
183
+ end
184
+ end # @convert.each
185
+
186
+ @gsub_parsed = []
187
+ @gsub.nil? or @gsub.each_slice(3) do |field, needle, replacement|
188
+ if [field, needle, replacement].any? {|n| n.nil?}
189
+ raise LogStash::ConfigurationError, I18n.t("logstash.agent.configuration.invalid_plugin_register",
190
+ :plugin => "filter", :type => "mutate",
191
+ :error => "Invalid gsub configuration #{[field, needle, replacement]}. gsub requires 3 non-nil elements per config entry")
192
+ end
193
+
194
+ @gsub_parsed << {
195
+ :field => field,
196
+ :needle => (needle.index("%{").nil?? Regexp.new(needle): needle),
197
+ :replacement => replacement
198
+ }
199
+ end
200
+ end # def register
201
+
202
+ public
203
+ def filter(event)
204
+ return unless filter?(event)
205
+
206
+ rename(event) if @rename
207
+ update(event) if @update
208
+ replace(event) if @replace
209
+ convert(event) if @convert
210
+ gsub(event) if @gsub
211
+ uppercase(event) if @uppercase
212
+ lowercase(event) if @lowercase
213
+ strip(event) if @strip
214
+ remove(event) if @remove
215
+ split(event) if @split
216
+ join(event) if @join
217
+ merge(event) if @merge
218
+
219
+ filter_matched(event)
220
+ end # def filter
221
+
222
+ private
223
+ def remove(event)
224
+ # TODO(sissel): use event.sprintf on the field names?
225
+ @remove.each do |field|
226
+ event.remove(field)
227
+ end
228
+ end # def remove
229
+
230
+ private
231
+ def rename(event)
232
+ # TODO(sissel): use event.sprintf on the field names?
233
+ @rename.each do |old, new|
234
+ next unless event.include?(old)
235
+ event[new] = event.remove(old)
236
+ end
237
+ end # def rename
238
+
239
+ private
240
+ def update(event)
241
+ @update.each do |field, newvalue|
242
+ next unless event.include?(field)
243
+ event[field] = event.sprintf(newvalue)
244
+ end
245
+ end # def update
246
+
247
+ private
248
+ def replace(event)
249
+ @replace.each do |field, newvalue|
250
+ event[field] = event.sprintf(newvalue)
251
+ end
252
+ end # def replace
253
+
254
+ def convert(event)
255
+ @convert.each do |field, type|
256
+ next unless event.include?(field)
257
+ original = event[field]
258
+
259
+ # calls convert_{string,integer,float} depending on type requested.
260
+ converter = method("convert_" + type)
261
+ if original.nil?
262
+ next
263
+ elsif original.is_a?(Hash)
264
+ @logger.debug("I don't know how to type convert a hash, skipping",
265
+ :field => field, :value => original)
266
+ next
267
+ elsif original.is_a?(Array)
268
+ value = original.map { |v| converter.call(v) }
269
+ else
270
+ value = converter.call(original)
271
+ end
272
+ event[field] = value
273
+ end
274
+ end # def convert
275
+
276
+ def convert_string(value)
277
+ # since this is a filter and all inputs should be already UTF-8
278
+ # we wont check valid_encoding? but just force UTF-8 for
279
+ # the Fixnum#to_s case which always result in US-ASCII
280
+ # see https://twitter.com/jordansissel/status/444613207143903232
281
+ return value.to_s.force_encoding(Encoding::UTF_8)
282
+ end # def convert_string
283
+
284
+ def convert_integer(value)
285
+ return value.to_i
286
+ end # def convert_integer
287
+
288
+ def convert_float(value)
289
+ return value.to_f
290
+ end # def convert_float
291
+
292
+ private
293
+ def gsub(event)
294
+ @gsub_parsed.each do |config|
295
+ field = config[:field]
296
+ needle = config[:needle]
297
+ replacement = config[:replacement]
298
+
299
+ if event[field].is_a?(Array)
300
+ event[field] = event[field].map do |v|
301
+ if not v.is_a?(String)
302
+ @logger.warn("gsub mutation is only applicable for Strings, " +
303
+ "skipping", :field => field, :value => v)
304
+ v
305
+ else
306
+ gsub_dynamic_fields(event, v, needle, replacement)
307
+ end
308
+ end
309
+ else
310
+ if not event[field].is_a?(String)
311
+ @logger.debug("gsub mutation is only applicable for Strings, " +
312
+ "skipping", :field => field, :value => event[field])
313
+ next
314
+ end
315
+ event[field] = gsub_dynamic_fields(event, event[field], needle, replacement)
316
+ end
317
+ end # @gsub_parsed.each
318
+ end # def gsub
319
+
320
+ private
321
+ def gsub_dynamic_fields(event, original, needle, replacement)
322
+ if needle.is_a? Regexp
323
+ original.gsub(needle, event.sprintf(replacement))
324
+ else
325
+ # we need to replace any dynamic fields
326
+ original.gsub(Regexp.new(event.sprintf(needle)), event.sprintf(replacement))
327
+ end
328
+ end
329
+
330
+ private
331
+ def uppercase(event)
332
+ @uppercase.each do |field|
333
+ if event[field].is_a?(Array)
334
+ event[field].each { |v| v.upcase! }
335
+ elsif event[field].is_a?(String)
336
+ event[field].upcase!
337
+ else
338
+ @logger.debug("Can't uppercase something that isn't a string",
339
+ :field => field, :value => event[field])
340
+ end
341
+ end
342
+ end # def uppercase
343
+
344
+ private
345
+ def lowercase(event)
346
+ @lowercase.each do |field|
347
+ if event[field].is_a?(Array)
348
+ event[field].each { |v| v.downcase! }
349
+ elsif event[field].is_a?(String)
350
+ event[field].downcase!
351
+ else
352
+ @logger.debug("Can't lowercase something that isn't a string",
353
+ :field => field, :value => event[field])
354
+ end
355
+ end
356
+ end # def lowercase
357
+
358
+ private
359
+ def split(event)
360
+ @split.each do |field, separator|
361
+ if event[field].is_a?(String)
362
+ event[field] = event[field].split(separator)
363
+ else
364
+ @logger.debug("Can't split something that isn't a string",
365
+ :field => field, :value => event[field])
366
+ end
367
+ end
368
+ end
369
+
370
+ private
371
+ def join(event)
372
+ @join.each do |field, separator|
373
+ if event[field].is_a?(Array)
374
+ event[field] = event[field].join(separator)
375
+ end
376
+ end
377
+ end
378
+
379
+ private
380
+ def strip(event)
381
+ @strip.each do |field|
382
+ if event[field].is_a?(Array)
383
+ event[field] = event[field].map{|s| s.strip }
384
+ elsif event[field].is_a?(String)
385
+ event[field] = event[field].strip
386
+ end
387
+ end
388
+ end
389
+
390
+ private
391
+ def merge(event)
392
+ @merge.each do |dest_field, added_fields|
393
+ #When multiple calls, added_field is an array
394
+ added_fields = [ added_fields ] if ! added_fields.is_a?(Array)
395
+ added_fields.each do |added_field|
396
+ if event[dest_field].is_a?(Hash) ^ event[added_field].is_a?(Hash)
397
+ @logger.error("Not possible to merge an array and a hash: ",
398
+ :dest_field => dest_field,
399
+ :added_field => added_field )
400
+ next
401
+ end
402
+ if event[dest_field].is_a?(Hash) #No need to test the other
403
+ event[dest_field].update(event[added_field])
404
+ else
405
+ event[dest_field] = [event[dest_field]] if ! event[dest_field].is_a?(Array)
406
+ event[added_field] = [event[added_field]] if ! event[added_field].is_a?(Array)
407
+ event[dest_field].concat(event[added_field])
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ end # class LogStash::Filters::Mutate
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-filter-mutate'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "The mutate filter allows you to perform general mutations on fields. You can rename, remove, replace, and modify fields in your events."
7
+ s.description = "The mutate filter allows you to perform general mutations on fields. You can rename, remove, replace, and modify fields in your events."
8
+ s.authors = ["Elasticsearch"]
9
+ s.email = 'richard.pijnenburg@elasticsearch.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "group" => "filter" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
24
+
25
+ end
26
+
@@ -0,0 +1,9 @@
1
+ require "gem_publisher"
2
+
3
+ desc "Publish gem to RubyGems.org"
4
+ task :publish_gem do |t|
5
+ gem_file = Dir.glob(File.expand_path('../*.gemspec',File.dirname(__FILE__))).first
6
+ gem = GemPublisher.publish_if_updated(gem_file, :rubygems)
7
+ puts "Published #{gem}" if gem
8
+ end
9
+
@@ -0,0 +1,169 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/sha1"
4
+
5
+ def vendor(*args)
6
+ return File.join("vendor", *args)
7
+ end
8
+
9
+ directory "vendor/" => ["vendor"] do |task, args|
10
+ mkdir task.name
11
+ end
12
+
13
+ def fetch(url, sha1, output)
14
+
15
+ puts "Downloading #{url}"
16
+ actual_sha1 = download(url, output)
17
+
18
+ if actual_sha1 != sha1
19
+ fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
20
+ end
21
+ end # def fetch
22
+
23
+ def file_fetch(url, sha1)
24
+ filename = File.basename( URI(url).path )
25
+ output = "vendor/#{filename}"
26
+ task output => [ "vendor/" ] do
27
+ begin
28
+ actual_sha1 = file_sha1(output)
29
+ if actual_sha1 != sha1
30
+ fetch(url, sha1, output)
31
+ end
32
+ rescue Errno::ENOENT
33
+ fetch(url, sha1, output)
34
+ end
35
+ end.invoke
36
+
37
+ return output
38
+ end
39
+
40
+ def file_sha1(path)
41
+ digest = Digest::SHA1.new
42
+ fd = File.new(path, "r")
43
+ while true
44
+ begin
45
+ digest << fd.sysread(16384)
46
+ rescue EOFError
47
+ break
48
+ end
49
+ end
50
+ return digest.hexdigest
51
+ ensure
52
+ fd.close if fd
53
+ end
54
+
55
+ def download(url, output)
56
+ uri = URI(url)
57
+ digest = Digest::SHA1.new
58
+ tmp = "#{output}.tmp"
59
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
60
+ request = Net::HTTP::Get.new(uri.path)
61
+ http.request(request) do |response|
62
+ fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
63
+ size = (response["content-length"].to_i || -1).to_f
64
+ count = 0
65
+ File.open(tmp, "w") do |fd|
66
+ response.read_body do |chunk|
67
+ fd.write(chunk)
68
+ digest << chunk
69
+ if size > 0 && $stdout.tty?
70
+ count += chunk.bytesize
71
+ $stdout.write(sprintf("\r%0.2f%%", count/size * 100))
72
+ end
73
+ end
74
+ end
75
+ $stdout.write("\r \r") if $stdout.tty?
76
+ end
77
+ end
78
+
79
+ File.rename(tmp, output)
80
+
81
+ return digest.hexdigest
82
+ rescue SocketError => e
83
+ puts "Failure while downloading #{url}: #{e}"
84
+ raise
85
+ ensure
86
+ File.unlink(tmp) if File.exist?(tmp)
87
+ end # def download
88
+
89
+ def untar(tarball, &block)
90
+ require "archive/tar/minitar"
91
+ tgz = Zlib::GzipReader.new(File.open(tarball))
92
+ # Pull out typesdb
93
+ tar = Archive::Tar::Minitar::Input.open(tgz)
94
+ tar.each do |entry|
95
+ path = block.call(entry)
96
+ next if path.nil?
97
+ parent = File.dirname(path)
98
+
99
+ mkdir_p parent unless File.directory?(parent)
100
+
101
+ # Skip this file if the output file is the same size
102
+ if entry.directory?
103
+ mkdir path unless File.directory?(path)
104
+ else
105
+ entry_mode = entry.instance_eval { @mode } & 0777
106
+ if File.exists?(path)
107
+ stat = File.stat(path)
108
+ # TODO(sissel): Submit a patch to archive-tar-minitar upstream to
109
+ # expose headers in the entry.
110
+ entry_size = entry.instance_eval { @size }
111
+ # If file sizes are same, skip writing.
112
+ next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
113
+ end
114
+ puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
115
+ File.open(path, "w") do |fd|
116
+ # eof? check lets us skip empty files. Necessary because the API provided by
117
+ # Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
118
+ # IO object. Something about empty files in this EntryStream causes
119
+ # IO.copy_stream to throw "can't convert nil into String" on JRuby
120
+ # TODO(sissel): File a bug about this.
121
+ while !entry.eof?
122
+ chunk = entry.read(16384)
123
+ fd.write(chunk)
124
+ end
125
+ #IO.copy_stream(entry, fd)
126
+ end
127
+ File.chmod(entry_mode, path)
128
+ end
129
+ end
130
+ tar.close
131
+ File.unlink(tarball) if File.file?(tarball)
132
+ end # def untar
133
+
134
+ def ungz(file)
135
+
136
+ outpath = file.gsub('.gz', '')
137
+ tgz = Zlib::GzipReader.new(File.open(file))
138
+ begin
139
+ File.open(outpath, "w") do |out|
140
+ IO::copy_stream(tgz, out)
141
+ end
142
+ File.unlink(file)
143
+ rescue
144
+ File.unlink(outpath) if File.file?(outpath)
145
+ raise
146
+ end
147
+ tgz.close
148
+ end
149
+
150
+ desc "Process any vendor files required for this plugin"
151
+ task "vendor" do |task, args|
152
+
153
+ @files.each do |file|
154
+ download = file_fetch(file['url'], file['sha1'])
155
+ if download =~ /.tar.gz/
156
+ prefix = download.gsub('.tar.gz', '').gsub('vendor/', '')
157
+ untar(download) do |entry|
158
+ if !file['files'].nil?
159
+ next unless file['files'].include?(entry.full_name.gsub(prefix, ''))
160
+ out = entry.full_name.split("/").last
161
+ end
162
+ File.join('vendor', out)
163
+ end
164
+ elsif download =~ /.gz/
165
+ ungz(download)
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,258 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+ require "logstash/filters/mutate"
5
+
6
+ describe LogStash::Filters::Mutate do
7
+
8
+ context "config validation" do
9
+ describe "invalid convert type should raise a configuration error" do
10
+ config <<-CONFIG
11
+ filter {
12
+ mutate {
13
+ convert => [ "message", "int"] //should be integer
14
+ }
15
+ }
16
+ CONFIG
17
+
18
+ sample "not_really_important" do
19
+ insist {subject}.raises LogStash::ConfigurationError
20
+ end
21
+ end
22
+ describe "invalid gsub triad should raise a configuration error" do
23
+ config <<-CONFIG
24
+ filter {
25
+ mutate {
26
+ gsub => [ "message", "toreplace"]
27
+ }
28
+ }
29
+ CONFIG
30
+
31
+ sample "not_really_important" do
32
+ insist {subject}.raises LogStash::ConfigurationError
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "basics" do
38
+ config <<-CONFIG
39
+ filter {
40
+ mutate {
41
+ lowercase => "lowerme"
42
+ uppercase => "upperme"
43
+ convert => [ "intme", "integer", "floatme", "float" ]
44
+ rename => [ "rename1", "rename2" ]
45
+ replace => [ "replaceme", "hello world" ]
46
+ replace => [ "newfield", "newnew" ]
47
+ update => [ "nosuchfield", "weee" ]
48
+ update => [ "updateme", "updated" ]
49
+ remove => [ "removeme" ]
50
+ }
51
+ }
52
+ CONFIG
53
+
54
+ event = {
55
+ "lowerme" => [ "ExAmPlE" ],
56
+ "upperme" => [ "ExAmPlE" ],
57
+ "intme" => [ "1234", "7890.4", "7.9" ],
58
+ "floatme" => [ "1234.455" ],
59
+ "rename1" => [ "hello world" ],
60
+ "updateme" => [ "who cares" ],
61
+ "replaceme" => [ "who cares" ],
62
+ "removeme" => [ "something" ]
63
+ }
64
+
65
+ sample event do
66
+ insist { subject["lowerme"] } == ['example']
67
+ insist { subject["upperme"] } == ['EXAMPLE']
68
+ insist { subject["intme"] } == [1234, 7890, 7]
69
+ insist { subject["floatme"] } == [1234.455]
70
+ reject { subject }.include?("rename1")
71
+ insist { subject["rename2"] } == [ "hello world" ]
72
+ reject { subject }.include?("removeme")
73
+
74
+ insist { subject }.include?("newfield")
75
+ insist { subject["newfield"] } == "newnew"
76
+ reject { subject }.include?("nosuchfield")
77
+ insist { subject["updateme"] } == "updated"
78
+ end
79
+ end
80
+
81
+ describe "remove multiple fields" do
82
+ config '
83
+ filter {
84
+ mutate {
85
+ remove => [ "remove-me", "remove-me2", "diedie", "[one][two]" ]
86
+ }
87
+ }'
88
+
89
+ sample(
90
+ "remove-me" => "Goodbye!",
91
+ "remove-me2" => 1234,
92
+ "diedie" => [1, 2, 3, 4],
93
+ "survivor" => "Hello.",
94
+ "one" => { "two" => "wee" }
95
+ ) do
96
+ insist { subject["survivor"] } == "Hello."
97
+ reject { subject }.include?("remove-me")
98
+ reject { subject }.include?("remove-me2")
99
+ reject { subject }.include?("diedie")
100
+ reject { subject["one"] }.include?("two")
101
+ end
102
+ end
103
+
104
+ describe "convert one field to string" do
105
+ config '
106
+ filter {
107
+ mutate {
108
+ convert => [ "unicorns", "string" ]
109
+ }
110
+ }'
111
+
112
+ sample("unicorns" => 1234) do
113
+ insist { subject["unicorns"] } == "1234"
114
+ end
115
+ end
116
+
117
+ describe "gsub on a String" do
118
+ config '
119
+ filter {
120
+ mutate {
121
+ gsub => [ "unicorns", "but extinct", "and common" ]
122
+ }
123
+ }'
124
+
125
+ sample("unicorns" => "Magnificient, but extinct, animals") do
126
+ insist { subject["unicorns"] } == "Magnificient, and common, animals"
127
+ end
128
+ end
129
+
130
+ describe "gsub on an Array of Strings" do
131
+ config '
132
+ filter {
133
+ mutate {
134
+ gsub => [ "unicorns", "extinct", "common" ]
135
+ }
136
+ }'
137
+
138
+ sample("unicorns" => [
139
+ "Magnificient extinct animals", "Other extinct ideas" ]
140
+ ) do
141
+ insist { subject["unicorns"] } == [
142
+ "Magnificient common animals",
143
+ "Other common ideas"
144
+ ]
145
+ end
146
+ end
147
+
148
+ describe "gsub on multiple fields" do
149
+ config '
150
+ filter {
151
+ mutate {
152
+ gsub => [ "colors", "red", "blue",
153
+ "shapes", "square", "circle" ]
154
+ }
155
+ }'
156
+
157
+ sample("colors" => "One red car", "shapes" => "Four red squares") do
158
+ insist { subject["colors"] } == "One blue car"
159
+ insist { subject["shapes"] } == "Four red circles"
160
+ end
161
+ end
162
+
163
+ describe "regression - mutate should lowercase a field created by grok" do
164
+ config <<-CONFIG
165
+ filter {
166
+ grok {
167
+ match => { "message" => "%{WORD:foo}" }
168
+ }
169
+ mutate {
170
+ lowercase => "foo"
171
+ }
172
+ }
173
+ CONFIG
174
+
175
+ sample "HELLO WORLD" do
176
+ insist { subject["foo"] } == "hello"
177
+ end
178
+ end
179
+
180
+ describe "LOGSTASH-757: rename should do nothing with a missing field" do
181
+ config <<-CONFIG
182
+ filter {
183
+ mutate {
184
+ rename => [ "nosuchfield", "hello" ]
185
+ }
186
+ }
187
+ CONFIG
188
+
189
+ sample "whatever" do
190
+ reject { subject }.include?("nosuchfield")
191
+ reject { subject }.include?("hello")
192
+ end
193
+ end
194
+
195
+ describe "convert should work on nested fields" do
196
+ config <<-CONFIG
197
+ filter {
198
+ mutate {
199
+ convert => [ "[foo][bar]", "integer" ]
200
+ }
201
+ }
202
+ CONFIG
203
+
204
+ sample({ "foo" => { "bar" => "1000" } }) do
205
+ insist { subject["[foo][bar]"] } == 1000
206
+ insist { subject["[foo][bar]"] }.is_a?(Fixnum)
207
+ end
208
+ end
209
+
210
+ #LOGSTASH-1529
211
+ describe "gsub on a String with dynamic fields (%{}) in pattern" do
212
+ config '
213
+ filter {
214
+ mutate {
215
+ gsub => [ "unicorns", "of type %{unicorn_type}", "green" ]
216
+ }
217
+ }'
218
+
219
+ sample("unicorns" => "Unicorns of type blue are common", "unicorn_type" => "blue") do
220
+ insist { subject["unicorns"] } == "Unicorns green are common"
221
+ end
222
+ end
223
+
224
+ #LOGSTASH-1529
225
+ describe "gsub on a String with dynamic fields (%{}) in pattern and replace" do
226
+ config '
227
+ filter {
228
+ mutate {
229
+ gsub => [ "unicorns2", "of type %{unicorn_color}", "%{unicorn_color} and green" ]
230
+ }
231
+ }'
232
+
233
+ sample("unicorns2" => "Unicorns of type blue are common", "unicorn_color" => "blue") do
234
+ insist { subject["unicorns2"] } == "Unicorns blue and green are common"
235
+ end
236
+ end
237
+
238
+ #LOGSTASH-1529
239
+ describe "gsub on a String array with dynamic fields in pattern" do
240
+ config '
241
+ filter {
242
+ mutate {
243
+ gsub => [ "unicorns_array", "of type %{color}", "blue and green" ]
244
+ }
245
+ }'
246
+
247
+ sample("unicorns_array" => [
248
+ "Unicorns of type blue are found in Alaska", "Unicorns of type blue are extinct" ],
249
+ "color" => "blue"
250
+ ) do
251
+ insist { subject["unicorns_array"] } == [
252
+ "Unicorns blue and green are found in Alaska",
253
+ "Unicorns blue and green are extinct"
254
+ ]
255
+ end
256
+ end
257
+ end
258
+
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-filter-mutate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elasticsearch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ description: The mutate filter allows you to perform general mutations on fields.
34
+ You can rename, remove, replace, and modify fields in your events.
35
+ email: richard.pijnenburg@elasticsearch.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - .gitignore
41
+ - Gemfile
42
+ - Rakefile
43
+ - lib/logstash/filters/mutate.rb
44
+ - logstash-filter-mutate.gemspec
45
+ - rakelib/publish.rake
46
+ - rakelib/vendor.rake
47
+ - spec/filters/mutate_spec.rb
48
+ homepage: http://logstash.net/
49
+ licenses:
50
+ - Apache License (2.0)
51
+ metadata:
52
+ logstash_plugin: 'true'
53
+ group: filter
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.4.1
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: The mutate filter allows you to perform general mutations on fields. You
74
+ can rename, remove, replace, and modify fields in your events.
75
+ test_files:
76
+ - spec/filters/mutate_spec.rb