logstash-integration-zeromq 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/CONTRIBUTORS +10 -0
- data/DEVELOPER.md +2 -0
- data/Gemfile +3 -0
- data/LICENSE +11 -0
- data/README.md +93 -0
- data/lib/logstash/filters/zeromq.rb +217 -0
- data/lib/logstash/inputs/zeromq.rb +186 -0
- data/lib/logstash/outputs/zeromq.rb +135 -0
- data/lib/logstash/zeromq_mixin.rb +58 -0
- data/lib/logstash_registry.rb +9 -0
- data/logstash-integration-zeromq.gemspec +24 -0
- data/spec/filters/zeromq_spec.rb +56 -0
- data/spec/inputs/zeromq_spec.rb +82 -0
- data/spec/integration/input_spec.rb +37 -0
- data/spec/integration/output_spec.rb +34 -0
- data/spec/outputs/zeromq_spec.rb +34 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/client.rb +48 -0
- data/spec/zeromq_spec.rb +37 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTORS
ADDED
@@ -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.
|
data/DEVELOPER.md
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/spec/zeromq_spec.rb
ADDED
@@ -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
|