logstash-input-burrow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +11 -0
- data/LICENSE +202 -0
- data/README.md +50 -0
- data/docs/index.asciidoc +362 -0
- data/lib/logstash/inputs/burrow.rb +280 -0
- data/logstash-input-burrow.gemspec +32 -0
- data/spec/inputs/burrow_spec.rb +281 -0
- metadata +208 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/inputs/base"
|
3
|
+
require "logstash/namespace"
|
4
|
+
require "logstash/plugin_mixins/http_client"
|
5
|
+
require "socket" # for Socket.gethostname
|
6
|
+
require "manticore"
|
7
|
+
require "rufus/scheduler"
|
8
|
+
|
9
|
+
class LogStash::Inputs::Burrow < LogStash::Inputs::Base
|
10
|
+
include LogStash::PluginMixins::HttpClient
|
11
|
+
|
12
|
+
config_name "burrow"
|
13
|
+
|
14
|
+
# The Burrow Client configuration, with at least url
|
15
|
+
# client_config => {
|
16
|
+
# url => "http://localhost:8000"
|
17
|
+
# }
|
18
|
+
config :client_config, :validate => :hash, :required => true
|
19
|
+
# The Burrow API version
|
20
|
+
config :api_version, :default => "v3"
|
21
|
+
# Schedule of when to periodically poll from the url
|
22
|
+
# Format: A hash with
|
23
|
+
# + key: "cron" | "every" | "in" | "at"
|
24
|
+
# + value: string
|
25
|
+
# Examples:
|
26
|
+
# a) { "every" => "1h" }
|
27
|
+
# b) { "cron" => "* * * * * UTC" }
|
28
|
+
# See: rufus/scheduler for details about different schedule options and value string format
|
29
|
+
config :schedule, :validate => :hash, :default => %w(every 30s)
|
30
|
+
|
31
|
+
default :codec, "json"
|
32
|
+
|
33
|
+
Schedule_types = %w(cron every at in)
|
34
|
+
|
35
|
+
def register
|
36
|
+
@host = Socket.gethostname.force_encoding(Encoding::UTF_8)
|
37
|
+
|
38
|
+
@logger.info("Registering burrow Input", :type => @type, :schedule => @schedule, :timeout => @timeout)
|
39
|
+
|
40
|
+
if @api_version != "v3"
|
41
|
+
raise LogStash::ConfigurationError, "at the moment only Burrow API version v3 is supported."
|
42
|
+
end
|
43
|
+
|
44
|
+
if @codec.class.config_name != "json"
|
45
|
+
raise LogStash::ConfigurationError, "this plugin needs codec to be json."
|
46
|
+
end
|
47
|
+
|
48
|
+
setup_request!
|
49
|
+
end
|
50
|
+
|
51
|
+
def stop
|
52
|
+
Stud.stop!(@interval_thread) if @interval_thread
|
53
|
+
@scheduler.stop if @scheduler
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def setup_request!
|
59
|
+
@request = normalize_request(client_config)
|
60
|
+
end
|
61
|
+
|
62
|
+
def normalize_request(client_spec)
|
63
|
+
if client_spec.is_a?(String)
|
64
|
+
res = [client_spec + ('/' unless client_spec.end_with?('/')) + @api_version << "/kafka"]
|
65
|
+
elsif client_spec.is_a?(Hash)
|
66
|
+
# The client will expect keys / values
|
67
|
+
spec = Hash[client_spec.clone.map {|k, v| [k.to_sym, v]}] # symbolize keys
|
68
|
+
|
69
|
+
# method and url aren't really part of the options, so we pull them out
|
70
|
+
spec.delete(:method)
|
71
|
+
url = spec.delete(:url)
|
72
|
+
|
73
|
+
raise LogStash::ConfigurationError, "Invalid URL #{url}" unless URI::DEFAULT_PARSER.regexp[:ABS_URI].match(url)
|
74
|
+
|
75
|
+
# Manticore wants auth options that are like {:auth => {:user => u, :pass => p}}
|
76
|
+
# We allow that because earlier versions of this plugin documented that as the main way to
|
77
|
+
# to do things, but now prefer top level "user", and "password" options
|
78
|
+
# So, if the top level user/password are defined they are moved to the :auth key for manticore
|
79
|
+
# if those attributes are already in :auth they still need to be transformed to symbols
|
80
|
+
auth = spec[:auth]
|
81
|
+
user = spec.delete(:user) || (auth && auth["user"])
|
82
|
+
password = spec.delete(:password) || (auth && auth["password"])
|
83
|
+
|
84
|
+
if user.nil? ^ password.nil?
|
85
|
+
raise LogStash::ConfigurationError, "'user' and 'password' must both be specified for input HTTP poller!"
|
86
|
+
end
|
87
|
+
|
88
|
+
if user && password
|
89
|
+
spec[:auth] = {
|
90
|
+
user: user,
|
91
|
+
pass: password,
|
92
|
+
eager: true
|
93
|
+
}
|
94
|
+
end
|
95
|
+
res = [url + ('/' unless url.end_with?('/')) + @api_version << "/kafka", spec]
|
96
|
+
else
|
97
|
+
raise LogStash::ConfigurationError, "Invalid URL or request spec: '#{client_spec}', expected a String or Hash!"
|
98
|
+
end
|
99
|
+
|
100
|
+
validate_request!(res)
|
101
|
+
res
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def validate_request!(request)
|
107
|
+
url, spec = request
|
108
|
+
|
109
|
+
raise LogStash::ConfigurationError, "Invalid URL #{url}" unless URI::DEFAULT_PARSER.regexp[:ABS_URI].match(url)
|
110
|
+
|
111
|
+
raise LogStash::ConfigurationError, "No URL provided for request! #{url}" unless url
|
112
|
+
if spec && spec[:auth]
|
113
|
+
unless spec[:auth][:user]
|
114
|
+
raise LogStash::ConfigurationError, "Auth was specified, but 'user' was not!"
|
115
|
+
end
|
116
|
+
unless spec[:auth][:pass]
|
117
|
+
raise LogStash::ConfigurationError, "Auth was specified, but 'password' was not!"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
request
|
122
|
+
end
|
123
|
+
|
124
|
+
public
|
125
|
+
|
126
|
+
def run(queue)
|
127
|
+
setup_schedule(queue)
|
128
|
+
end
|
129
|
+
|
130
|
+
def setup_schedule(queue)
|
131
|
+
#schedule hash must contain exactly one of the allowed keys
|
132
|
+
msg_invalid_schedule = "Invalid config. schedule hash must contain " +
|
133
|
+
"exactly one of the following keys - cron, at, every or in"
|
134
|
+
raise Logstash::ConfigurationError, msg_invalid_schedule if @schedule.keys.length != 1
|
135
|
+
schedule_type = @schedule.keys.first
|
136
|
+
schedule_value = @schedule[schedule_type]
|
137
|
+
raise LogStash::ConfigurationError, msg_invalid_schedule unless Schedule_types.include?(schedule_type)
|
138
|
+
|
139
|
+
@scheduler = Rufus::Scheduler.new(:max_work_threads => 1)
|
140
|
+
#as of v3.0.9, :first_in => :now doesn't work. Use the following workaround instead
|
141
|
+
opts = schedule_type == "every" ? {:first_in => 0.01} : {}
|
142
|
+
@scheduler.send(schedule_type, schedule_value, opts) {run_once(queue)}
|
143
|
+
@scheduler.join
|
144
|
+
end
|
145
|
+
|
146
|
+
def run_once(queue)
|
147
|
+
request(queue, @request)
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def request(queue, request)
|
154
|
+
@logger.debug? && @logger.debug?("Fetching URL", :client_config => request)
|
155
|
+
started = Time.now
|
156
|
+
|
157
|
+
url, spec = request
|
158
|
+
client.get(url, spec).
|
159
|
+
on_success do |cluster_response|
|
160
|
+
body = cluster_response.body
|
161
|
+
if body && body.size > 0
|
162
|
+
@codec.decode(body) do |clusters|
|
163
|
+
cluster_list = clusters.get('clusters')
|
164
|
+
@logger.debug? && @logger.debug?("found clusters", :cluster_list => cluster_list)
|
165
|
+
cluster_list.each {|cluster|
|
166
|
+
consumer_url = url + "/" << cluster + "/consumer"
|
167
|
+
client.get(consumer_url, spec).
|
168
|
+
on_success do |consumers_response|
|
169
|
+
body = consumers_response.body
|
170
|
+
if body && body.size > 0
|
171
|
+
@codec.decode(body) do |consumers|
|
172
|
+
consumer_list = consumers.get('consumers')
|
173
|
+
@logger.debug? && @logger.debug?("found consumers", :consumer_list => consumer_list)
|
174
|
+
consumer_list.each {|consumer|
|
175
|
+
lag_url = url + "/" + cluster + "/consumer" + "/" + consumer + "/lag"
|
176
|
+
client.get(lag_url, spec).
|
177
|
+
on_success do |consumer_lag_response|
|
178
|
+
handle_success(queue, [lag_url, spec], consumer_lag_response, Time.now - started)
|
179
|
+
end.
|
180
|
+
on_failure do |exception|
|
181
|
+
handle_failure(queue, [lag_url, spec], exception, Time.now - started)
|
182
|
+
end.call
|
183
|
+
} unless consumer_list.nil?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end.
|
187
|
+
on_failure do |exception|
|
188
|
+
handle_failure(queue, [consumer_url, spec], exception, Time.now - started)
|
189
|
+
end.call
|
190
|
+
} unless cluster_list.nil?
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end.
|
194
|
+
on_failure do |exception|
|
195
|
+
handle_failure(queue, request, exception, Time.now - started)
|
196
|
+
end.call
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def handle_success(queue, request, response, execution_time)
|
202
|
+
body = response.body
|
203
|
+
# If there is a usable response. HEAD request are `nil` and empty get
|
204
|
+
# responses come up as "" which will cause the codec to not yield anything
|
205
|
+
if body && body.size > 0
|
206
|
+
decode_and_flush(@codec, body) do |decoded|
|
207
|
+
event = decoded
|
208
|
+
handle_decoded_event(queue, request, response, event, execution_time)
|
209
|
+
end
|
210
|
+
else
|
211
|
+
event = ::LogStash::Event.new
|
212
|
+
handle_decoded_event(queue, request, response, event, execution_time)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def decode_and_flush(codec, body, &yielder)
|
219
|
+
codec.decode(body, &yielder)
|
220
|
+
codec.flush(&yielder)
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def handle_decoded_event(queue, request, response, event, _execution_time)
|
226
|
+
decorate(event)
|
227
|
+
queue << event
|
228
|
+
rescue StandardError, java.lang.Exception => e
|
229
|
+
@logger.error? && @logger.error("Error eventifying response!",
|
230
|
+
:exception => e,
|
231
|
+
:exception_message => e.message,
|
232
|
+
:client_config => request,
|
233
|
+
:response => response
|
234
|
+
)
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
# Beware, on old versions of manticore some uncommon failures are not handled
|
240
|
+
def handle_failure(queue, request, exception, execution_time)
|
241
|
+
event = LogStash::Event.new
|
242
|
+
|
243
|
+
event.tag("_http_request_failure")
|
244
|
+
|
245
|
+
# This is also in the metadata, but we send it anyone because we want this
|
246
|
+
# persisted by default, whereas metadata isn't. People don't like mysterious errors
|
247
|
+
event.set("http_request_failure", {
|
248
|
+
"request" => structure_request(request),
|
249
|
+
"error" => exception.to_s,
|
250
|
+
"backtrace" => exception.backtrace,
|
251
|
+
"runtime_seconds" => execution_time
|
252
|
+
})
|
253
|
+
|
254
|
+
queue << event
|
255
|
+
rescue StandardError, java.lang.Exception => e
|
256
|
+
@logger.error? && @logger.error("Cannot read URL or send the error as an event!",
|
257
|
+
:exception => e,
|
258
|
+
:exception_message => e.message,
|
259
|
+
:exception_backtrace => e.backtrace)
|
260
|
+
|
261
|
+
# If we are running in debug mode we can display more information about the
|
262
|
+
# specific request which could give more details about the connection.
|
263
|
+
@logger.debug? && @logger.debug("Cannot read URL or send the error as an event!",
|
264
|
+
:exception => e,
|
265
|
+
:exception_message => e.message,
|
266
|
+
:exception_backtrace => e.backtrace,
|
267
|
+
:client_config => request)
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
# Turn [method, url, spec] request into a hash for friendlier logging / ES indexing
|
273
|
+
def structure_request(request)
|
274
|
+
url, spec = request
|
275
|
+
# Flatten everything into the 'spec' hash, also stringify any keys to normalize
|
276
|
+
Hash[(spec || {}).merge({
|
277
|
+
"url" => url,
|
278
|
+
}).map {|k, v| [k.to_s, v]}]
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'logstash-input-burrow'
|
3
|
+
s.version = '1.0.0'
|
4
|
+
s.licenses = ['Apache License (2.0)']
|
5
|
+
s.summary = "Decodes the output of Burrow HTTP API into events"
|
6
|
+
s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
|
7
|
+
s.authors = [ "markush81"]
|
8
|
+
s.email = 'info@markushelbig.de'
|
9
|
+
s.homepage = "https://github.com/markush81/logstash-input-burrow"
|
10
|
+
s.require_paths = ["lib"]
|
11
|
+
|
12
|
+
# Files
|
13
|
+
s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "VERSION", "docs/**/*"]
|
14
|
+
# Tests
|
15
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
|
+
|
17
|
+
# Special flag to let us know this is actually a logstash plugin
|
18
|
+
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
19
|
+
|
20
|
+
# Gem dependencies
|
21
|
+
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
|
22
|
+
s.add_runtime_dependency 'logstash-codec-plain'
|
23
|
+
s.add_runtime_dependency 'logstash-mixin-http_client', ">= 6.0.0", "< 7.0.0"
|
24
|
+
s.add_runtime_dependency 'stud', "~> 0.0.22"
|
25
|
+
s.add_runtime_dependency 'rufus-scheduler', "~>3.0.9"
|
26
|
+
|
27
|
+
s.add_development_dependency 'logstash-codec-json'
|
28
|
+
s.add_development_dependency 'logstash-codec-line'
|
29
|
+
s.add_development_dependency 'logstash-devutils'
|
30
|
+
s.add_development_dependency 'flores'
|
31
|
+
s.add_development_dependency 'timecop'
|
32
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
require "logstash/devutils/rspec/spec_helper"
|
2
|
+
require 'logstash/inputs/burrow'
|
3
|
+
require 'flores/random'
|
4
|
+
require "timecop"
|
5
|
+
# Workaround for the bug reported in https://github.com/jruby/jruby/issues/4637
|
6
|
+
require 'rspec/matchers/built_in/raise_error.rb'
|
7
|
+
|
8
|
+
describe LogStash::Inputs::Burrow do
|
9
|
+
let(:queue) {Queue.new}
|
10
|
+
let(:default_schedule) {
|
11
|
+
{"every" => "30s"}
|
12
|
+
}
|
13
|
+
|
14
|
+
let(:default_url) {"http://localhost:8000"}
|
15
|
+
let(:default_client_config) {{"url" => default_url}}
|
16
|
+
let(:default_opts) {
|
17
|
+
{
|
18
|
+
"schedule" => default_schedule,
|
19
|
+
"client_config" => default_client_config
|
20
|
+
}
|
21
|
+
}
|
22
|
+
let(:klass) {LogStash::Inputs::Burrow}
|
23
|
+
|
24
|
+
describe "instances" do
|
25
|
+
subject {klass.new(default_opts)}
|
26
|
+
|
27
|
+
before do
|
28
|
+
subject.register
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#run" do
|
32
|
+
it "should setup a scheduler" do
|
33
|
+
runner = Thread.new do
|
34
|
+
subject.run(double("queue"))
|
35
|
+
expect(subject.instance_variable_get("@scheduler")).to be_a_kind_of(Rufus::Scheduler)
|
36
|
+
end
|
37
|
+
runner.kill
|
38
|
+
runner.join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#run_once" do
|
43
|
+
it "should issue an request for each url" do
|
44
|
+
normalized_request = subject.send(:normalize_request, default_client_config)
|
45
|
+
expect(subject).to receive(:request).with(queue, normalized_request).once
|
46
|
+
|
47
|
+
subject.send(:run_once, queue) # :run_once is a private method
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "normalizing a request spec" do
|
52
|
+
shared_examples "a normalized request" do
|
53
|
+
it "should set the options to the URL string" do
|
54
|
+
expect(normalized[0]).to eql(spec_url + "/v3/kafka")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
let(:normalized) {subject.send(:normalize_request, client_config)}
|
59
|
+
|
60
|
+
describe "a string URL" do
|
61
|
+
let(:client_config) {default_client_config}
|
62
|
+
let(:spec_url) {default_url}
|
63
|
+
|
64
|
+
include_examples("a normalized request")
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "URL specs" do
|
68
|
+
|
69
|
+
context "missing an URL" do
|
70
|
+
let(:client_config) {}
|
71
|
+
|
72
|
+
it "should raise an error" do
|
73
|
+
expect {normalized}.to raise_error(LogStash::ConfigurationError)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
shared_examples "auth" do
|
78
|
+
context "with auth enabled but no pass" do
|
79
|
+
let(:auth) {{"user" => "foo"}}
|
80
|
+
|
81
|
+
it "should raise an error" do
|
82
|
+
expect {normalized}.to raise_error(LogStash::ConfigurationError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "with auth enabled, a path, but no user" do
|
87
|
+
let(:client_config) {{"auth" => {"password" => "bar"}}}
|
88
|
+
it "should raise an error" do
|
89
|
+
expect {normalized}.to raise_error(LogStash::ConfigurationError)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
context "with auth enabled correctly" do
|
93
|
+
let(:auth) {{"user" => "foo", "password" => "bar"}}
|
94
|
+
|
95
|
+
it "should raise an error" do
|
96
|
+
expect {normalized}.not_to raise_error
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should properly set the auth parameter" do
|
100
|
+
expect(normalized[1][:auth]).to eq({
|
101
|
+
:user => auth["user"],
|
102
|
+
:pass => auth["password"],
|
103
|
+
:eager => true
|
104
|
+
})
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# The new 'right' way to do things
|
110
|
+
describe "auth with direct auth options" do
|
111
|
+
let(:client_config) {{"url" => "http://localhost", "user" => auth["user"], "password" => auth["password"]}}
|
112
|
+
|
113
|
+
include_examples("auth")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#structure_request" do
|
119
|
+
it "Should turn a simple request into the expected structured request" do
|
120
|
+
expected = {"url" => "http://example.net"}
|
121
|
+
expect(subject.send(:structure_request, ["http://example.net"])).to eql(expected)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should turn a complex request into the expected structured one" do
|
125
|
+
headers = {
|
126
|
+
"X-Fry" => " Like a balloon, and... something bad happens! "
|
127
|
+
}
|
128
|
+
expected = {
|
129
|
+
"url" => "http://example.net",
|
130
|
+
"headers" => headers
|
131
|
+
}
|
132
|
+
expect(subject.send(:structure_request, ["http://example.net", {"headers" => headers}])).to eql(expected)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "events" do
|
138
|
+
|
139
|
+
shared_examples "unprocessable_requests" do
|
140
|
+
let(:burow) {LogStash::Inputs::Burrow.new(settings)}
|
141
|
+
subject(:event) {
|
142
|
+
burow.send(:run_once, queue)
|
143
|
+
queue.pop(true)
|
144
|
+
}
|
145
|
+
|
146
|
+
before do
|
147
|
+
burow.register
|
148
|
+
allow(burow).to receive(:handle_failure).and_call_original
|
149
|
+
allow(burow).to receive(:handle_success)
|
150
|
+
event # materialize the subject
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should enqueue a message" do
|
154
|
+
expect(event).to be_a(LogStash::Event)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should enqueue a message with 'http_request_failure' set" do
|
158
|
+
expect(event.get("http_request_failure")).to be_a(Hash)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should tag the event with '_http_request_failure'" do
|
162
|
+
expect(event.get("tags")).to include('_http_request_failure')
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should invoke handle failure exactly once" do
|
166
|
+
expect(burow).to have_received(:handle_failure).once
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should not invoke handle success at all" do
|
170
|
+
expect(burow).not_to have_received(:handle_success)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context "with a non responsive server" do
|
175
|
+
context "due to a non-existant host" do # Fail with handlers
|
176
|
+
let(:url) {"http://thouetnhoeu89ueoueohtueohtneuohn"}
|
177
|
+
let(:code) {nil} # no response expected
|
178
|
+
|
179
|
+
let(:settings) {default_opts.merge("client_config" => {"url" => url})}
|
180
|
+
|
181
|
+
include_examples("unprocessable_requests")
|
182
|
+
end
|
183
|
+
|
184
|
+
context "due to a bogus port number" do # fail with return?
|
185
|
+
let(:invalid_port) {Flores::Random.integer(65536..1000000)}
|
186
|
+
|
187
|
+
let(:url) {"http://127.0.0.1:#{invalid_port}"}
|
188
|
+
let(:settings) {default_opts.merge("client_config" => {"url" => url})}
|
189
|
+
let(:code) {nil} # No response expected
|
190
|
+
|
191
|
+
include_examples("unprocessable_requests")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "a valid request and decoded response" do
|
196
|
+
let(:cluster) {{"clusters" => ["default"]}}
|
197
|
+
let(:consumer) {{"consumers" => ["console-1"]}}
|
198
|
+
let(:lag) {{"message" => "consumer status returned"}}
|
199
|
+
|
200
|
+
let(:opts) {default_opts}
|
201
|
+
let(:instance) {
|
202
|
+
klass.new(opts)
|
203
|
+
}
|
204
|
+
let(:code) {202}
|
205
|
+
|
206
|
+
subject(:event) {
|
207
|
+
queue.pop(true)
|
208
|
+
}
|
209
|
+
|
210
|
+
before do
|
211
|
+
instance.register
|
212
|
+
instance.client.stub(default_url + "/v3/kafka",
|
213
|
+
:body => LogStash::Json.dump(cluster),
|
214
|
+
:code => code)
|
215
|
+
|
216
|
+
instance.client.stub(default_url + "/v3/kafka/default/consumer",
|
217
|
+
:body => LogStash::Json.dump(consumer),
|
218
|
+
:code => code)
|
219
|
+
|
220
|
+
instance.client.stub(default_url + "/v3/kafka/default/consumer/console-1/lag",
|
221
|
+
:body => LogStash::Json.dump(lag),
|
222
|
+
:code => code)
|
223
|
+
|
224
|
+
allow(instance).to receive(:decorate)
|
225
|
+
instance.send(:run_once, queue)
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should have a matching message" do
|
229
|
+
expect(event.to_hash).to include(lag)
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should decorate the event" do
|
233
|
+
expect(instance).to have_received(:decorate).once
|
234
|
+
end
|
235
|
+
|
236
|
+
context "with empty cluster" do
|
237
|
+
let(:cluster) {{ "clusters" => [] }}
|
238
|
+
|
239
|
+
it "should have no event" do
|
240
|
+
expect(instance).to have_received(:decorate).exactly(0).times
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
context "with empty consumers" do
|
245
|
+
let(:consumer) {{ "consumers" => [] }}
|
246
|
+
|
247
|
+
it "should have no event" do
|
248
|
+
expect(instance).to have_received(:decorate).exactly(0).times
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context "with a complex URL spec" do
|
253
|
+
let(:client_config) {
|
254
|
+
{
|
255
|
+
"url" => default_url,
|
256
|
+
"headers" => {
|
257
|
+
"X-Fry" => "I'm having one of those things, like a headache, with pictures..."
|
258
|
+
}
|
259
|
+
}
|
260
|
+
}
|
261
|
+
let(:opts) {
|
262
|
+
{
|
263
|
+
"schedule" => {
|
264
|
+
"cron" => "* * * * * UTC"
|
265
|
+
},
|
266
|
+
"client_config" => client_config
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
it "should have a matching message" do
|
271
|
+
expect(event.to_hash).to include(lag)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe "stopping" do
|
278
|
+
let(:config) {default_opts}
|
279
|
+
it_behaves_like "an interruptible input plugin"
|
280
|
+
end
|
281
|
+
end
|