logstash-filter-range 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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Nzk5Y2ZkNTcwNTczZDA1YjEyYjY0ZDZjMjZkNGE2MGI2NmE3M2E4Yg==
5
+ data.tar.gz: !binary |-
6
+ M2YwYjc5Mjc5ZGY1NjVlY2VlMWZkNWJhOThhMjNkYzdiMGJkMjI0Ng==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODk3ODk4NjU3MzdmM2M0MTk3ZGViN2ZiOTVjMjYxYjZkZWM1NThmYTAxMzc3
10
+ M2JlZmRjYTQwY2M0YmU1MzEwNDRmMTA0MjU0ZTM3ZDM3MjFmZTA3MTk4ZjMx
11
+ YWQxMGNiNGM2NjAyNjA3Yzg3YjNmN2ViNDZkYzZmNWJlOWU0MGQ=
12
+ data.tar.gz: !binary |-
13
+ YjQ0ZGE0ZTZhMDc3NGIyNWE2MTUwNDNkNDA1NTE4NGI3NzIzNzBkMmM0ODdl
14
+ ZTAyMTkyZmJiMmMwMWUwZTc5YjNiNjVhNGMyYjQwMzlkOWM4ZGIzZWMzNGVk
15
+ NTc3MjhlZTM0ZjM5M2MwNDY0OWU2MjU1M2VhZThjNmMzOGY1MmU=
data/.gitignore ADDED
@@ -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'
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,143 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+
5
+
6
+ # This filter is used to check that certain fields are within expected size/length ranges.
7
+ # Supported types are numbers and strings.
8
+ # Numbers are checked to be within numeric value range.
9
+ # Strings are checked to be within string length range.
10
+ # More than one range can be specified for same fieldname, actions will be applied incrementally.
11
+ # When field value is within a specified range an action will be taken.
12
+ # Supported actions are drop event, add tag, or add field with specified value.
13
+ #
14
+ # Example use cases are for histogram-like tagging of events
15
+ # or for finding anomaly values in fields or too big events that should be dropped.
16
+
17
+ class LogStash::Filters::Range < LogStash::Filters::Base
18
+ config_name "range"
19
+ milestone 1
20
+
21
+ # An array of field, min, max, action tuples.
22
+ # Example:
23
+ #
24
+ # filter {
25
+ # %PLUGIN% {
26
+ # ranges => [ "message", 0, 10, "tag:short",
27
+ # "message", 11, 100, "tag:medium",
28
+ # "message", 101, 1000, "tag:long",
29
+ # "message", 1001, 1e1000, "drop",
30
+ # "duration", 0, 100, "field:latency:fast",
31
+ # "duration", 101, 200, "field:latency:normal",
32
+ # "duration", 201, 1000, "field:latency:slow",
33
+ # "duration", 1001, 1e1000, "field:latency:outlier",
34
+ # "requests", 0, 10, "tag:too_few_%{host}_requests" ]
35
+ # }
36
+ # }
37
+ #
38
+ # Supported actions are drop tag or field with specified value.
39
+ # Added tag names and field names and field values can have %{dynamic} values.
40
+ #
41
+ # TODO(piavlo): The action syntax is ugly at the moment due to logstash grammar limitations - arrays grammar should support
42
+ # TODO(piavlo): simple not nested hashses as values in addition to numaric and string values to prettify the syntax.
43
+ config :ranges, :validate => :array, :default => []
44
+
45
+ # Negate the range match logic, events should be outsize of the specified range to match.
46
+ config :negate, :validate => :boolean, :default => false
47
+
48
+ public
49
+ def register
50
+ if @ranges.length % 4 != 0
51
+ raise "#{self.class.name}: ranges array should consist of 4 field tuples (field,min,max,action)"
52
+ end
53
+
54
+ @range_tuples = {}
55
+
56
+ while !@ranges.empty?
57
+ fieldname, min, max, action = @ranges.shift(4)
58
+
59
+ raise "#{self.class.name}: range field name value should be a string" if !fieldname.is_a?(String)
60
+ raise "#{self.class.name}: range min value should be a number" if !min.is_a?(Integer) and !min.is_a?(Float)
61
+ raise "#{self.class.name}: range max value should be a number" if !max.is_a?(Integer) and !max.is_a?(Float)
62
+ raise "#{self.class.name}: range action value should be a string" if !action.is_a?(String)
63
+
64
+ action = action.split(':')
65
+
66
+ case action.first
67
+ when "drop"
68
+ raise "#{self.class.name}: drop action does not accept any parameters" unless action.length == 1
69
+ action = { :name => :drop }
70
+ when "tag"
71
+ raise "#{self.class.name}: tag action accepts exactly one arg which is a tag name" unless action.length == 2
72
+ action = { :name => :add_tag, :tag => action.last }
73
+ when "field"
74
+ raise "#{self.class.name}: field action accepts exactly 2 args which are a field name and field value" unless action.length == 3
75
+ if action.last == action.last.to_i.to_s
76
+ value = action.last.to_i
77
+ elsif action.last == action.last.to_f.to_s
78
+ value = action.last.to_f
79
+ else
80
+ value = action.last
81
+ end
82
+ action = { :name => :add_field, :field => action[1], :value => value }
83
+ else
84
+ raise "#{self.class.name}: unsupported action #{action}"
85
+ end
86
+
87
+ @range_tuples[fieldname] ||= []
88
+ @range_tuples[fieldname] << { :min => min, :max => max, :action => action }
89
+ end
90
+ end # def register
91
+
92
+
93
+ public
94
+ def filter(event)
95
+ return unless filter?(event)
96
+
97
+ @range_tuples.each_key do |fieldname|
98
+ if event.include?(fieldname)
99
+ @range_tuples[fieldname].each do |range|
100
+ matched = false
101
+
102
+ field = event[fieldname]
103
+ case field
104
+ when Integer
105
+ matched = field.between?(range[:min], range[:max])
106
+ when Float
107
+ matched = field.between?(range[:min], range[:max])
108
+ when String
109
+ matched = field.length.between?(range[:min], range[:max])
110
+ else
111
+ @logger.warn("#{self.class.name}: action field value has unsupported type")
112
+ end
113
+
114
+ matched = !matched if @negate
115
+ next unless matched
116
+
117
+ case range[:action][:name]
118
+ when :drop
119
+ @logger.debug? and @logger.debug("#{self.class.name}: dropping event due to range match", :event => event)
120
+ event.cancel
121
+ return
122
+ when :add_tag
123
+ @logger.debug? and @logger.debug("#{self.class.name}: adding tag due to range match",
124
+ :event => event, :tag => range[:action][:tag] )
125
+ event.tag(event.sprintf(range[:action][:tag]))
126
+ when :add_field
127
+ @logger.debug? and @logger.debug("#{self.class.name}: adding field due to range match",
128
+ :event => event, :field => range[:action][:field], :value => range[:action][:value])
129
+ new_field = event.sprintf(range[:action][:field])
130
+ if event[new_field]
131
+ event[new_field] = [event[new_field]] if !event[new_field].is_a?(Array)
132
+ event[new_field] << event.sprintf(range[:action][:value])
133
+ else
134
+ event[new_field] = range[:action][:value].is_a?(String) ? event.sprintf(range[:action][:value]) : range[:action][:value]
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ filter_matched(event)
142
+ end # def filter
143
+ end # class LogStash::Filters::Range
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-filter-range'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "This filter is used to check that certain fields are within expected size/length ranges."
7
+ s.description = "This filter is used to check that certain fields are within expected size/length ranges."
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,169 @@
1
+ require "spec_helper"
2
+ require "logstash/filters/range"
3
+
4
+ describe LogStash::Filters::Range do
5
+
6
+
7
+ describe "range match integer field on tag action" do
8
+ config <<-CONFIG
9
+ filter {
10
+ range {
11
+ ranges => [ "duration", 10, 100, "tag:cool",
12
+ "duration", 1, 1, "tag:boring" ]
13
+ }
14
+ }
15
+ CONFIG
16
+
17
+ sample("duration" => 50) do
18
+ insist { subject["tags"] }.include?("cool")
19
+ reject { subject["tags"] }.include?("boring")
20
+ end
21
+ end
22
+
23
+ describe "range match float field on tag action" do
24
+ config <<-CONFIG
25
+ filter {
26
+ range {
27
+ ranges => [ "duration", 0, 100, "tag:cool",
28
+ "duration", 0, 1, "tag:boring" ]
29
+ }
30
+ }
31
+ CONFIG
32
+
33
+ sample("duration" => 50.0) do
34
+ insist { subject["tags"] }.include?("cool")
35
+ reject { subject["tags"] }.include?("boring")
36
+ end
37
+ end
38
+
39
+ describe "range match string field on tag action" do
40
+ config <<-CONFIG
41
+ filter {
42
+ range {
43
+ ranges => [ "length", 0, 10, "tag:cool",
44
+ "length", 0, 1, "tag:boring" ]
45
+ }
46
+ }
47
+ CONFIG
48
+
49
+ sample("length" => "123456789") do
50
+ insist { subject["tags"] }.include?("cool")
51
+ reject { subject["tags"] }.include?("boring")
52
+ end
53
+ end
54
+
55
+ describe "range match with negation" do
56
+ config <<-CONFIG
57
+ filter {
58
+ range {
59
+ ranges => [ "length", 0, 10, "tag:cool",
60
+ "length", 0, 1, "tag:boring" ]
61
+ negate => true
62
+ }
63
+ }
64
+ CONFIG
65
+
66
+ sample("length" => "123456789") do
67
+ reject { subject["tags"] }.include?("cool")
68
+ insist { subject["tags"] }.include?("boring")
69
+ end
70
+ end
71
+
72
+ describe "range match on drop action" do
73
+ config <<-CONFIG
74
+ filter {
75
+ range {
76
+ ranges => [ "length", 0, 10, "drop" ]
77
+ }
78
+ }
79
+ CONFIG
80
+
81
+ sample("length" => "123456789") do
82
+ insist { subject }.nil?
83
+ end
84
+ end
85
+
86
+ describe "range match on field action with string value" do
87
+ config <<-CONFIG
88
+ filter {
89
+ range {
90
+ ranges => [ "duration", 10, 100, "field:cool:foo",
91
+ "duration", 1, 1, "field:boring:foo" ]
92
+ }
93
+ }
94
+ CONFIG
95
+
96
+ sample("duration" => 50) do
97
+ insist { subject }.include?("cool")
98
+ insist { subject["cool"] } == "foo"
99
+ reject { subject }.include?("boring")
100
+ end
101
+ end
102
+
103
+ describe "range match on field action with integer value" do
104
+ config <<-CONFIG
105
+ filter {
106
+ range {
107
+ ranges => [ "duration", 10, 100, "field:cool:666",
108
+ "duration", 1, 1, "field:boring:666" ]
109
+ }
110
+ }
111
+ CONFIG
112
+
113
+ sample("duration" => 50) do
114
+ insist { subject }.include?("cool")
115
+ insist { subject["cool"] } == 666
116
+ reject { subject }.include?("boring")
117
+ end
118
+ end
119
+
120
+ describe "range match on field action with float value" do
121
+ config <<-CONFIG
122
+ filter {
123
+ range {
124
+ ranges => [ "duration", 10, 100, "field:cool:3.14",
125
+ "duration", 1, 1, "field:boring:3.14" ]
126
+ }
127
+ }
128
+ CONFIG
129
+
130
+ sample("duration" => 50) do
131
+ insist { subject }.include?("cool")
132
+ insist { subject["cool"] } == 3.14
133
+ reject { subject }.include?("boring")
134
+ end
135
+ end
136
+
137
+ describe "range match on tag action with dynamic string value" do
138
+ config <<-CONFIG
139
+ filter {
140
+ range {
141
+ ranges => [ "duration", 10, 100, "tag:cool_%{dynamic}_dynamic",
142
+ "duration", 1, 1, "tag:boring_%{dynamic}_dynamic" ]
143
+ }
144
+ }
145
+ CONFIG
146
+
147
+ sample("duration" => 50, "dynamic" => "and") do
148
+ insist { subject["tags"] }.include?("cool_and_dynamic")
149
+ reject { subject["tags"] }.include?("boring_and_dynamic")
150
+ end
151
+ end
152
+
153
+ describe "range match on field action with dynamic string field and value" do
154
+ config <<-CONFIG
155
+ filter {
156
+ range {
157
+ ranges => [ "duration", 10, 100, "field:cool_%{dynamic}_dynamic:foo_%{dynamic}_bar",
158
+ "duration", 1, 1, "field:boring_%{dynamic}_dynamic:foo_%{dynamic}_bar" ]
159
+ }
160
+ }
161
+ CONFIG
162
+
163
+ sample("duration" => 50, "dynamic" => "and") do
164
+ insist { subject }.include?("cool_and_dynamic")
165
+ insist { subject["cool_and_dynamic"] } == "foo_and_bar"
166
+ reject { subject }.include?("boring_and_dynamic")
167
+ end
168
+ end
169
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-filter-range
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-11-02 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: This filter is used to check that certain fields are within expected
34
+ size/length ranges.
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/range.rb
44
+ - logstash-filter-range.gemspec
45
+ - rakelib/publish.rake
46
+ - rakelib/vendor.rake
47
+ - spec/filters/range_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: This filter is used to check that certain fields are within expected size/length
74
+ ranges.
75
+ test_files:
76
+ - spec/filters/range_spec.rb