logstash-input-tcp 4.2.2-java
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 +64 -0
- data/CONTRIBUTORS +25 -0
- data/Gemfile +11 -0
- data/LICENSE +13 -0
- data/NOTICE.TXT +5 -0
- data/README.md +98 -0
- data/docs/index.asciidoc +205 -0
- data/lib/logstash-input-tcp_jars.rb +4 -0
- data/lib/logstash/inputs/tcp.rb +387 -0
- data/lib/logstash/inputs/tcp/decoder_impl.rb +55 -0
- data/logstash-input-tcp.gemspec +36 -0
- data/spec/inputs/tcp_spec.rb +421 -0
- data/spec/spec_helper.rb +91 -0
- data/vendor/jar-dependencies/org/logstash/inputs/logstash-input-tcp/4.2.2/logstash-input-tcp-4.2.2.jar +0 -0
- data/version +1 -0
- metadata +181 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'java'
|
3
|
+
|
4
|
+
java_import org.logstash.tcp.Decoder
|
5
|
+
|
6
|
+
class DecoderImpl
|
7
|
+
|
8
|
+
include Decoder
|
9
|
+
|
10
|
+
def initialize(codec, tcp)
|
11
|
+
@tcp = tcp
|
12
|
+
@codec = codec
|
13
|
+
@first_read = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(channel_addr, data)
|
17
|
+
bytes = Java::byte[data.readableBytes].new
|
18
|
+
data.getBytes(0, bytes)
|
19
|
+
data.release
|
20
|
+
tbuf = String.from_java_bytes bytes, "ASCII-8BIT"
|
21
|
+
if @first_read
|
22
|
+
tbuf = init_first_read(channel_addr, tbuf)
|
23
|
+
end
|
24
|
+
@tcp.decode_buffer(@address, @port, @codec,
|
25
|
+
@proxy_address, @proxy_port, tbuf)
|
26
|
+
end
|
27
|
+
|
28
|
+
def copy()
|
29
|
+
DecoderImpl.new(@codec.clone, @tcp)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def init_first_read(channel_addr, received)
|
34
|
+
if @tcp.proxy_protocol
|
35
|
+
pp_hdr, filtered = received.split("\r\n", 2)
|
36
|
+
pp_info = pp_hdr.split(/\s/)
|
37
|
+
# PROXY proto clientip proxyip clientport proxyport
|
38
|
+
if pp_info[0] != "PROXY"
|
39
|
+
@tcp.logger.error("invalid proxy protocol header label", :hdr => pp_hdr)
|
40
|
+
raise IOError
|
41
|
+
else
|
42
|
+
@proxy_address = pp_info[3]
|
43
|
+
@proxy_port = pp_info[5]
|
44
|
+
@address = pp_info[2]
|
45
|
+
@port = pp_info[4]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
filtered = received
|
49
|
+
@address = channel_addr.get_address.get_host_address
|
50
|
+
@port = channel_addr.get_port
|
51
|
+
end
|
52
|
+
@first_read = false
|
53
|
+
filtered
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'logstash-input-tcp'
|
3
|
+
s.version = ::File.read('version').split("\n").first
|
4
|
+
s.licenses = ['Apache License (2.0)']
|
5
|
+
s.summary = "Read events over a TCP socket."
|
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 = ["Elastic"]
|
8
|
+
s.email = 'info@elastic.co'
|
9
|
+
s.homepage = "http://www.elastic.co/guide/en/logstash/current/index.html"
|
10
|
+
s.platform = "java"
|
11
|
+
s.require_paths = ["lib", "vendor/jar-dependencies"]
|
12
|
+
|
13
|
+
# Files
|
14
|
+
s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "version", "docs/**/*"]
|
15
|
+
|
16
|
+
# Tests
|
17
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
|
+
|
19
|
+
# Special flag to let us know this is actually a logstash plugin
|
20
|
+
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
21
|
+
|
22
|
+
# Gem dependencies
|
23
|
+
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
|
24
|
+
|
25
|
+
# line vs streaming codecs required for fix_streaming_codecs
|
26
|
+
# TODO: fix_streaming_codecs should be refactored to not
|
27
|
+
# require the codecs to be installed.
|
28
|
+
s.add_runtime_dependency 'logstash-codec-plain'
|
29
|
+
s.add_runtime_dependency 'logstash-codec-line'
|
30
|
+
s.add_runtime_dependency 'logstash-codec-json'
|
31
|
+
s.add_runtime_dependency 'logstash-codec-json_lines'
|
32
|
+
|
33
|
+
s.add_development_dependency 'logstash-devutils'
|
34
|
+
s.add_development_dependency 'flores', '~> 0.0.6'
|
35
|
+
s.add_development_dependency 'stud', '~> 0.0.22'
|
36
|
+
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/devutils/rspec/spec_helper"
|
3
|
+
require "socket"
|
4
|
+
require "timeout"
|
5
|
+
require "logstash/json"
|
6
|
+
require "logstash/inputs/tcp"
|
7
|
+
require "stud/try"
|
8
|
+
require "stud/task"
|
9
|
+
require "flores/pki"
|
10
|
+
require "openssl"
|
11
|
+
|
12
|
+
require_relative "../spec_helper"
|
13
|
+
|
14
|
+
#Cabin::Channel.get(LogStash).subscribe(STDOUT)
|
15
|
+
#Cabin::Channel.get(LogStash).level = :debug
|
16
|
+
describe LogStash::Inputs::Tcp do
|
17
|
+
|
18
|
+
context "codec (PR #1372)" do
|
19
|
+
it "switches from plain to line" do
|
20
|
+
require "logstash/codecs/plain"
|
21
|
+
require "logstash/codecs/line"
|
22
|
+
plugin = LogStash::Inputs::Tcp.new("codec" => LogStash::Codecs::Plain.new, "port" => 0)
|
23
|
+
plugin.register
|
24
|
+
insist { plugin.codec }.is_a?(LogStash::Codecs::Line)
|
25
|
+
plugin.close
|
26
|
+
end
|
27
|
+
it "switches from json to json_lines" do
|
28
|
+
require "logstash/codecs/json"
|
29
|
+
require "logstash/codecs/json_lines"
|
30
|
+
plugin = LogStash::Inputs::Tcp.new("codec" => LogStash::Codecs::JSON.new, "port" => 0)
|
31
|
+
plugin.register
|
32
|
+
insist { plugin.codec }.is_a?(LogStash::Codecs::JSONLines)
|
33
|
+
plugin.close
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should read plain with unicode" do
|
38
|
+
event_count = 10
|
39
|
+
port = rand(1024..65535)
|
40
|
+
conf = <<-CONFIG
|
41
|
+
input {
|
42
|
+
tcp {
|
43
|
+
port => #{port}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
CONFIG
|
47
|
+
|
48
|
+
host = '127.0.0.1'
|
49
|
+
events = input(conf) do |pipeline, queue|
|
50
|
+
socket = Stud::try(5.times) { TCPSocket.new(host, port) }
|
51
|
+
event_count.times do |i|
|
52
|
+
# unicode smiley for testing unicode support!
|
53
|
+
socket.puts("#{i} ☹")
|
54
|
+
socket.flush
|
55
|
+
end
|
56
|
+
socket.close
|
57
|
+
|
58
|
+
event_count.times.collect {queue.pop}
|
59
|
+
end
|
60
|
+
|
61
|
+
insist { events.length } == event_count
|
62
|
+
event_count.times do |i|
|
63
|
+
event = events[i]
|
64
|
+
insist { event.get("message") } == "#{i} ☹"
|
65
|
+
insist { event.get("host") } == host
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should handle PROXY protocol v1 connections" do
|
70
|
+
event_count = 10
|
71
|
+
port = rand(1024..65535)
|
72
|
+
conf = <<-CONFIG
|
73
|
+
input {
|
74
|
+
tcp {
|
75
|
+
port => #{port}
|
76
|
+
proxy_protocol => true
|
77
|
+
}
|
78
|
+
}
|
79
|
+
CONFIG
|
80
|
+
|
81
|
+
events = input(conf) do |pipeline, queue|
|
82
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
83
|
+
socket.puts("PROXY TCP4 1.2.3.4 5.6.7.8 1234 5678\r");
|
84
|
+
socket.flush
|
85
|
+
event_count.times do |i|
|
86
|
+
# unicode smiley for testing unicode support!
|
87
|
+
socket.puts("#{i} ☹")
|
88
|
+
socket.flush
|
89
|
+
end
|
90
|
+
socket.close
|
91
|
+
|
92
|
+
event_count.times.collect {queue.pop}
|
93
|
+
end
|
94
|
+
|
95
|
+
insist { events.length } == event_count
|
96
|
+
event_count.times do |i|
|
97
|
+
insist { events[i].get("message") } == "#{i} ☹"
|
98
|
+
insist { events[i].get("host") } == "1.2.3.4"
|
99
|
+
insist { events[i].get("port") } == "1234"
|
100
|
+
insist { events[i].get("proxy_host") } == "5.6.7.8"
|
101
|
+
insist { events[i].get("proxy_port") } == "5678"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should read events with plain codec and ISO-8859-1 charset" do
|
106
|
+
port = rand(1024..65535)
|
107
|
+
charset = "ISO-8859-1"
|
108
|
+
conf = <<-CONFIG
|
109
|
+
input {
|
110
|
+
tcp {
|
111
|
+
port => #{port}
|
112
|
+
codec => plain { charset => "#{charset}" }
|
113
|
+
}
|
114
|
+
}
|
115
|
+
CONFIG
|
116
|
+
|
117
|
+
event = input(conf) do |pipeline, queue|
|
118
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
119
|
+
text = "\xA3" # the £ symbol in ISO-8859-1 aka Latin-1
|
120
|
+
text.force_encoding("ISO-8859-1")
|
121
|
+
socket.puts(text)
|
122
|
+
socket.close
|
123
|
+
|
124
|
+
queue.pop
|
125
|
+
end
|
126
|
+
|
127
|
+
# Make sure the 0xA3 latin-1 code converts correctly to UTF-8.
|
128
|
+
insist { event.get("message").size } == 1
|
129
|
+
insist { event.get("message").bytesize } == 2
|
130
|
+
insist { event.get("message") } == "£"
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should read events with json codec" do
|
134
|
+
port = rand(1024..65535)
|
135
|
+
conf = <<-CONFIG
|
136
|
+
input {
|
137
|
+
tcp {
|
138
|
+
port => #{port}
|
139
|
+
codec => json
|
140
|
+
}
|
141
|
+
}
|
142
|
+
CONFIG
|
143
|
+
|
144
|
+
data = {
|
145
|
+
"hello" => "world",
|
146
|
+
"foo" => [1,2,3],
|
147
|
+
"baz" => { "1" => "2" },
|
148
|
+
"host" => "example host"
|
149
|
+
}
|
150
|
+
|
151
|
+
event = input(conf) do |pipeline, queue|
|
152
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
153
|
+
socket.puts(LogStash::Json.dump(data))
|
154
|
+
socket.close
|
155
|
+
|
156
|
+
queue.pop
|
157
|
+
end
|
158
|
+
|
159
|
+
insist { event.get("hello") } == data["hello"]
|
160
|
+
insist { event.get("foo").to_a } == data["foo"] # to_a to cast Java ArrayList produced by JrJackson
|
161
|
+
insist { event.get("baz") } == data["baz"]
|
162
|
+
|
163
|
+
# Make sure the tcp input, w/ json codec, uses the event's 'host' value,
|
164
|
+
# if present, instead of providing its own
|
165
|
+
insist { event.get("host") } == data["host"]
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should read events with json codec (testing 'host' handling)" do
|
169
|
+
port = rand(1024..65535)
|
170
|
+
conf = <<-CONFIG
|
171
|
+
input {
|
172
|
+
tcp {
|
173
|
+
port => #{port}
|
174
|
+
codec => json
|
175
|
+
}
|
176
|
+
}
|
177
|
+
CONFIG
|
178
|
+
|
179
|
+
data = {
|
180
|
+
"hello" => "world"
|
181
|
+
}
|
182
|
+
|
183
|
+
event = input(conf) do |pipeline, queue|
|
184
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
185
|
+
socket.puts(LogStash::Json.dump(data))
|
186
|
+
socket.close
|
187
|
+
|
188
|
+
queue.pop
|
189
|
+
end
|
190
|
+
|
191
|
+
insist { event.get("hello") } == data["hello"]
|
192
|
+
insist { event }.include?("host")
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should read events with json_lines codec" do
|
196
|
+
port = rand(1024..65535)
|
197
|
+
conf = <<-CONFIG
|
198
|
+
input {
|
199
|
+
tcp {
|
200
|
+
port => #{port}
|
201
|
+
codec => json_lines
|
202
|
+
}
|
203
|
+
}
|
204
|
+
CONFIG
|
205
|
+
|
206
|
+
data = {
|
207
|
+
"hello" => "world",
|
208
|
+
"foo" => [1,2,3],
|
209
|
+
"baz" => { "1" => "2" },
|
210
|
+
"idx" => 0
|
211
|
+
}
|
212
|
+
event_count = 5
|
213
|
+
|
214
|
+
events = input(conf) do |pipeline, queue|
|
215
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
216
|
+
(1..event_count).each do |idx|
|
217
|
+
data["idx"] = idx
|
218
|
+
socket.puts(LogStash::Json.dump(data) + "\n")
|
219
|
+
end
|
220
|
+
socket.close
|
221
|
+
|
222
|
+
(1..event_count).map{queue.pop}
|
223
|
+
end
|
224
|
+
|
225
|
+
events.each_with_index do |event, idx|
|
226
|
+
insist { event.get("hello") } == data["hello"]
|
227
|
+
insist { event.get("foo").to_a } == data["foo"] # to_a to cast Java ArrayList produced by JrJackson
|
228
|
+
insist { event.get("baz") } == data["baz"]
|
229
|
+
insist { event.get("idx") } == idx + 1
|
230
|
+
end # do
|
231
|
+
end # describe
|
232
|
+
|
233
|
+
it "should one message per connection" do
|
234
|
+
event_count = 10
|
235
|
+
port = rand(1024..65535)
|
236
|
+
conf = <<-CONFIG
|
237
|
+
input {
|
238
|
+
tcp {
|
239
|
+
port => #{port}
|
240
|
+
}
|
241
|
+
}
|
242
|
+
CONFIG
|
243
|
+
|
244
|
+
events = input(conf) do |pipeline, queue|
|
245
|
+
event_count.times do |i|
|
246
|
+
socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
|
247
|
+
socket.puts("#{i}")
|
248
|
+
socket.flush
|
249
|
+
socket.close
|
250
|
+
end
|
251
|
+
|
252
|
+
# since each message is sent on its own tcp connection & thread, exact receiving order cannot be garanteed
|
253
|
+
event_count.times.collect{queue.pop}.sort_by{|event| event.get("message")}
|
254
|
+
end
|
255
|
+
|
256
|
+
event_count.times do |i|
|
257
|
+
insist { events[i].get("message") } == "#{i}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# below are new specs added in the context of the shutdown semantic refactor.
|
262
|
+
# TODO:
|
263
|
+
# - refactor all specs using this new model
|
264
|
+
# - pipelineless_input has been basically copied from the udp input specs, it should be DRYied up
|
265
|
+
# - see if we should miminc the udp input UDPClient helper class instead of directly using TCPSocket
|
266
|
+
|
267
|
+
describe "LogStash::Inputs::Tcp new specs style" do
|
268
|
+
|
269
|
+
before do
|
270
|
+
srand(RSpec.configuration.seed)
|
271
|
+
end
|
272
|
+
|
273
|
+
let(:port) { rand(1024..65535) }
|
274
|
+
subject { LogStash::Plugin.lookup("input", "tcp").new({ "port" => port }) }
|
275
|
+
let!(:helper) { TcpHelpers.new }
|
276
|
+
|
277
|
+
after :each do
|
278
|
+
subject.close rescue nil
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "#register" do
|
282
|
+
it "should register without errors" do
|
283
|
+
expect { subject.register }.to_not raise_error
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "#receive" do
|
288
|
+
shared_examples "receiving events" do
|
289
|
+
# TODO(sissel): Implement normal event-receipt tests as as a shared example
|
290
|
+
end
|
291
|
+
|
292
|
+
context "when ssl_enable is true" do
|
293
|
+
let(:pki) { Flores::PKI.generate }
|
294
|
+
let(:certificate) { pki[0] }
|
295
|
+
let(:key) { pki[1] }
|
296
|
+
let(:certificate_file) { Stud::Temporary.file }
|
297
|
+
let(:key_file) { Stud::Temporary.file }
|
298
|
+
let(:queue) { Queue.new }
|
299
|
+
|
300
|
+
let(:config) do
|
301
|
+
{
|
302
|
+
"host" => "127.0.0.1",
|
303
|
+
"port" => port,
|
304
|
+
"ssl_enable" => true,
|
305
|
+
"ssl_cert" => certificate_file.path,
|
306
|
+
"ssl_key" => key_file.path,
|
307
|
+
|
308
|
+
# Trust our self-signed cert.
|
309
|
+
# TODO(sissel): Make this a separate certificate for the client
|
310
|
+
"ssl_extra_chain_certs" => certificate_file.path
|
311
|
+
}
|
312
|
+
end
|
313
|
+
|
314
|
+
subject(:input) { LogStash::Plugin.lookup("input", "tcp").new(config) }
|
315
|
+
|
316
|
+
before do
|
317
|
+
certificate_file.write(certificate)
|
318
|
+
key_file.write(key)
|
319
|
+
|
320
|
+
# Close to flush the file writes.
|
321
|
+
certificate_file.close
|
322
|
+
key_file.close
|
323
|
+
subject.register
|
324
|
+
end
|
325
|
+
|
326
|
+
after do
|
327
|
+
File.unlink(certificate_file.path)
|
328
|
+
File.unlink(key_file.path)
|
329
|
+
end
|
330
|
+
|
331
|
+
context "with a poorly-behaving client" do
|
332
|
+
let!(:input_task) { Stud::Task.new { input.run(queue) } }
|
333
|
+
|
334
|
+
after { input.close }
|
335
|
+
|
336
|
+
context "that disconnects before doing TLS handshake" do
|
337
|
+
before do
|
338
|
+
client = TCPSocket.new("127.0.0.1", port)
|
339
|
+
client.close
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should not negatively impact the plugin" do
|
343
|
+
# TODO(sissel): Look for a better way to detect this failure
|
344
|
+
# besides a sleep/wait.
|
345
|
+
result = input_task.thread.join(0.5)
|
346
|
+
expect(result).to be_nil
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context "that sends garbage instead of TLS handshake" do
|
351
|
+
let!(:input_task) { Stud::Task.new { input.run(queue) } }
|
352
|
+
let(:max_length) { 1000 }
|
353
|
+
let(:garbage) { Flores::Random.iterations(max_length).collect { Flores::Random.integer(1...255) }.pack("C*") }
|
354
|
+
before do
|
355
|
+
# Assertion to verify this test is actually sending something.
|
356
|
+
expect(garbage.length).to be > 0
|
357
|
+
|
358
|
+
client = TCPSocket.new("127.0.0.1", port)
|
359
|
+
client.write(garbage)
|
360
|
+
client.flush
|
361
|
+
Thread.new { sleep(1); client.close }
|
362
|
+
end
|
363
|
+
it "should not negatively impact the plugin" do
|
364
|
+
# TODO(sissel): Look for a better way to detect this failure besides a sleep/wait.
|
365
|
+
result = input_task.thread.join(0.5)
|
366
|
+
expect(result).to be_nil
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
context "connection was healthy but now has garbage or corruption" do
|
371
|
+
let!(:input_task) { Stud::Task.new { input.run(queue) } }
|
372
|
+
let(:tcp) { TCPSocket.new("127.0.0.1", port) }
|
373
|
+
let(:sslcontext) { OpenSSL::SSL::SSLContext.new }
|
374
|
+
let(:sslsocket) { OpenSSL::SSL::SSLSocket.new(tcp, sslcontext) }
|
375
|
+
let(:max_length) { 1000 }
|
376
|
+
let(:garbage) { Flores::Random.iterations(max_length).collect { Flores::Random.integer(1...255) }.pack("C*") }
|
377
|
+
|
378
|
+
before do
|
379
|
+
sslcontext.cert = certificate
|
380
|
+
sslcontext.key = key
|
381
|
+
sslcontext.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
382
|
+
|
383
|
+
sslsocket.connect
|
384
|
+
sslsocket.write("Hello world\n")
|
385
|
+
|
386
|
+
# Assertion to verify this test is actually sending something.
|
387
|
+
expect(garbage.length).to be > 0
|
388
|
+
tcp.write(garbage)
|
389
|
+
tcp.flush
|
390
|
+
sslsocket.close
|
391
|
+
tcp.close
|
392
|
+
end
|
393
|
+
|
394
|
+
it "should not negatively impact the plugin" do
|
395
|
+
# TODO(sissel): Look for a better way to detect this failure besides a sleep/wait.
|
396
|
+
result = input_task.thread.join(0.5)
|
397
|
+
expect(result).to be_nil
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# TODO(sissel): Spec multiple clients where only one is bad.
|
403
|
+
|
404
|
+
context "with client certificate problems" do
|
405
|
+
context "using an expired certificate"
|
406
|
+
context "using an untrusted certificate"
|
407
|
+
end
|
408
|
+
|
409
|
+
context "with a good connection" do
|
410
|
+
# TODO(sissel): use shared example
|
411
|
+
include_examples "receiving events"
|
412
|
+
end
|
413
|
+
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
it_behaves_like "an interruptible input plugin" do
|
418
|
+
let(:config) { { "port" => port } }
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|