logstash-output-pipe 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
+ OTNkNDQxODQ3NjhjZjU0ODQwYWUzNDc1NWM5MDA5ZDY0ZjE4ZDFiZQ==
5
+ data.tar.gz: !binary |-
6
+ MzdjYjkxYWI2ZjRmYzg3NjdjYzZjMDZiODNmMjY5MjA2YmNiZmI4OQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ Y2U2MWZlYmRmNjdhZmNlNGU2OTZiOTI0NWE5ZWY1NDA3N2MyNzhhMGI3Mzcy
10
+ NTdlY2MyNjM1OTIzOGRiZDY5NThkNDZlMTAwZWQ5N2M3NTg2NjNhMmZjYzM3
11
+ MmZiMTBlYWQzYTcyZWFiNmIwMjRiNDZmYmVhNGQ5OTZmMjU3Zjk=
12
+ data.tar.gz: !binary |-
13
+ NjM4ZWZkNTU2MWYxZmNmOGZjYzhhZjE3ZGVkZTE2ODAzZTVhZWNkODQ5NmZh
14
+ Mzk5Mjc2OGE0ZTJkN2I2MDlkN2FhYWE4Mzk2Y2ZlMGU4N2MwNzExMTQwOWRh
15
+ YjJiMDVlMDk1OWY5ZWQwZGMxOTYwMDNjOGQ4MjRmMDRhZTczMmM=
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
4
+ gem 'archive-tar-minitar'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2014 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+ require "logstash/namespace"
3
+ require "logstash/outputs/base"
4
+
5
+ # Pipe output.
6
+ #
7
+ # Pipe events to stdin of another program. You can use fields from the
8
+ # event as parts of the command.
9
+ # WARNING: This feature can cause logstash to fork off multiple children if you are not carefull with per-event commandline.
10
+ class LogStash::Outputs::Pipe < LogStash::Outputs::Base
11
+
12
+ config_name "pipe"
13
+ milestone 1
14
+
15
+ # The format to use when writing events to the pipe. This value
16
+ # supports any string and can include %{name} and other dynamic
17
+ # strings.
18
+ #
19
+ # If this setting is omitted, the full json representation of the
20
+ # event will be written as a single line.
21
+ config :message_format, :validate => :string
22
+
23
+ # Command line to launch and pipe to
24
+ config :command, :validate => :string, :required => true
25
+
26
+ # Close pipe that hasn't been used for TTL seconds. -1 or 0 means never close.
27
+ config :ttl, :validate => :number, :default => 10
28
+ public
29
+ def register
30
+ @pipes = {}
31
+ @last_stale_cleanup_cycle = Time.now
32
+ end # def register
33
+
34
+ public
35
+ def receive(event)
36
+ return unless output?(event)
37
+
38
+ command = event.sprintf(@command)
39
+
40
+ if @message_format
41
+ output = event.sprintf(@message_format) + "\n"
42
+ else
43
+ output = event.to_json
44
+ end
45
+
46
+ begin
47
+ pipe = get_pipe(command)
48
+ pipe.puts(output)
49
+ rescue IOError, Errno::EPIPE, Errno::EBADF => e
50
+ @logger.error("Error writing to pipe, closing pipe.", :command => command, :pipe => pipe)
51
+ drop_pipe(command)
52
+ retry
53
+ end
54
+
55
+ close_stale_pipes
56
+ end # def receive
57
+
58
+ def teardown
59
+ @logger.info("Teardown: closing pipes")
60
+ @pipes.each do |command, pipe|
61
+ begin
62
+ drop_pipe(command)
63
+ @logger.debug("Closed pipe #{command}", :pipe => pipe)
64
+ rescue Exception => e
65
+ @logger.error("Excpetion while closing pipes.", :exception => e)
66
+ end
67
+ end
68
+ finished
69
+ end
70
+
71
+ private
72
+ # every 10 seconds or so (triggered by events, but if there are no events there's no point closing files anyway)
73
+ def close_stale_pipes
74
+ return if @ttl <= 0
75
+ now = Time.now
76
+ return unless now - @last_stale_cleanup_cycle >= @ttl
77
+ @logger.info("Starting stale pipes cleanup cycle", :pipes => @pipes)
78
+ inactive_pipes = @pipes.select { |command, pipe| not pipe.active }
79
+ @logger.debug("%d stale pipes found" % inactive_pipes.count, :inactive_pipes => inactive_pipes)
80
+ inactive_pipes.each do |command, pipe|
81
+ drop_pipe(command)
82
+ end
83
+ # mark all pipes as inactive, a call to write will mark them as active again
84
+ @pipes.each { |command, pipe| pipe.active = false }
85
+ @last_stale_cleanup_cycle = now
86
+ end
87
+
88
+ def drop_pipe(command)
89
+ return unless @pipes.include? command
90
+ @logger.info("Closing pipe \"%s\"" % command)
91
+ begin
92
+ @pipes[command].close
93
+ rescue Exception => e
94
+ @logger.warn("Failed to close pipe.", :error => e, :command => command)
95
+ end
96
+ @pipes.delete(command)
97
+ end
98
+
99
+ def get_pipe(command)
100
+ return @pipes[command] if @pipes.include?(command)
101
+
102
+ @logger.info("Opening pipe", :command => command)
103
+
104
+ @pipes[command] = PipeWrapper.new(command, mode="a+")
105
+ end
106
+ end # class LogStash::Outputs::Pipe
107
+
108
+ class PipeWrapper
109
+ attr_accessor :active
110
+ def initialize(command, mode="a+")
111
+ @pipe = IO.popen(command, mode)
112
+ @active = false
113
+ end
114
+
115
+ def method_missing(m, *args)
116
+ if @pipe.respond_to? m
117
+ @pipe.send(m, *args)
118
+ else
119
+ raise NoMethodError
120
+ end
121
+ end
122
+
123
+ def puts(txt)
124
+ @pipe.puts(txt)
125
+ @pipe.flush
126
+ @active = true
127
+ end
128
+
129
+ def write(txt)
130
+ @pipe.write(txt)
131
+ @active = true
132
+ end
133
+ end
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-pipe'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Pipe events to stdin of another program."
7
+ s.description = "Pipe events to stdin of another program."
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($\)+::Dir.glob('vendor/*')
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" => "output" }
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 @@
1
+ require 'spec_helper'
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-output-pipe
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-06 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: Pipe events to stdin of another program.
34
+ email: richard.pijnenburg@elasticsearch.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - Gemfile
41
+ - LICENSE
42
+ - Rakefile
43
+ - lib/logstash/outputs/pipe.rb
44
+ - logstash-output-pipe.gemspec
45
+ - rakelib/publish.rake
46
+ - rakelib/vendor.rake
47
+ - spec/outputs/pipe_spec.rb
48
+ homepage: http://logstash.net/
49
+ licenses:
50
+ - Apache License (2.0)
51
+ metadata:
52
+ logstash_plugin: 'true'
53
+ group: output
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: Pipe events to stdin of another program.
74
+ test_files:
75
+ - spec/outputs/pipe_spec.rb