logstash-filter-mutate 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,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