logstash-integration-zeromq 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b71c26b1bd4ecbc2b7cfc0d6eb5ad13fb23adf09
4
+ data.tar.gz: 2faecee1a1c09daadf0bfb069497d3f1bcc9ddf6
5
+ SHA512:
6
+ metadata.gz: 7457d6155c7defe32bb244ecd03f51d3ac325929fec0d2d57b0f2136db040755d45b8c7320d85b7613e843c257ccbbfeec2b9cbf05810333a3fb6a05f223059d
7
+ data.tar.gz: d5f1a035835c50e363a26a8e83955ada5271e7650f22615afe490180c5d2510548d7430d68bd31386d0e4df0e5a9928dfac18e7dae388e80cefbbe3539f6d8f9
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ - Plugin created with the logstash plugin generator
@@ -0,0 +1,10 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Joao Duarte - jsvduarte@gmail.com
6
+
7
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
8
+ Logstash, and you aren't on the list above and want to be, please let us know
9
+ and we'll make sure you're here. Contributions from folks like you are what make
10
+ open source awesome.
@@ -0,0 +1,2 @@
1
+ # logstash-filter-zeromq
2
+ Example filter plugin. This should help bootstrap your effort to write your own filter plugin!
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
@@ -0,0 +1,93 @@
1
+ NOTE: all code for the individual plugins was taken from:
2
+
3
+ * input - https://github.com/logstash-plugins/logstash-input-zeromq
4
+ * filter - https://github.com/logstash-plugins/logstash-filter-zeromq
5
+ * output - https://github.com/logstash-plugins/logstash-output-zeromq
6
+ * mixin - https://github.com/logstash-plugins/logstash-mixin-zeromq
7
+
8
+ # Logstash Plugin
9
+
10
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
11
+
12
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
13
+
14
+ ## Documentation
15
+
16
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
17
+
18
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
19
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
20
+
21
+ ## Need Help?
22
+
23
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
24
+
25
+ ## Developing
26
+
27
+ ### 1. Plugin Developement and Testing
28
+
29
+ #### Code
30
+ - To get started, you'll need JRuby with the Bundler gem installed.
31
+
32
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
33
+
34
+ - Install dependencies
35
+ ```sh
36
+ bundle install
37
+ ```
38
+
39
+ #### Test
40
+
41
+ - Update your dependencies
42
+
43
+ ```sh
44
+ bundle install
45
+ ```
46
+
47
+ - Run tests
48
+
49
+ ```sh
50
+ bundle exec rspec
51
+ ```
52
+
53
+ ### 2. Running your unpublished Plugin in Logstash
54
+
55
+ #### 2.1 Run in a local Logstash clone
56
+
57
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
58
+ ```ruby
59
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
60
+ ```
61
+ - Install plugin
62
+ ```sh
63
+ bin/logstash-plugin install --no-verify
64
+ ```
65
+ - Run Logstash with your plugin
66
+ ```sh
67
+ bin/logstash -e 'filter {awesome {}}'
68
+ ```
69
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
70
+
71
+ #### 2.2 Run in an installed Logstash
72
+
73
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
74
+
75
+ - Build your plugin gem
76
+ ```sh
77
+ gem build logstash-filter-awesome.gemspec
78
+ ```
79
+ - Install the plugin from the Logstash home
80
+ ```sh
81
+ bin/logstash-plugin install /your/local/plugin/logstash-filter-awesome.gem
82
+ ```
83
+ - Start Logstash and proceed to test the plugin
84
+
85
+ ## Contributing
86
+
87
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
88
+
89
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
90
+
91
+ It is more important to the community that you are able to contribute.
92
+
93
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
@@ -0,0 +1,217 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+ require "logstash/json"
5
+
6
+ # ZeroMQ filter. This is the best way to send an event externally for filtering
7
+ # It works much like an exec filter would by sending the event "offsite"
8
+ # for processing and waiting for a response
9
+ #
10
+ # The protocol here is:
11
+ # * REQ sent with JSON-serialized logstash event
12
+ # * REP read expected to be the full JSON 'filtered' event
13
+ # * - if reply read is an empty string, it will cancel the event.
14
+ #
15
+ # Note that this is a limited subset of the zeromq functionality in
16
+ # inputs and outputs. The only topology that makes sense here is:
17
+ # REQ/REP. bunde
18
+ class LogStash::Filters::ZeroMQ < LogStash::Filters::Base
19
+
20
+ config_name "zeromq"
21
+
22
+ # 0mq socket address to connect or bind
23
+ # Please note that inproc:// will not work with logstash
24
+ # as we use a context per thread
25
+ # By default, filters connect
26
+ config :address, :validate => :string, :default => "tcp://127.0.0.1:2121"
27
+
28
+ # The field to send off-site for processing
29
+ # If this is unset, the whole event will be sent
30
+ config :field, :validate => :string
31
+
32
+ # A sentinel value to signal the filter to cancel the event
33
+ # If the peer returns the sentinel value, the event will be cancelled
34
+ config :sentinel, :validate => :string, :default => ""
35
+
36
+ # 0mq mode
37
+ # server mode binds/listens
38
+ # client mode connects
39
+ config :mode, :validate => ["server", "client"], :default => "client"
40
+
41
+ # timeout in milliseconds on which to wait for a reply.
42
+ config :timeout, :validate => :number, :default => 500
43
+
44
+ # number of retries, used for both sending and receiving messages.
45
+ # for sending, retries should return instantly.
46
+ # for receiving, the total blocking time is up to retries X timeout,
47
+ # which by default is 3 X 500 = 1500ms
48
+ config :retries, :validate => :number, :default => 3
49
+
50
+ # tag to add if zeromq timeout expires before getting back an answer.
51
+ # If set to "" then no tag will be added.
52
+ config :add_tag_on_timeout, :validate => :string, :default => "zeromqtimeout"
53
+
54
+ # 0mq socket options
55
+ # This exposes zmq_setsockopt
56
+ # for advanced tuning
57
+ # see http://api.zeromq.org/2-1:zmq-setsockopt for details
58
+ #
59
+ # This is where you would set values like:
60
+ # ZMQ::HWM - high water mark
61
+ # ZMQ::IDENTITY - named queues
62
+ # ZMQ::SWAP_SIZE - space for disk overflow
63
+ # ZMQ::SUBSCRIBE - topic filters for pubsub
64
+ #
65
+ # example: sockopt => ["ZMQ::HWM", 50, "ZMQ::IDENTITY", "my_named_queue"]
66
+ config :sockopt, :validate => :hash
67
+
68
+ public
69
+ def initialize(params)
70
+ super(params)
71
+
72
+ @threadsafe = false
73
+ end
74
+
75
+ public
76
+ def register
77
+ require "ffi-rzmq"
78
+ require "logstash/zeromq_mixin"
79
+ self.class.send(:include, LogStash::PluginMixins::ZeroMQ)
80
+ connect
81
+ end #def register
82
+
83
+ public
84
+ def close
85
+ close
86
+ super()
87
+ end #def close
88
+
89
+ private
90
+ def close
91
+ @logger.debug("0mq: closing socket.")
92
+ @poller.deregister(@zsocket, ZMQ::POLLIN)
93
+ @zsocket.close
94
+ end #def close
95
+
96
+ private
97
+ def connect
98
+ @logger.debug("0mq: connecting socket")
99
+ @zsocket = context.socket(ZMQ::REQ)
100
+ error_check(@zsocket.setsockopt(ZMQ::LINGER, 0),
101
+ "while setting ZMQ::LINGER == 0)")
102
+ @poller = ZMQ::Poller.new
103
+ @poller.register(@zsocket, ZMQ::POLLIN)
104
+
105
+ if @sockopt
106
+ #TODO: should make sure that ZMQ::LINGER and ZMQ::POLLIN are not changed
107
+ setopts(@zsocket, @sockopt)
108
+ end
109
+
110
+ setup(@zsocket, @address)
111
+ end #def connect
112
+
113
+ private
114
+ def reconnect
115
+ close
116
+ connect
117
+ end #def reconnect
118
+
119
+ #send and receive data. message is assumed to be json
120
+ #will return a boolean for success, and a string containing one of several things:
121
+ # - empty string: response from server
122
+ # - updated string: response from server
123
+ # - original message: could not send request or get response from server in time
124
+ private
125
+ def send_recv(message)
126
+ success = false
127
+ @retries.times do
128
+ @logger.debug? && @logger.debug("0mq: sending", :request => message)
129
+ rc = @zsocket.send_string(message)
130
+ if ZMQ::Util.resultcode_ok?(rc)
131
+ success = true
132
+ break
133
+ else
134
+ @logger.debug? && @logger.debug("0mq: error sending message (zmq_errno = #{ZMQ::Util.errno}, zmq_error_string = '#{ZMQ::Util.error_string}'")
135
+ reconnect
136
+ end #if resultcode
137
+ end #retries.times
138
+
139
+ #if we did not succeed log it and fail here.
140
+ if not success
141
+ @logger.warn("0mq: error sending message (zmq_errno = #{ZMQ::Util.errno}, zmq_error_string = '#{ZMQ::Util.error_string}'")
142
+ return success, message
143
+ end
144
+
145
+ #now get reply
146
+ reply = ''
147
+ success = false
148
+ @retries.times do
149
+ @logger.debug? && @logger.debug("0mq: polling for reply for #{@timeout}ms.")
150
+ #poll the socket. If > 0, something to read. If < 0, error. If zero, loop
151
+ num_readable = @poller.poll(@timeout)
152
+ if num_readable > 0
153
+ #something to read, do it.
154
+ rc = @zsocket.recv_string(reply)
155
+ @logger.debug? && @logger.debug("0mq: message received, checking error")
156
+ error_check(rc, "in recv_string")
157
+ success = true
158
+ break
159
+ elsif num_readable < 0
160
+ #error, reconnect
161
+ close
162
+ connect
163
+ end
164
+ end # @retries.times
165
+
166
+ #if we maxed out on number of retries, then set reply to message so that
167
+ #the event isn't cancelled. we want to carry on if the server is down.
168
+ if not success
169
+ @logger.warn("0mq: did not receive reply (zmq_errno = #{ZMQ::Util.errno}, zmq_error_string = '#{ZMQ::Util.error_string}'")
170
+ return success, message
171
+ end
172
+
173
+ return success, reply
174
+ end #def send_recv
175
+
176
+ public
177
+ def filter(event)
178
+ begin
179
+ # TODO (lusis): Allow filtering multiple fields
180
+ if @field
181
+ success, reply = send_recv(event.get(@field))
182
+ else
183
+ success, reply = send_recv(event.to_json)
184
+ end
185
+ # If we receive an sentinel value as reply, this is an indication that the filter
186
+ # wishes to cancel this event.
187
+ if success && reply == @sentinel
188
+ @logger.debug? && @logger.debug("0mq: recieved sentinel value, cancelling event.")
189
+ event.cancel
190
+ return
191
+ end
192
+ @logger.debug? && @logger.debug("0mq: receiving", :reply => reply)
193
+ if @field
194
+ event.set(@field, event.sprintf(reply))
195
+ filter_matched(event)
196
+ else
197
+ reply = LogStash::Event.new(LogStash::Json.load(reply))
198
+ event.overwrite(reply)
199
+ end
200
+ filter_matched(event)
201
+ #if message send/recv was not successful add the timeout
202
+ if not success
203
+ tags = event.get("tags") || []
204
+ tags << @add_tag_on_timeout
205
+ event.set("tags", tags)
206
+ end
207
+ rescue => e
208
+ @logger.warn("0mq filter exception", :address => @address, :exception => e, :backtrace => e.backtrace)
209
+ end
210
+ end # def filter
211
+
212
+ private
213
+ def server?
214
+ @mode == "server"
215
+ end # def server?
216
+
217
+ end
@@ -0,0 +1,186 @@
1
+ # encoding: utf-8
2
+ require "logstash/inputs/base"
3
+ require "logstash/namespace"
4
+ require "socket"
5
+
6
+ # Read events over a 0MQ SUB socket.
7
+ #
8
+ # You need to have the 0mq 2.1.x library installed to be able to use
9
+ # this input plugin.
10
+ #
11
+ # The default settings will create a subscriber binding to `tcp://127.0.0.1:2120`
12
+ # waiting for connecting publishers.
13
+ #
14
+ class LogStash::Inputs::ZeroMQ < LogStash::Inputs::Base
15
+
16
+ config_name "zeromq"
17
+
18
+ default :codec, "json"
19
+
20
+ # 0mq socket address to connect or bind
21
+ # Please note that `inproc://` will not work with logstash
22
+ # as each we use a context per thread.
23
+ # By default, inputs bind/listen
24
+ # and outputs connect
25
+ config :address, :validate => :array, :default => ["tcp://*:2120"]
26
+
27
+ # 0mq topology
28
+ # The default logstash topologies work as follows:
29
+ #
30
+ # * pushpull - inputs are pull, outputs are push
31
+ # * pubsub - inputs are subscribers, outputs are publishers
32
+ # * pair - inputs are clients, inputs are servers
33
+ #
34
+ # If the predefined topology flows don't work for you,
35
+ # you can change the `mode` setting
36
+ # TODO (lusis) add req/rep MAYBE
37
+ # TODO (lusis) add router/dealer
38
+ config :topology, :validate => ["pushpull", "pubsub", "pair"], :required => true
39
+
40
+ # 0mq topic
41
+ # This is used for the `pubsub` topology only
42
+ # On inputs, this allows you to filter messages by topic
43
+ # On outputs, this allows you to tag a message for routing
44
+ # NOTE: ZeroMQ does subscriber side filtering.
45
+ # NOTE: All topics have an implicit wildcard at the end
46
+ # You can specify multiple topics here
47
+ config :topic, :validate => :array
48
+
49
+ # Event topic field
50
+ # This is used for the `pubsub` topology only
51
+ # When a message is received on a topic, the topic name on which
52
+ # the message was received will saved in this field.
53
+ config :topic_field, :validate => :string, :default => "topic"
54
+
55
+ # mode
56
+ # server mode binds/listens
57
+ # client mode connects
58
+ config :mode, :validate => ["server", "client"], :default => "server"
59
+
60
+ # sender
61
+ # overrides the sender to
62
+ # set the source of the event
63
+ # default is `zmq+topology://type/`
64
+ config :sender, :validate => :string
65
+
66
+ # 0mq socket options
67
+ # This exposes `zmq_setsockopt`
68
+ # for advanced tuning
69
+ # see http://api.zeromq.org/2-1:zmq-setsockopt for details
70
+ #
71
+ # This is where you would set values like:
72
+ #
73
+ # * `ZMQ::HWM` - high water mark
74
+ # * `ZMQ::IDENTITY` - named queues
75
+ # * `ZMQ::SWAP_SIZE` - space for disk overflow
76
+ #
77
+ # Example:
78
+ # [source,ruby]
79
+ # sockopt => {
80
+ # "ZMQ::HWM" => 50
81
+ # "ZMQ::IDENTITY" => "my_named_queue"
82
+ # }
83
+ #
84
+ # defaults to: `sockopt => { "ZMQ::RCVTIMEO" => "1000" }`, which has the effect of "interrupting"
85
+ # the recv operation at least once every second to allow for properly shutdown handling.
86
+ config :sockopt, :validate => :hash, :default => { "ZMQ::RCVTIMEO" => "1000" }
87
+
88
+ public
89
+ def register
90
+ require "ffi-rzmq"
91
+ require "logstash/zeromq_mixin"
92
+ self.class.send(:include, LogStash::PluginMixins::ZeroMQ)
93
+ @host = Socket.gethostname
94
+ init_socket
95
+ end # def register
96
+
97
+ def init_socket
98
+ case @topology
99
+ when "pair"
100
+ zmq_const = ZMQ::PAIR
101
+ when "pushpull"
102
+ zmq_const = ZMQ::PULL
103
+ when "pubsub"
104
+ zmq_const = ZMQ::SUB
105
+ end # case socket_type
106
+ @zsocket = context.socket(zmq_const)
107
+ error_check(@zsocket.setsockopt(ZMQ::LINGER, 1),
108
+ "while setting ZMQ::LINGER == 1)")
109
+
110
+ if @sockopt
111
+ setopts(@zsocket, @sockopt)
112
+ end
113
+
114
+ @address.each do |addr|
115
+ setup(@zsocket, addr)
116
+ end
117
+
118
+ if @topology == "pubsub"
119
+ if @topic.nil?
120
+ @logger.debug("ZMQ - No topic provided. Subscribing to all messages")
121
+ error_check(@zsocket.setsockopt(ZMQ::SUBSCRIBE, ""),
122
+ "while setting ZMQ::SUBSCRIBE")
123
+ else
124
+ @topic.each do |t|
125
+ @logger.debug("ZMQ subscribing to topic: #{t}")
126
+ error_check(@zsocket.setsockopt(ZMQ::SUBSCRIBE, t),
127
+ "while setting ZMQ::SUBSCRIBE == #{t}")
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def close
134
+ begin
135
+ error_check(@zsocket.close, "while closing the zmq socket")
136
+ context.terminate
137
+ rescue RuntimeError => e
138
+ @logger.error("Failed to properly teardown ZeroMQ")
139
+ end
140
+ end # def close
141
+
142
+ def server?
143
+ @mode == "server"
144
+ end # def server?
145
+
146
+ def run(output_queue)
147
+ begin
148
+ while !stop?
149
+ handle_message(output_queue)
150
+ end
151
+ rescue => e
152
+ @logger.debug? && @logger.debug("ZMQ Error", :subscriber => @zsocket,
153
+ :exception => e)
154
+ retry
155
+ end # begin
156
+ end # def run
157
+
158
+ private
159
+ def build_source_string
160
+ id = @address.first.clone
161
+ end
162
+
163
+ def handle_message(output_queue)
164
+ # Here's the unified receiver
165
+ more = true
166
+ parts = []
167
+ rc = @zsocket.recv_strings(parts)
168
+ error_check(rc, "in recv_strings", true)
169
+ return unless ZMQ::Util.resultcode_ok?(rc)
170
+
171
+ if @topology == "pubsub" && parts.length > 1
172
+ # assume topic is a simple string
173
+ topic, *parts = parts
174
+ else
175
+ topic = nil
176
+ end
177
+ parts.each do |msg|
178
+ @codec.decode(msg) do |event|
179
+ event.set("host", event.get("host") || @host)
180
+ event.set(@topic_field, topic.force_encoding('UTF-8')) unless topic.nil?
181
+ decorate(event)
182
+ output_queue << event
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+ require "logstash/namespace"
4
+
5
+ # Write events to a 0MQ PUB socket.
6
+ #
7
+ # You need to have the 0mq 2.1.x library installed to be able to use
8
+ # this output plugin.
9
+ #
10
+ # The default settings will create a publisher connecting to a subscriber
11
+ # bound to tcp://127.0.0.1:2120
12
+ #
13
+ class LogStash::Outputs::ZeroMQ < LogStash::Outputs::Base
14
+
15
+ config_name "zeromq"
16
+
17
+ default :codec, "json"
18
+
19
+ # 0mq socket address to connect or bind.
20
+ # Please note that `inproc://` will not work with logstashi.
21
+ # For each we use a context per thread.
22
+ # By default, inputs bind/listen and outputs connect.
23
+ config :address, :validate => :array, :default => ["tcp://127.0.0.1:2120"]
24
+
25
+ # The default logstash topologies work as follows:
26
+ #
27
+ # * pushpull - inputs are pull, outputs are push
28
+ # * pubsub - inputs are subscribers, outputs are publishers
29
+ # * pair - inputs are clients, outputs are servers
30
+ #
31
+ # If the predefined topology flows don't work for you,
32
+ # you can change the 'mode' setting
33
+ config :topology, :validate => ["pushpull", "pubsub", "pair"], :required => true
34
+
35
+ # This is used for the 'pubsub' topology only.
36
+ # On inputs, this allows you to filter messages by topic.
37
+ # On outputs, this allows you to tag a message for routing.
38
+ # NOTE: ZeroMQ does subscriber-side filtering
39
+ # NOTE: Topic is evaluated with `event.sprintf` so macros are valid here.
40
+ config :topic, :validate => :string, :default => ""
41
+
42
+ # Server mode binds/listens. Client mode connects.
43
+ config :mode, :validate => ["server", "client"], :default => "client"
44
+
45
+ # This exposes zmq_setsockopt for advanced tuning.
46
+ # See http://api.zeromq.org/2-1:zmq-setsockopt for details.
47
+ #
48
+ # This is where you would set values like:
49
+ #
50
+ # * ZMQ::HWM - high water mark
51
+ # * ZMQ::IDENTITY - named queues
52
+ # * ZMQ::SWAP_SIZE - space for disk overflow
53
+ #
54
+ # Example:
55
+ # [source,ruby]
56
+ # sockopt => {
57
+ # "ZMQ::HWM" => 50
58
+ # "ZMQ::IDENTITY" => "my_named_queue"
59
+ # }
60
+ config :sockopt, :validate => :hash
61
+
62
+ public
63
+ def register
64
+ load_zmq
65
+
66
+ if @mode == "server"
67
+ workers_not_supported("With 'mode => server', only one zeromq socket may bind to a port and may not be shared among threads. Going to single-worker mode for this plugin!")
68
+ end
69
+
70
+ connect
71
+
72
+ @codec.on_event(&method(:publish))
73
+ end # def register
74
+
75
+ public
76
+ def close
77
+ begin
78
+ error_check(@zsocket.close, "while closing the socket")
79
+ rescue RuntimeError => e
80
+ @logger.error("Failed to properly teardown ZeroMQ")
81
+ end
82
+ end # def close
83
+
84
+ def receive(event)
85
+ @codec.encode(event)
86
+ end
87
+
88
+ private
89
+ def server?
90
+ @mode == "server"
91
+ end # def server?
92
+
93
+ def publish(event, payload)
94
+ if @topology == "pubsub"
95
+ topic = event.sprintf(@topic)
96
+ error_check(@zsocket.send_string(topic, ZMQ::SNDMORE), "in topic send_string")
97
+ end
98
+ @logger.debug? && @logger.debug("0mq: sending", :event => payload)
99
+ error_check(@zsocket.send_string(payload), "in send_string")
100
+ rescue => e
101
+ warn e.inspect
102
+ @logger.warn("0mq output exception", :address => @address, :exception => e)
103
+ end
104
+
105
+ def load_zmq
106
+ require "ffi-rzmq"
107
+ require "logstash/zeromq_mixin"
108
+ self.class.send(:include, LogStash::PluginMixins::ZeroMQ)
109
+ end
110
+
111
+ def connect
112
+ # Translate topology shorthand to socket types
113
+ case @topology
114
+ when "pair"
115
+ zmq_const = ZMQ::PAIR
116
+ when "pushpull"
117
+ zmq_const = ZMQ::PUSH
118
+ when "pubsub"
119
+ zmq_const = ZMQ::PUB
120
+ end # case socket_type
121
+
122
+ @zsocket = context.socket(zmq_const)
123
+
124
+ error_check(@zsocket.setsockopt(ZMQ::LINGER, 1),
125
+ "while setting ZMQ::LINGER == 1)")
126
+
127
+ if @sockopt
128
+ setopts(@zsocket, @sockopt)
129
+ end
130
+
131
+ @address.each do |addr|
132
+ setup(@zsocket, addr)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+ require 'ffi-rzmq'
3
+ require "logstash/namespace"
4
+
5
+ class LogStash::PluginMixins::ZeroMQContext
6
+ class << self
7
+ def context
8
+ @context ||= ZMQ::Context.new
9
+ end
10
+ end
11
+ end
12
+
13
+ module LogStash::PluginMixins::ZeroMQ
14
+ # LOGSTASH-400
15
+ # see https://github.com/chuckremes/ffi-rzmq/blob/master/lib/ffi-rzmq/socket.rb#L93-117
16
+ STRING_OPTS = %w{IDENTITY SUBSCRIBE UNSUBSCRIBE}
17
+
18
+ def context
19
+ LogStash::PluginMixins::ZeroMQContext.context
20
+ end
21
+
22
+ def terminate_context
23
+ context.terminate
24
+ end
25
+
26
+ def setup(socket, address)
27
+ if server?
28
+ error_check(socket.bind(address), "binding to #{address}")
29
+ else
30
+ error_check(socket.connect(address), "connecting to #{address}")
31
+ end
32
+ @logger.info("0mq: #{server? ? 'bound' : 'connected'}", :address => address)
33
+ end
34
+
35
+ def error_check(rc, doing, eagain_not_error=false)
36
+ unless ZMQ::Util.resultcode_ok?(rc) || (ZMQ::Util.errno == ZMQ::EAGAIN && eagain_not_error)
37
+ @logger.error("ZeroMQ error while #{doing}", { :error_code => rc })
38
+ raise "ZeroMQ Error while #{doing}"
39
+ end
40
+ end # def error_check
41
+
42
+ def setopts(socket, options)
43
+ options.each do |opt,value|
44
+ sockopt = opt.split('::')[1]
45
+ option = ZMQ.const_defined?(sockopt) ? ZMQ.const_get(sockopt) : ZMQ.const_missing(sockopt)
46
+ unless STRING_OPTS.include?(sockopt)
47
+ begin
48
+ Float(value)
49
+ value = value.to_i
50
+ rescue ArgumentError
51
+ raise "#{sockopt} requires a numeric value. #{value} is not numeric"
52
+ end
53
+ end # end unless
54
+ error_check(socket.setsockopt(option, value),
55
+ "while setting #{opt} == #{value}")
56
+ end # end each
57
+ end # end setopts
58
+ end # module LogStash::PluginMixins::ZeroMQ
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ require "logstash/plugins/registry"
3
+ require "logstash/inputs/zeromq"
4
+ require "logstash/filters/zeromq"
5
+ require "logstash/outputs/zeromq"
6
+
7
+ LogStash::PLUGIN_REGISTRY.add(:input, "zeromq", LogStash::Inputs::ZeroMQ)
8
+ LogStash::PLUGIN_REGISTRY.add(:filter, "zeromq", LogStash::Filters::ZeroMQ)
9
+ LogStash::PLUGIN_REGISTRY.add(:output, "zeromq", LogStash::Outputs::ZeroMQ)
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-integration-zeromq'
3
+ s.version = '0.1.1'
4
+ s.licenses = ['Apache License (2.0)']
5
+ s.summary = 'Logstash integration with ZeroMQ with input filter and output plugins'
6
+ s.homepage = 'https://elastic.co'
7
+ s.authors = ['Joao Duarte']
8
+ s.email = 'jsvduarte@gmail.com'
9
+ s.require_paths = ['lib']
10
+
11
+ # Files
12
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
13
+ # Tests
14
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
+
16
+ # Special flag to let us know this is actually a logstash plugin
17
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "pack" }
18
+
19
+ # Gem dependencies
20
+ s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
21
+ s.add_runtime_dependency "ffi-rzmq", "~> 2.0"
22
+ s.add_development_dependency 'logstash-devutils'
23
+ s.add_development_dependency 'logstash-codec-json'
24
+ end
@@ -0,0 +1,56 @@
1
+ require "logstash/devutils/rspec/spec_helper"
2
+ require 'logstash/filters/zeromq'
3
+ require 'json'
4
+ require 'securerandom'
5
+
6
+ describe LogStash::Filters::ZeroMQ do
7
+ let(:sentinel) { SecureRandom.hex[1..2] }
8
+ let(:filter) { LogStash::Plugin.lookup("filter", "zeromq").new({"sentinel" => sentinel}) }
9
+ let(:event) do
10
+ LogStash::Event.new({
11
+ "message" => "some message",
12
+ "seq" => rand(1000),
13
+ "field1" => "some field value",
14
+ "@version" => "1",
15
+ "@timestamp" => Time.now.utc.iso8601(3)
16
+ })
17
+ end
18
+
19
+ let(:event_text) { event.to_json }
20
+
21
+ before do
22
+ allow(filter).to receive(:connect)
23
+ end
24
+
25
+ it "should send entire event as json if field not specified" do
26
+ expect(filter).to receive(:send_recv).with(event_text)
27
+ filter.filter(event)
28
+ end
29
+
30
+ it "should cancel event if peer returns an answer==sentinel" do
31
+ expect(filter).to receive(:send_recv).and_return([true, sentinel])
32
+ expect(event).to receive(:cancel)
33
+ filter.filter(event)
34
+ end
35
+
36
+ it "should not cancel event if send_recv failed" do
37
+ expect(filter).to receive(:send_recv).and_return([false, event_text])
38
+ expect(event).to_not receive(:cancel)
39
+ filter.filter(event)
40
+ end
41
+
42
+ context "Filter specific field" do
43
+ let(:filter) { LogStash::Plugin.lookup("filter", "zeromq").new({"field" => "field1"}) }
44
+
45
+ it "should send only a single field" do
46
+ expect(filter).to receive(:send_recv).with(event.get("field1"))
47
+ filter.filter(event)
48
+ end
49
+
50
+ it "should not cancel event if send_recv failed and event field==sentinel" do
51
+ expect(filter).to receive(:send_recv).and_return([false, sentinel])
52
+ expect(event).to_not receive(:cancel)
53
+ filter.filter(event)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+ require_relative "../spec_helper"
3
+ require "logstash/plugin"
4
+ require "logstash/event"
5
+ require "securerandom"
6
+
7
+ def send_mock_messages(messages, &block)
8
+ socket = double("socket")
9
+ expect(socket).to receive(:recv_strings) do |arr|
10
+ messages.each do |msg|
11
+ msg.each do |frame|
12
+ arr << frame
13
+ end
14
+ end
15
+ 0
16
+ end
17
+ plugin.instance_variable_set(:@zsocket, socket)
18
+ q = []
19
+ plugin.send(:handle_message, q)
20
+ q
21
+ end
22
+
23
+ describe LogStash::Inputs::ZeroMQ, :zeromq => true do
24
+
25
+ context "when register and close" do
26
+
27
+ let(:plugin) { LogStash::Plugin.lookup("input", "zeromq").new({ "topology" => "pushpull" }) }
28
+
29
+ it "should register and close without errors" do
30
+ expect { plugin.register }.to_not raise_error
31
+ expect { plugin.close }.to_not raise_error
32
+ end
33
+
34
+ context "when interrupting the plugin" do
35
+ it_behaves_like "an interruptible input plugin" do
36
+ let(:config) { { "topology" => "pushpull" } }
37
+ before do
38
+ subject.register
39
+ end
40
+ after do
41
+ subject.close
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ context "pubsub" do
49
+ topic_field = SecureRandom.hex
50
+ let(:plugin) { LogStash::Plugin.lookup("input", "zeromq").new({"topology" => "pubsub", "topic_field" => topic_field}) }
51
+
52
+ before do
53
+ allow(plugin).to receive(:init_socket)
54
+ plugin.register
55
+ end
56
+
57
+ it "should set the topic field with multiple message frames" do
58
+ events = send_mock_messages([["topic", '{"message": "message"}', '{"message": "message2"}']])
59
+ expect(events.first.get(topic_field)).to eq("topic")
60
+ expect(events.first.get("message")).to eq("message")
61
+ expect(events[1].get("message")).to eq("message2")
62
+ expect(events[1].get(topic_field)).to eq("topic")
63
+ expect(events.length).to eq(2)
64
+ end
65
+ end
66
+
67
+ context "pushpull" do
68
+ let(:plugin) { LogStash::Plugin.lookup("input", "zeromq").new({ "topology" => "pushpull" }) }
69
+
70
+ before do
71
+ allow(plugin).to receive(:init_socket)
72
+ plugin.register
73
+ end
74
+
75
+ it "should receive multiple frames" do
76
+ events = send_mock_messages([['{"message": "message"}', '{"message": "message2"}']])
77
+ expect(events.first.get("message")).to eq("message")
78
+ expect(events[1].get("message")).to eq("message2")
79
+ expect(events.length).to eq(2)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ require_relative "../spec_helper"
3
+ require "logstash/plugin"
4
+ require "logstash/event"
5
+ require "json"
6
+
7
+ describe LogStash::Inputs::ZeroMQ, :integration => true do
8
+
9
+ let(:helpers) { ZeroMQHelpers.new }
10
+
11
+ describe "receive events" do
12
+
13
+ let(:nevents) { 10 }
14
+ let(:port) { rand(1000)+1025 }
15
+
16
+ let(:conf) do
17
+ { "address" => ["tcp://127.0.0.1:#{port}"],
18
+ "topology" => "pubsub" }
19
+ end
20
+
21
+ let(:events) do
22
+ helpers.input(conf, nevents) do
23
+ client = ZeroMQClient.new("127.0.0.1", port)
24
+ nevents.times do |value|
25
+ client.send("TOPIC", ZMQ::SNDMORE)
26
+ client.send({"message" => "data #{value}"}.to_json)
27
+ end
28
+ client.close
29
+ end
30
+ end
31
+
32
+ it "should receive the events" do
33
+ expect(events.count).to be(nevents)
34
+ expect(events.map(&:to_hash)).to all(include("topic" => "TOPIC"))
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ require_relative "../spec_helper"
3
+ require "logstash/plugin"
4
+ require "logstash/event"
5
+
6
+ describe LogStash::Outputs::ZeroMQ, :integration => true do
7
+
8
+ describe "send events" do
9
+
10
+ let(:nevents) { 10 }
11
+ let(:port) { rand(1000)+1025 }
12
+
13
+ let(:conf) do
14
+ { "address" => ["tcp://127.0.0.1:#{port}"],
15
+ "topology" => "pushpull" }
16
+ end
17
+
18
+ let(:events) do
19
+ output(conf, nevents) do
20
+ res = []
21
+ client = ZeroMQServer.new("127.0.0.1", port)
22
+ nevents.times do
23
+ res << client.recv
24
+ end
25
+ client.close
26
+ res
27
+ end
28
+ end
29
+
30
+ it "should receive the events" do
31
+ expect(events.count).to be(nevents)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ require_relative "../spec_helper"
3
+ require "logstash/plugin"
4
+ require "logstash/event"
5
+
6
+ describe LogStash::Outputs::ZeroMQ, :zeromq => true do
7
+
8
+ context "when register and close" do
9
+
10
+ let(:plugin) { LogStash::Plugin.lookup("output", "zeromq").new({ "topology" => "pushpull" }) }
11
+
12
+ it "should register and close without errors" do
13
+ expect { plugin.register }.to_not raise_error
14
+ expect { plugin.do_close }.to_not raise_error
15
+ end
16
+
17
+ end
18
+
19
+ context "pubsub" do
20
+ let(:plugin) { LogStash::Plugin.lookup("output", "zeromq").new({"topology" => "pubsub", "topic" => "%{topic}"})}
21
+
22
+ before do
23
+ plugin.send(:load_zmq)
24
+ end
25
+
26
+ it "should use topic field as topic" do
27
+ mock_socket = instance_spy("ZMQ::Socket", :send_string => 0)
28
+ plugin.instance_variable_set(:@zsocket, mock_socket)
29
+ plugin.send(:publish, LogStash::Event.new("topic" => "test-topic", "message" => "text"), "payload")
30
+ expect(mock_socket).to have_received(:send_string).with("test-topic", ZMQ::SNDMORE).once.ordered
31
+ expect(mock_socket).to have_received(:send_string).with("payload").ordered
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/inputs/zeromq"
4
+ require "logstash/filters/zeromq"
5
+ require "logstash/outputs/zeromq"
6
+ require_relative "support/client"
7
+
8
+ class ZeroMQHelpers
9
+
10
+ def input(config, size, &block)
11
+ plugin = LogStash::Plugin.lookup("input", "zeromq").new(config)
12
+ plugin.register
13
+ queue = Queue.new
14
+
15
+ pipeline_thread = Thread.new { plugin.run(queue) }
16
+ sleep 0.3
17
+ block.call
18
+ sleep 0.1 while queue.size != size
19
+ result = size.times.inject([]) do |acc|
20
+ acc << queue.pop
21
+ end
22
+ plugin.do_stop
23
+ pipeline_thread.join
24
+ result
25
+ end # def input
26
+
27
+ end
28
+
29
+ RSpec.configure do |config|
30
+ # config.filter_run_excluding({ :zeromq => true, :integration => true })
31
+ config.order = :random
32
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ require 'ffi-rzmq'
3
+
4
+ class ZeroMQClient
5
+
6
+ attr_reader :addr, :port, :context, :requester
7
+
8
+ def initialize(addr, port)
9
+ @addr = addr
10
+ @port = port
11
+ @context = ZMQ::Context.new(1)
12
+ @requester = context.socket(ZMQ::PUB)
13
+ @requester.connect("tcp://#{addr}:#{port}")
14
+ end
15
+
16
+ def send(data, flags=0)
17
+ @requester.send_string(data, flags)
18
+ end
19
+
20
+ def close
21
+ @requester.close
22
+ @context.terminate
23
+ end
24
+ end
25
+
26
+ class ZeroMQServer
27
+
28
+ attr_reader :addr, :port, :context, :requester
29
+
30
+ def initialize(addr, port)
31
+ @addr = addr
32
+ @port = port
33
+ @context = ZMQ::Context.new(1)
34
+ @requester = context.socket(ZMQ::PULL)
35
+ @requester.bind("tcp://#{addr}:#{port}")
36
+ end
37
+
38
+ def recv
39
+ s = ''
40
+ @requester.recv_string s
41
+ s
42
+ end
43
+
44
+ def close
45
+ @requester.close
46
+ @context.terminate
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ require_relative "spec_helper"
3
+ require "logstash/plugin"
4
+ require "logstash/event"
5
+ require "json"
6
+
7
+ describe LogStash::Inputs::ZeroMQ, :integration => true do
8
+
9
+ let(:helpers) { ZeroMQHelpers.new }
10
+
11
+ describe "receive events" do
12
+
13
+ let(:nevents) { 10 }
14
+ let(:port) { rand(1000)+1025 }
15
+
16
+ let(:conf) do
17
+ { "address" => ["tcp://127.0.0.1:#{port}"],
18
+ "topology" => "pubsub" }
19
+ end
20
+
21
+ let(:events) do
22
+ helpers.input(conf, nevents) do
23
+ client = ZeroMQClient.new("127.0.0.1", port)
24
+ nevents.times do |value|
25
+ client.send("TOPIC", ZMQ::SNDMORE)
26
+ client.send({"message" => "data #{value}"}.to_json)
27
+ end
28
+ client.close
29
+ end
30
+ end
31
+
32
+ it "should receive the events" do
33
+ expect(events.count).to be(nevents)
34
+ expect(events.map(&:to_hash)).to all(include("topic" => "TOPIC"))
35
+ end
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-integration-zeromq
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Joao Duarte
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ name: logstash-core-plugin-api
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ name: ffi-rzmq
34
+ prerelease: false
35
+ type: :runtime
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ name: logstash-devutils
48
+ prerelease: false
49
+ type: :development
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ name: logstash-codec-json
62
+ prerelease: false
63
+ type: :development
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email: jsvduarte@gmail.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - CONTRIBUTORS
77
+ - DEVELOPER.md
78
+ - Gemfile
79
+ - LICENSE
80
+ - README.md
81
+ - lib/logstash/filters/zeromq.rb
82
+ - lib/logstash/inputs/zeromq.rb
83
+ - lib/logstash/outputs/zeromq.rb
84
+ - lib/logstash/zeromq_mixin.rb
85
+ - lib/logstash_registry.rb
86
+ - logstash-integration-zeromq.gemspec
87
+ - spec/filters/zeromq_spec.rb
88
+ - spec/inputs/zeromq_spec.rb
89
+ - spec/integration/input_spec.rb
90
+ - spec/integration/output_spec.rb
91
+ - spec/outputs/zeromq_spec.rb
92
+ - spec/spec_helper.rb
93
+ - spec/support/client.rb
94
+ - spec/zeromq_spec.rb
95
+ homepage: https://elastic.co
96
+ licenses:
97
+ - Apache License (2.0)
98
+ metadata:
99
+ logstash_plugin: 'true'
100
+ logstash_group: pack
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.4.8
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Logstash integration with ZeroMQ with input filter and output plugins
121
+ test_files:
122
+ - spec/filters/zeromq_spec.rb
123
+ - spec/inputs/zeromq_spec.rb
124
+ - spec/integration/input_spec.rb
125
+ - spec/integration/output_spec.rb
126
+ - spec/outputs/zeromq_spec.rb
127
+ - spec/spec_helper.rb
128
+ - spec/support/client.rb
129
+ - spec/zeromq_spec.rb