logstash-input-beats 2.0.3 → 2.1.1
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/logstash/inputs/beats.rb +115 -101
- data/lib/logstash/{circuit_breaker.rb → inputs/beats_support/circuit_breaker.rb} +16 -10
- data/lib/logstash/inputs/beats_support/codec_callback_listener.rb +26 -0
- data/lib/logstash/inputs/beats_support/connection_handler.rb +79 -0
- data/lib/logstash/inputs/beats_support/decoded_event_transform.rb +34 -0
- data/lib/logstash/inputs/beats_support/event_transform_common.rb +40 -0
- data/lib/logstash/inputs/beats_support/raw_event_transform.rb +18 -0
- data/lib/logstash/inputs/beats_support/synchronous_queue_with_offer.rb +36 -0
- data/lib/lumberjack/beats/server.rb +58 -11
- data/logstash-input-beats.gemspec +4 -3
- data/spec/inputs/beats_spec.rb +35 -126
- data/spec/{logstash → inputs/beats_support}/circuit_breaker_spec.rb +11 -10
- data/spec/inputs/beats_support/codec_callback_listener_spec.rb +52 -0
- data/spec/inputs/beats_support/connection_handler_spec.rb +93 -0
- data/spec/inputs/beats_support/decoded_event_transform_spec.rb +67 -0
- data/spec/inputs/beats_support/event_transform_common_spec.rb +11 -0
- data/spec/inputs/beats_support/raw_event_transform_spec.rb +26 -0
- data/spec/integration_spec.rb +22 -12
- data/spec/lumberjack/beats/server_spec.rb +3 -3
- data/spec/support/logstash_test.rb +25 -0
- data/spec/support/shared_examples.rb +56 -0
- metadata +74 -45
- data/lib/logstash/sized_queue_timeout.rb +0 -64
- data/spec/logstash/size_queue_timeout_spec.rb +0 -100
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/inputs/beats_support/event_transform_common"
|
3
|
+
module LogStash::Inputs::BeatsSupport
|
4
|
+
# Take the extracted content from the codec, merged with the other data coming
|
5
|
+
# from beats, apply the configured tags, normalize the host and try to coerce
|
6
|
+
# the timestamp if it was provided in the hash.
|
7
|
+
class DecodedEventTransform < EventTransformCommon
|
8
|
+
def transform(event, hash)
|
9
|
+
ts = coerce_ts(hash.delete("@timestamp"))
|
10
|
+
|
11
|
+
event["@timestamp"] = ts unless ts.nil?
|
12
|
+
hash.each { |k, v| event[k] = v }
|
13
|
+
super(event)
|
14
|
+
event.tag("beats_input_codec_#{@input.codec.base_codec.class.config_name}_applied")
|
15
|
+
event
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def coerce_ts(ts)
|
20
|
+
return nil if ts.nil?
|
21
|
+
timestamp = LogStash::Timestamp.coerce(ts)
|
22
|
+
|
23
|
+
return timestamp if timestamp
|
24
|
+
|
25
|
+
@logger.warn("Unrecognized @timestamp value, setting current time to @timestamp",
|
26
|
+
:value => ts.inspect)
|
27
|
+
return nil
|
28
|
+
rescue LogStash::TimestampParserError => e
|
29
|
+
@logger.warn("Error parsing @timestamp string, setting current time to @timestamp",
|
30
|
+
:value => ts.inspect, :exception => e.message)
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module LogStash::Inputs::BeatsSupport
|
3
|
+
# Base Transform class, expose the plugin decorate method,
|
4
|
+
# apply the tags and make sure we copy the beat hostname into `host`
|
5
|
+
# for backward compatibility.
|
6
|
+
class EventTransformCommon
|
7
|
+
def initialize(input)
|
8
|
+
@input = input
|
9
|
+
@logger = input.logger
|
10
|
+
end
|
11
|
+
|
12
|
+
# Copies the beat.hostname field into the host field unless
|
13
|
+
# the host field is already defined
|
14
|
+
def copy_beat_hostname(event)
|
15
|
+
host = event["[beat][hostname]"]
|
16
|
+
|
17
|
+
if host && event["host"].nil?
|
18
|
+
event["host"] = host
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# This break the `#decorate` method visibility of the plugin base
|
23
|
+
# class, the method is protected and we cannot access it, but well ruby
|
24
|
+
# can let you do all the wrong thing.
|
25
|
+
#
|
26
|
+
# I think the correct behavior would be to allow the plugin to return a
|
27
|
+
# `Decorator` object that we can pass to other objects, since only the
|
28
|
+
# plugin know the data used to decorate. This would allow a more component
|
29
|
+
# based workflow.
|
30
|
+
def decorate(event)
|
31
|
+
@input.send(:decorate, event)
|
32
|
+
end
|
33
|
+
|
34
|
+
def transform(event)
|
35
|
+
copy_beat_hostname(event)
|
36
|
+
decorate(event)
|
37
|
+
event
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/inputs/beats_support/event_transform_common"
|
3
|
+
module LogStash::Inputs::BeatsSupport
|
4
|
+
# Take the the raw output from the library, decorate it with
|
5
|
+
# the configured tags in the plugins and normalize the hostname
|
6
|
+
# for backward compatibility
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# @see [Lumberjack::Beats::Parser]
|
10
|
+
#
|
11
|
+
class RawEventTransform < EventTransformCommon
|
12
|
+
def transform(event)
|
13
|
+
super(event)
|
14
|
+
event.tag("beats_input_raw_event")
|
15
|
+
event
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module LogStash::Inputs::BeatsSupport
|
3
|
+
# Wrap the `Java SynchronousQueue` to acts as the synchronization mechanism
|
4
|
+
# this queue can block for a maximum amount of time logstash's queue
|
5
|
+
# doesn't implement that feature.
|
6
|
+
#
|
7
|
+
# See proposal for core: https://github.com/elastic/logstash/pull/4408
|
8
|
+
#
|
9
|
+
# See https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/SynchronousQueue.html
|
10
|
+
java_import "java.util.concurrent.SynchronousQueue"
|
11
|
+
java_import "java.util.concurrent.TimeUnit"
|
12
|
+
class SynchronousQueueWithOffer
|
13
|
+
def initialize(timeout, fairness_policy = true)
|
14
|
+
# set Fairness policy to `FIFO`
|
15
|
+
#
|
16
|
+
# In the context of the input it makes sense to
|
17
|
+
# try to deal with the older connection before
|
18
|
+
# the newer one, since the older will be closer to
|
19
|
+
# reach the connection timeout.
|
20
|
+
#
|
21
|
+
@timeout = timeout
|
22
|
+
@queue = java.util.concurrent.SynchronousQueue.new(fairness_policy)
|
23
|
+
end
|
24
|
+
|
25
|
+
# This method will return true if it successfully added the element to the queue.
|
26
|
+
# If the timeout is reached and it wasn't inserted successfully to
|
27
|
+
# the queue it will return false.
|
28
|
+
def offer(element, timeout = nil)
|
29
|
+
@queue.offer(element, timeout || @timeout, java.util.concurrent.TimeUnit::SECONDS)
|
30
|
+
end
|
31
|
+
|
32
|
+
def take
|
33
|
+
@queue.take
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -57,6 +57,13 @@ module Lumberjack module Beats
|
|
57
57
|
end
|
58
58
|
end # def initialize
|
59
59
|
|
60
|
+
# Server#run method, allow the library to manage all the connection
|
61
|
+
# threads, this handing is quite minimal and don't handler
|
62
|
+
# all the possible cases deconnection/connection.
|
63
|
+
#
|
64
|
+
# To have a more granular control over the connection you should manage
|
65
|
+
# them yourself, see Server#accept method which return a Connection
|
66
|
+
# instance.
|
60
67
|
def run(&block)
|
61
68
|
while !closed?
|
62
69
|
connection = accept
|
@@ -67,7 +74,14 @@ module Lumberjack module Beats
|
|
67
74
|
next unless connection
|
68
75
|
|
69
76
|
Thread.new(connection) do |connection|
|
70
|
-
|
77
|
+
begin
|
78
|
+
connection.run(&block)
|
79
|
+
rescue Lumberjack::Beats::Connection::ConnectionClosed
|
80
|
+
# Connection will raise a wrapped exception upstream,
|
81
|
+
# but if the threads are managed by the library we can simply ignore it.
|
82
|
+
#
|
83
|
+
# Note: This follow the previous behavior of the perfect silence.
|
84
|
+
end
|
71
85
|
end
|
72
86
|
end
|
73
87
|
end # def run
|
@@ -132,6 +146,7 @@ module Lumberjack module Beats
|
|
132
146
|
PROTOCOL_VERSION_2 = "2".ord
|
133
147
|
|
134
148
|
SUPPORTED_PROTOCOLS = [PROTOCOL_VERSION_1, PROTOCOL_VERSION_2]
|
149
|
+
class UnsupportedProtocol < StandardError; end
|
135
150
|
|
136
151
|
def initialize
|
137
152
|
@buffer_offset = 0
|
@@ -222,7 +237,7 @@ module Lumberjack module Beats
|
|
222
237
|
if supported_protocol?(version)
|
223
238
|
yield :version, version
|
224
239
|
else
|
225
|
-
raise "unsupported protocol #{version}"
|
240
|
+
raise UnsupportedProtocol, "unsupported protocol #{version}"
|
226
241
|
end
|
227
242
|
end
|
228
243
|
|
@@ -298,9 +313,37 @@ module Lumberjack module Beats
|
|
298
313
|
end # class Parser
|
299
314
|
|
300
315
|
class Connection
|
316
|
+
# Wrap the original exception into a common one,
|
317
|
+
# to make upstream managing and reporting easier
|
318
|
+
# But lets make sure we keep the meaning of the original exception.
|
319
|
+
class ConnectionClosed < StandardError
|
320
|
+
attr_reader :original_exception
|
321
|
+
|
322
|
+
def initialize(original_exception)
|
323
|
+
super(original_exception)
|
324
|
+
|
325
|
+
@original_exception = original_exception
|
326
|
+
set_backtrace(original_exception.backtrace) if original_exception
|
327
|
+
end
|
328
|
+
|
329
|
+
def to_s
|
330
|
+
"#{self.class.name} wrapping: #{original_exception.class.name}, #{super.to_s}"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
301
334
|
READ_SIZE = 16384
|
335
|
+
PEER_INFORMATION_NOT_AVAILABLE = "<PEER INFORMATION NOT AVAILABLE>"
|
336
|
+
RESCUED_CONNECTION_EXCEPTIONS = [
|
337
|
+
EOFError,
|
338
|
+
OpenSSL::SSL::SSLError,
|
339
|
+
IOError,
|
340
|
+
Errno::ECONNRESET,
|
341
|
+
Errno::EPIPE,
|
342
|
+
Lumberjack::Beats::Parser::UnsupportedProtocol
|
343
|
+
]
|
302
344
|
|
303
345
|
attr_accessor :server
|
346
|
+
attr_reader :peer
|
304
347
|
|
305
348
|
def initialize(fd, server)
|
306
349
|
@parser = Parser.new
|
@@ -308,23 +351,27 @@ module Lumberjack module Beats
|
|
308
351
|
|
309
352
|
@server = server
|
310
353
|
@ack_handler = nil
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
354
|
+
|
355
|
+
# Fetch the details of the host before reading anything from the socket
|
356
|
+
# se we can use that information when debugging connection issues with
|
357
|
+
# remote hosts.
|
358
|
+
begin
|
359
|
+
@peer = "#{@fd.peeraddr[3]}:#{@fd.peeraddr[1]}"
|
360
|
+
rescue IOError
|
361
|
+
# This can happen if the connection is drop or close before
|
362
|
+
# fetching the host details, lets return a generic string.
|
363
|
+
@peer = PEER_INFORMATION_NOT_AVAILABLE
|
364
|
+
end
|
315
365
|
end
|
316
366
|
|
317
367
|
def run(&block)
|
318
368
|
while !server.closed?
|
319
369
|
read_socket(&block)
|
320
370
|
end
|
321
|
-
rescue
|
322
|
-
OpenSSL::SSL::SSLError,
|
323
|
-
IOError,
|
324
|
-
Errno::ECONNRESET,
|
325
|
-
Errno::EPIPE
|
371
|
+
rescue *RESCUED_CONNECTION_EXCEPTIONS => e
|
326
372
|
# EOF or other read errors, only action is to shutdown which we'll do in
|
327
373
|
# 'ensure'
|
374
|
+
raise ConnectionClosed.new(e)
|
328
375
|
rescue
|
329
376
|
# when the server is shutting down we can safely ignore any exceptions
|
330
377
|
# On windows, we can get a `SystemCallErr`
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "logstash-input-beats"
|
3
|
-
s.version = "2.
|
3
|
+
s.version = "2.1.1"
|
4
4
|
s.licenses = ["Apache License (2.0)"]
|
5
5
|
s.summary = "Receive events using the lumberjack protocol."
|
6
6
|
s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.require_paths = ["lib"]
|
11
11
|
|
12
12
|
# Files
|
13
|
-
s.files = Dir["lib/**/*","spec/**/*","
|
13
|
+
s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT"]
|
14
14
|
|
15
15
|
# Tests
|
16
16
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
@@ -23,7 +23,8 @@ Gem::Specification.new do |s|
|
|
23
23
|
|
24
24
|
s.add_runtime_dependency "logstash-codec-plain"
|
25
25
|
s.add_runtime_dependency "concurrent-ruby", "~> 0.9.2"
|
26
|
-
s.add_runtime_dependency "
|
26
|
+
s.add_runtime_dependency "thread_safe", "~> 0.3.5"
|
27
|
+
s.add_runtime_dependency "logstash-codec-multiline", "~> 2.0.5"
|
27
28
|
|
28
29
|
s.add_development_dependency "flores", "~>0.0.6"
|
29
30
|
s.add_development_dependency "rspec"
|
data/spec/inputs/beats_spec.rb
CHANGED
@@ -70,140 +70,49 @@ describe LogStash::Inputs::Beats do
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
let(:
|
73
|
+
context "#handle_new_connection" do
|
74
|
+
let(:config) {{ "ssl" => false, "port" => 0, "type" => "example", "tags" => "beats" }}
|
75
|
+
let(:plugin) { LogStash::Inputs::Beats.new(config) }
|
76
|
+
let(:connection) { DummyConnection.new(events) }
|
77
|
+
let(:buffer_queue) { DummyNeverBlockedQueue.new }
|
78
|
+
let(:pipeline_queue) { [] }
|
79
|
+
let(:events) {
|
80
|
+
[
|
81
|
+
{ :map => { "id" => 1 }, :identity_stream => "/var/log/message" },
|
82
|
+
{ :map => { "id" => 2 }, :identity_stream => "/var/log/message_2" }
|
83
|
+
]
|
84
|
+
}
|
85
|
+
|
86
|
+
before :each do
|
87
|
+
plugin.register
|
88
|
+
|
89
|
+
# Event if we dont mock the actual socket work
|
90
|
+
# we have to call run because it will correctly setup the queues
|
91
|
+
# instances variables
|
92
|
+
t = Thread.new do
|
93
|
+
plugin.run(pipeline_queue)
|
94
|
+
end
|
76
95
|
|
77
|
-
|
78
|
-
{ "port" => port, "ssl_certificate" => certificate.ssl_cert, "ssl_key" => certificate.ssl_key,
|
79
|
-
"type" => "example", "codec" => codec }
|
96
|
+
sleep(0.1) until t.status == "run"
|
80
97
|
end
|
81
98
|
|
82
|
-
|
83
|
-
|
99
|
+
after :each do
|
100
|
+
plugin.stop
|
84
101
|
end
|
85
102
|
|
86
|
-
context "
|
87
|
-
let(:
|
88
|
-
|
89
|
-
|
90
|
-
let(:identity_stream) { "custom-type-input_type-source" }
|
91
|
-
|
92
|
-
context "without a `target_field` defined" do
|
93
|
-
it "decorates the event" do
|
94
|
-
beats.create_event(event_map, identity_stream) do |event|
|
95
|
-
expect(event["foo"]).to eq("bar")
|
96
|
-
expect(event["[@metadata][hidden]"]).to eq("secret")
|
97
|
-
expect(event["tags"]).to include("bonjour")
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
context "with a `target_field` defined" do
|
103
|
-
let(:event_map) { super.merge({"message" => "with a field"}) }
|
104
|
-
|
105
|
-
it "decorates the event" do
|
106
|
-
beats.create_event(event_map, identity_stream) do |event|
|
107
|
-
expect(event["foo"]).to eq("bar")
|
108
|
-
expect(event["[@metadata][hidden]"]).to eq("secret")
|
109
|
-
expect(event["tags"]).to include("bonjour")
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
context "when data is buffered in the codec" do
|
115
|
-
let(:codec) { LogStash::Codecs::Multiline.new("pattern" => '^\s', "what" => "previous") }
|
116
|
-
let(:event_map) { {"message" => "hello?", "tags" => ["syslog"]} }
|
117
|
-
|
118
|
-
it "returns nil" do
|
119
|
-
expect { |b| beats.create_event(event_map, identity_stream, &b) }.not_to yield_control
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
context "multiline" do
|
124
|
-
let(:codec) { LogStash::Codecs::Multiline.new("pattern" => '^2015', "what" => "previous", "negate" => true) }
|
125
|
-
let(:events_map) do
|
126
|
-
[
|
127
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "2015-11-10 10:14:38,907 line 1" },
|
128
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "line 1.1" },
|
129
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "2015-11-10 10:16:38,907 line 2" },
|
130
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "line 2.1" },
|
131
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "line 2.2" },
|
132
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "line 2.3" },
|
133
|
-
{ "beat" => { "id" => "main", "resource_id" => "md5"}, "message" => "2015-11-10 10:18:38,907 line 3" }
|
134
|
-
]
|
135
|
-
end
|
136
|
-
|
137
|
-
let(:queue) { [] }
|
138
|
-
before do
|
139
|
-
Thread.new { beats.run(queue) }
|
140
|
-
sleep(0.1)
|
141
|
-
end
|
142
|
-
|
143
|
-
it "should correctly merge multiple events" do
|
144
|
-
events_map.each { |map| beats.create_event(map, identity_stream) { |e| queue << e } }
|
145
|
-
# This cannot currently work without explicitely call a flush
|
146
|
-
# the flush is never timebased, if no new data is coming in we wont flush the buffer
|
147
|
-
# https://github.com/logstash-plugins/logstash-codec-multiline/issues/11
|
148
|
-
beats.stop
|
149
|
-
expect(queue.size).to eq(3)
|
150
|
-
|
151
|
-
expect(queue.collect { |e| e["message"] }).to include("2015-11-10 10:14:38,907 line 1\nline 1.1",
|
152
|
-
"2015-11-10 10:16:38,907 line 2\nline 2.1\nline 2.2\nline 2.3",
|
153
|
-
"2015-11-10 10:18:38,907 line 3")
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
context "with a beat.hostname field" do
|
158
|
-
let(:event_map) { {"message" => "hello", "beat" => {"hostname" => "linux01"} } }
|
159
|
-
|
160
|
-
it "copies it to the host field" do
|
161
|
-
beats.create_event(event_map, identity_stream) do |event|
|
162
|
-
expect(event["host"]).to eq("linux01")
|
163
|
-
end
|
164
|
-
end
|
103
|
+
context "when an exception occur" do
|
104
|
+
let(:connection_handler) { LogStash::Inputs::BeatsSupport::ConnectionHandler.new(connection, plugin, buffer_queue) }
|
105
|
+
before do
|
106
|
+
expect(LogStash::Inputs::BeatsSupport::ConnectionHandler).to receive(:new).with(any_args).and_return(connection_handler)
|
165
107
|
end
|
166
108
|
|
167
|
-
|
168
|
-
|
109
|
+
it "calls flush on the handler and tag the events" do
|
110
|
+
expect(connection_handler).to receive(:accept) { raise LogStash::Inputs::Beats::InsertingToQueueTakeTooLong }
|
111
|
+
expect(connection_handler).to receive(:flush).and_yield(LogStash::Event.new)
|
112
|
+
plugin.handle_new_connection(connection)
|
169
113
|
|
170
|
-
|
171
|
-
|
172
|
-
expect(event["host"]).to eq("linux01")
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
context "without a beat.hostname field" do
|
178
|
-
let(:event_map) { {"message" => "hello", "beat" => {"name" => "linux01"} } }
|
179
|
-
|
180
|
-
it "should not add a host field" do
|
181
|
-
beats.create_event(event_map, identity_stream) do |event|
|
182
|
-
expect(event["beat"]["name"]).to eq("linux01")
|
183
|
-
expect(event["host"]).to be_nil
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
context "with a beat.hostname and host fields" do
|
189
|
-
let(:event_map) { {"message" => "hello", "host" => "linux02", "beat" => {"hostname" => "linux01"} } }
|
190
|
-
|
191
|
-
it "should not overwrite host" do
|
192
|
-
beats.create_event(event_map, identity_stream) do |event|
|
193
|
-
expect(event["host"]).to eq("linux02")
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
context "with a host field in the message" do
|
199
|
-
let(:codec) { LogStash::Codecs::JSON.new }
|
200
|
-
let(:event_map) { {"message" => '{"host": "linux02"}', "beat" => {"hostname" => "linux01"} } }
|
201
|
-
|
202
|
-
it "should take the host from the JSON message" do
|
203
|
-
beats.create_event(event_map, identity_stream) do
|
204
|
-
expect(event["host"]).to eq("linux02")
|
205
|
-
end
|
206
|
-
end
|
114
|
+
event = pipeline_queue.shift
|
115
|
+
expect(event["tags"]).to include("beats_input_flushed_by_end_of_connection")
|
207
116
|
end
|
208
117
|
end
|
209
118
|
end
|