logstash-output-cloudwatchlogs-latest 2.0.0.pre1
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/.gitignore +17 -0
- data/Gemfile +2 -0
- data/LICENSE +26 -0
- data/NOTICE.txt +2 -0
- data/README.md +150 -0
- data/Rakefile +7 -0
- data/lib/logstash/outputs/cloudwatchlogs.rb +413 -0
- data/logstash-output-cloudwatchlogs.gemspec +28 -0
- data/spec/outputs/cloudwatchlogs_spec.rb +624 -0
- metadata +125 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
|
3
|
+
s.name = 'logstash-output-cloudwatchlogs-latest'
|
4
|
+
s.version = '2.0.0.pre1'
|
5
|
+
s.licenses = ['Amazon Software License']
|
6
|
+
s.summary = "This output lets you send logs to AWS CloudWatch Logs service"
|
7
|
+
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"
|
8
|
+
s.authors = ["AWS"]
|
9
|
+
s.email = 'cloudwatch-logs-feedback@amazon.com'
|
10
|
+
s.homepage = "http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/WhatIsCloudWatchLogs.html"
|
11
|
+
s.require_paths = ["lib"]
|
12
|
+
|
13
|
+
# Files
|
14
|
+
s.files = `git ls-files`.split($\)+::Dir.glob('vendor/*')
|
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" => "output" }
|
21
|
+
|
22
|
+
# Gem dependencies
|
23
|
+
s.add_runtime_dependency 'logstash-core', '>= 2.0.0', '< 3.0.0'
|
24
|
+
s.add_runtime_dependency 'logstash-codec-plain', '>= 2.0.0', '< 3.0.0'
|
25
|
+
s.add_runtime_dependency 'logstash-mixin-aws', '>= 2.0.0'
|
26
|
+
|
27
|
+
s.add_development_dependency 'logstash-devutils'
|
28
|
+
end
|
@@ -0,0 +1,624 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
5
|
+
#
|
6
|
+
# Licensed under the Amazon Software License (the "License").
|
7
|
+
# You may not use this file except in compliance with the License.
|
8
|
+
# A copy of the License is located at
|
9
|
+
#
|
10
|
+
# http://aws.amazon.com/asl/
|
11
|
+
#
|
12
|
+
# or in the "license" file accompanying this file. This file is distributed
|
13
|
+
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
14
|
+
# express or implied. See the License for the specific language governing
|
15
|
+
# permissions and limitations under the License.
|
16
|
+
|
17
|
+
require "logstash/devutils/rspec/spec_helper"
|
18
|
+
require "logstash/plugin"
|
19
|
+
require "logstash/json"
|
20
|
+
require "logstash/timestamp"
|
21
|
+
require "logstash/outputs/cloudwatchlogs"
|
22
|
+
|
23
|
+
require "aws-sdk"
|
24
|
+
|
25
|
+
describe "outputs/cloudwatchlogs" do
|
26
|
+
|
27
|
+
describe "#new" do
|
28
|
+
it "should raise error when log group name is not configured" do
|
29
|
+
expect {
|
30
|
+
LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
31
|
+
"log_group_name" => "lg")
|
32
|
+
}.to raise_error(LogStash::ConfigurationError)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should raise error when log stream name is not configured" do
|
36
|
+
expect {
|
37
|
+
LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
38
|
+
"log_stream_name" => "ls")
|
39
|
+
}.to raise_error(LogStash::ConfigurationError)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should create the output with log group/stream name" do
|
43
|
+
expect {
|
44
|
+
LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
45
|
+
"log_group_name" => "lg", "log_stream_name" => "ls")
|
46
|
+
}.to_not raise_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#register" do
|
51
|
+
it "should set the batch_count to MAX_BATCH_COUNT if a larger value is configured" do
|
52
|
+
output = LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
53
|
+
"log_group_name" => "lg", "log_stream_name" => "ls",
|
54
|
+
"batch_count" => LogStash::Outputs::CloudWatchLogs::MAX_BATCH_COUNT + 1)
|
55
|
+
expect {output.register}.to_not raise_error
|
56
|
+
output.batch_count.should eql(LogStash::Outputs::CloudWatchLogs::MAX_BATCH_COUNT)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should set the batch_size to MAX_BATCH_SIZE if a larger value is configured" do
|
60
|
+
output = LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
61
|
+
"log_group_name" => "lg", "log_stream_name" => "ls",
|
62
|
+
"batch_count" => LogStash::Outputs::CloudWatchLogs::MAX_BATCH_SIZE + 1)
|
63
|
+
expect {output.register}.to_not raise_error
|
64
|
+
output.batch_size.should eql(LogStash::Outputs::CloudWatchLogs::MAX_BATCH_SIZE)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should set the buffer_duration to MIN_BUFFER_DURATION if a smaler value is configured" do
|
68
|
+
output = LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
69
|
+
"log_group_name" => "lg", "log_stream_name" => "ls",
|
70
|
+
"buffer_duration" => LogStash::Outputs::CloudWatchLogs::MIN_BUFFER_DURATION - 1)
|
71
|
+
expect {output.register}.to_not raise_error
|
72
|
+
output.buffer_duration.should eql(LogStash::Outputs::CloudWatchLogs::MIN_BUFFER_DURATION)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#receive" do
|
77
|
+
before :each do
|
78
|
+
@output = LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
79
|
+
"log_group_name" => "lg", "log_stream_name" => "ls")
|
80
|
+
@output.register
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when event is invalid" do
|
84
|
+
before :each do
|
85
|
+
@event = LogStash::Event.new
|
86
|
+
@event.timestamp = LogStash::Timestamp.coerce("2015-02-13T01:19:08Z")
|
87
|
+
@event["message"] = "test"
|
88
|
+
expect(@output.buffer).not_to receive(:enq)
|
89
|
+
end
|
90
|
+
context "when event doesn't have @timestamp" do
|
91
|
+
it "should not save the event" do
|
92
|
+
@event.remove("@timestamp")
|
93
|
+
expect { @output.receive(@event) }.to_not raise_error
|
94
|
+
end
|
95
|
+
end
|
96
|
+
context "when event doesn't have message" do
|
97
|
+
it "should not save the event" do
|
98
|
+
@event.remove("message")
|
99
|
+
expect { @output.receive(@event) }.to_not raise_error
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "when event is valid" do
|
105
|
+
context "when first event is received" do
|
106
|
+
it "should save the event to buffer" do
|
107
|
+
expect(@output.buffer).to receive(:enq) { {:timestamp => 1423786748000.0, :message => "test"} }
|
108
|
+
event = LogStash::Event.new
|
109
|
+
event.timestamp = LogStash::Timestamp.coerce("2015-02-13T01:19:08Z")
|
110
|
+
event["message"] = "test"
|
111
|
+
expect { @output.receive(event) }.to_not raise_error
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#flush" do
|
119
|
+
before :each do
|
120
|
+
@cwl = Aws::CloudWatchLogs::Client.new(:region => "us-east-1")
|
121
|
+
@output = LogStash::Plugin.lookup("output", "cloudwatchlogs").new(
|
122
|
+
"log_group_name" => "lg", "log_stream_name" => "ls")
|
123
|
+
@output.register
|
124
|
+
@output.cwl = @cwl
|
125
|
+
end
|
126
|
+
|
127
|
+
context "when received zero events" do
|
128
|
+
it "should not make the service call" do
|
129
|
+
expect(@cwl).not_to receive(:put_log_events)
|
130
|
+
@output.flush([])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context "when received some events" do
|
135
|
+
before :each do
|
136
|
+
@output.sequence_token = 'token'
|
137
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
138
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
139
|
+
end
|
140
|
+
context "when events are not sorted" do
|
141
|
+
it "should sort the events before sending them to service" do
|
142
|
+
expect(@cwl).to receive(:put_log_events).with(
|
143
|
+
:log_events => [
|
144
|
+
{:timestamp => 123, :message => 'abc'},
|
145
|
+
{:timestamp => 124, :message => 'zzz'}],
|
146
|
+
:log_group_name => 'lg',
|
147
|
+
:log_stream_name => 'ls',
|
148
|
+
:sequence_token => 'token'
|
149
|
+
) { @response }
|
150
|
+
@output.flush([
|
151
|
+
{:timestamp => 124, :message => 'zzz'},
|
152
|
+
{:timestamp => 123, :message => 'abc'}])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
context "when events are sorted" do
|
156
|
+
it "should send the events as is" do
|
157
|
+
expect(@cwl).to receive(:put_log_events).with(
|
158
|
+
:log_events => [
|
159
|
+
{:timestamp => 123, :message => 'abc'},
|
160
|
+
{:timestamp => 124, :message => 'zzz'}],
|
161
|
+
:log_group_name => 'lg',
|
162
|
+
:log_stream_name => 'ls',
|
163
|
+
:sequence_token => 'token'
|
164
|
+
) { @response }
|
165
|
+
@output.flush([
|
166
|
+
{:timestamp => 123, :message => 'abc'},
|
167
|
+
{:timestamp => 124, :message => 'zzz'}])
|
168
|
+
end
|
169
|
+
end
|
170
|
+
context "when log events span more than 24 hours" do
|
171
|
+
it "should break log events into multiple batches and no batch spans more than 24 hours" do
|
172
|
+
twenty_four_hours_in_mills = 24 * 60 * 60 * 1000
|
173
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
174
|
+
:log_events => [
|
175
|
+
{:timestamp => 123, :message => 'abc'}],
|
176
|
+
:log_group_name => 'lg',
|
177
|
+
:log_stream_name => 'ls',
|
178
|
+
:sequence_token => 'token'
|
179
|
+
) { @response }
|
180
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
181
|
+
:log_events => [
|
182
|
+
{:timestamp => 123 + twenty_four_hours_in_mills + 1, :message => 'zzz'}],
|
183
|
+
:log_group_name => 'lg',
|
184
|
+
:log_stream_name => 'ls',
|
185
|
+
:sequence_token => 'ntoken'
|
186
|
+
)
|
187
|
+
@output.flush([
|
188
|
+
{:timestamp => 123, :message => 'abc'},
|
189
|
+
{:timestamp => 123 + twenty_four_hours_in_mills + 1, :message => 'zzz'}])
|
190
|
+
end
|
191
|
+
end
|
192
|
+
context "when log events span exactly 24 hours" do
|
193
|
+
it "should not break log events into multiple batches" do
|
194
|
+
twenty_four_hours_in_mills = 24 * 60 * 60 * 1000
|
195
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
196
|
+
:log_events => [
|
197
|
+
{:timestamp => 123, :message => 'abc'},
|
198
|
+
{:timestamp => 123 + twenty_four_hours_in_mills, :message => 'zzz'}],
|
199
|
+
:log_group_name => 'lg',
|
200
|
+
:log_stream_name => 'ls',
|
201
|
+
:sequence_token => 'token'
|
202
|
+
) { @response }
|
203
|
+
@output.flush([
|
204
|
+
{:timestamp => 123, :message => 'abc'},
|
205
|
+
{:timestamp => 123 + twenty_four_hours_in_mills, :message => 'zzz'}])
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "error handling" do
|
211
|
+
context "when sending the first batch" do
|
212
|
+
it "should use null sequence token" do
|
213
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
214
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
215
|
+
expect(@cwl).to receive(:put_log_events).with(
|
216
|
+
:log_events => [
|
217
|
+
{:timestamp => 123, :message => 'abc'}],
|
218
|
+
:log_group_name => 'lg',
|
219
|
+
:log_stream_name => 'ls',
|
220
|
+
:sequence_token => nil
|
221
|
+
) { @response }
|
222
|
+
@output.flush([
|
223
|
+
{:timestamp => 123, :message => 'abc'}])
|
224
|
+
end
|
225
|
+
context "when the log stream doesn't exist" do
|
226
|
+
it "should create log group and log stream" do
|
227
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
228
|
+
# throw exception for 1st call and good for following calls
|
229
|
+
allow(@cwl).to receive(:put_log_events) do
|
230
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
231
|
+
raise Aws::CloudWatchLogs::Errors::ResourceNotFoundException.new(nil, nil)
|
232
|
+
end
|
233
|
+
expect(@cwl).to receive(:put_log_events).exactly(2).times.with(
|
234
|
+
:log_events => [
|
235
|
+
{:timestamp => 123, :message => 'abc'}],
|
236
|
+
:log_group_name => 'lg',
|
237
|
+
:log_stream_name => 'ls',
|
238
|
+
:sequence_token => nil
|
239
|
+
)
|
240
|
+
expect(@cwl).to receive(:create_log_group).with(:log_group_name => 'lg')
|
241
|
+
expect(@cwl).to receive(:create_log_stream).with(:log_group_name => 'lg', :log_stream_name => 'ls')
|
242
|
+
@output.flush([
|
243
|
+
{:timestamp => 123, :message => 'abc'}])
|
244
|
+
end
|
245
|
+
end
|
246
|
+
context "when the log stream exists" do
|
247
|
+
context "log stream has no log event" do
|
248
|
+
it "should succeed" do
|
249
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
250
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
251
|
+
expect(@cwl).to receive(:put_log_events).with(
|
252
|
+
:log_events => [
|
253
|
+
{:timestamp => 123, :message => 'abc'}],
|
254
|
+
:log_group_name => 'lg',
|
255
|
+
:log_stream_name => 'ls',
|
256
|
+
:sequence_token => nil
|
257
|
+
)
|
258
|
+
@output.flush([
|
259
|
+
{:timestamp => 123, :message => 'abc'}])
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
context "log stream has some log events" do
|
264
|
+
context "when log stream only accpeted one batch in the past" do
|
265
|
+
context "when the sending batch is the same as accepted batch" do
|
266
|
+
it "should not retry upon DataAlreadyAcceptedException" do
|
267
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
268
|
+
@ex = Aws::CloudWatchLogs::Errors::DataAlreadyAcceptedException.new(nil, "The next batch can be sent with sequenceToken: 456")
|
269
|
+
# throw exception for 1st call and good for following calls
|
270
|
+
allow(@cwl).to receive(:put_log_events) do
|
271
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
272
|
+
raise @ex
|
273
|
+
end
|
274
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
275
|
+
:log_events => [
|
276
|
+
{:timestamp => 123, :message => 'abc'}],
|
277
|
+
:log_group_name => 'lg',
|
278
|
+
:log_stream_name => 'ls',
|
279
|
+
:sequence_token => nil
|
280
|
+
)
|
281
|
+
@output.flush([
|
282
|
+
{:timestamp => 123, :message => 'abc'}])
|
283
|
+
end
|
284
|
+
end
|
285
|
+
context "when the sending batch is different than accepted batch" do
|
286
|
+
it "should retry upon InvalidSequenceTokenException" do
|
287
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
288
|
+
@ex = Aws::CloudWatchLogs::Errors::InvalidSequenceTokenException.new(nil, "The next expected sequenceToken is: 456")
|
289
|
+
# throw exception for 1st call and good for following calls
|
290
|
+
allow(@cwl).to receive(:put_log_events) do
|
291
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
292
|
+
raise @ex
|
293
|
+
end
|
294
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
295
|
+
:log_events => [
|
296
|
+
{:timestamp => 123, :message => 'abc'}],
|
297
|
+
:log_group_name => 'lg',
|
298
|
+
:log_stream_name => 'ls',
|
299
|
+
:sequence_token => nil
|
300
|
+
)
|
301
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
302
|
+
:log_events => [
|
303
|
+
{:timestamp => 123, :message => 'abc'}],
|
304
|
+
:log_group_name => 'lg',
|
305
|
+
:log_stream_name => 'ls',
|
306
|
+
:sequence_token => '456'
|
307
|
+
) { @response }
|
308
|
+
@output.flush([
|
309
|
+
{:timestamp => 123, :message => 'abc'}])
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
context "when log stream already accepted more than one batch" do
|
314
|
+
it "should retry upon InvalidSequenceTokenException" do
|
315
|
+
@output.sequence_token = "lasttoken"
|
316
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
317
|
+
@ex = Aws::CloudWatchLogs::Errors::InvalidSequenceTokenException.new(nil, "The next expected sequenceToken is: 456")
|
318
|
+
# throw exception for 1st call and good for following calls
|
319
|
+
allow(@cwl).to receive(:put_log_events) do
|
320
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
321
|
+
raise @ex
|
322
|
+
end
|
323
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
324
|
+
:log_events => [
|
325
|
+
{:timestamp => 123, :message => 'abc'}],
|
326
|
+
:log_group_name => 'lg',
|
327
|
+
:log_stream_name => 'ls',
|
328
|
+
:sequence_token => 'lasttoken'
|
329
|
+
)
|
330
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
331
|
+
:log_events => [
|
332
|
+
{:timestamp => 123, :message => 'abc'}],
|
333
|
+
:log_group_name => 'lg',
|
334
|
+
:log_stream_name => 'ls',
|
335
|
+
:sequence_token => '456'
|
336
|
+
) { @response }
|
337
|
+
@output.flush([
|
338
|
+
{:timestamp => 123, :message => 'abc'}])
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
context "when sending batch after first batch" do
|
345
|
+
before :each do
|
346
|
+
@output.sequence_token = 'lasttoken'
|
347
|
+
end
|
348
|
+
it "should use the previous token" do
|
349
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
350
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
351
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
352
|
+
:log_events => [
|
353
|
+
{:timestamp => 123, :message => 'abc'}],
|
354
|
+
:log_group_name => 'lg',
|
355
|
+
:log_stream_name => 'ls',
|
356
|
+
:sequence_token => 'lasttoken'
|
357
|
+
) { @response }
|
358
|
+
@output.flush([
|
359
|
+
{:timestamp => 123, :message => 'abc'}])
|
360
|
+
end
|
361
|
+
end
|
362
|
+
context "when sending invalid request" do
|
363
|
+
it "should not retry" do
|
364
|
+
@output.log_group_name = nil
|
365
|
+
@output.log_stream_name = nil
|
366
|
+
@output.sequence_token = nil
|
367
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
368
|
+
@ex = Aws::CloudWatchLogs::Errors::InvalidParameterException.new(nil, nil)
|
369
|
+
allow(@cwl).to receive(:put_log_events) { raise @ex }
|
370
|
+
expect(@cwl).to receive(:put_log_events).once.with(
|
371
|
+
:log_events => [
|
372
|
+
{:timestamp => 123, :message => 'abc'}],
|
373
|
+
:log_group_name => nil,
|
374
|
+
:log_stream_name => nil,
|
375
|
+
:sequence_token => nil
|
376
|
+
)
|
377
|
+
@output.flush([
|
378
|
+
{:timestamp => 123, :message => 'abc'}])
|
379
|
+
end
|
380
|
+
end
|
381
|
+
context "when receiving unknown exception" do
|
382
|
+
it "should retry forever until getting back non-unknow exception" do
|
383
|
+
@output.sequence_token = nil
|
384
|
+
allow(@output).to receive(:sleep)
|
385
|
+
@response = double(Aws::PageableResponse, :next_sequence_token => 'ntoken')
|
386
|
+
@ex = Aws::CloudWatchLogs::Errors::ServiceUnavailableException.new(nil, nil)
|
387
|
+
# Throw 7 exceptions and then return normal
|
388
|
+
allow(@cwl).to receive(:put_log_events) do
|
389
|
+
allow(@cwl).to receive(:put_log_events) do
|
390
|
+
allow(@cwl).to receive(:put_log_events) do
|
391
|
+
allow(@cwl).to receive(:put_log_events) do
|
392
|
+
allow(@cwl).to receive(:put_log_events) do
|
393
|
+
allow(@cwl).to receive(:put_log_events) do
|
394
|
+
allow(@cwl).to receive(:put_log_events) do
|
395
|
+
allow(@cwl).to receive(:put_log_events) { @response }
|
396
|
+
raise @ex
|
397
|
+
end
|
398
|
+
raise @ex
|
399
|
+
end
|
400
|
+
raise @ex
|
401
|
+
end
|
402
|
+
raise @ex
|
403
|
+
end
|
404
|
+
raise @ex
|
405
|
+
end
|
406
|
+
raise @ex
|
407
|
+
end
|
408
|
+
raise @ex
|
409
|
+
end
|
410
|
+
expect(@cwl).to receive(:put_log_events).exactly(8).times.with(
|
411
|
+
:log_events => [
|
412
|
+
{:timestamp => 123, :message => 'abc'}],
|
413
|
+
:log_group_name => 'lg',
|
414
|
+
:log_stream_name => 'ls',
|
415
|
+
:sequence_token => nil
|
416
|
+
)
|
417
|
+
[2, 4, 8, 16, 32].each do |i|
|
418
|
+
expect(@output).to receive(:sleep).once.with(i)
|
419
|
+
end
|
420
|
+
# Should sleep upto 64 seconds for each retry
|
421
|
+
expect(@output).to receive(:sleep).twice.with(64)
|
422
|
+
@output.flush([
|
423
|
+
{:timestamp => 123, :message => 'abc'}])
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
describe "outputs/cloudwatchlogs/buffer" do
|
434
|
+
|
435
|
+
describe "#initialize" do
|
436
|
+
it "should create a buffer" do
|
437
|
+
buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
438
|
+
max_batch_count: 5, max_batch_size: 10, buffer_duration: 5000,
|
439
|
+
out_queue_size: 5,
|
440
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
describe "#enq" do
|
445
|
+
context "when batch is closed based on item count/size" do
|
446
|
+
before :each do
|
447
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
448
|
+
max_batch_count: 5, max_batch_size: 10, buffer_duration: 0,
|
449
|
+
out_queue_size: 5,
|
450
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
451
|
+
end
|
452
|
+
|
453
|
+
context "when the number of items is less than max batch count" do
|
454
|
+
it "should accept an item" do
|
455
|
+
@buffer.enq("ab")
|
456
|
+
@buffer.in_batch.should eql(["ab"])
|
457
|
+
@buffer.in_size.should == 2
|
458
|
+
@buffer.in_count.should == 1
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
context "when the number of items is equal to the max batch size" do
|
463
|
+
it "should batch items to the out queue and current batch is empty" do
|
464
|
+
5.times do |i| @buffer.enq("#{i}") end
|
465
|
+
@buffer.in_batch.should eql([])
|
466
|
+
@buffer.out_queue.deq(true).should eql(["0", "1", "2", "3", "4"])
|
467
|
+
@buffer.in_size.should == 0
|
468
|
+
@buffer.in_count.should == 0
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
context "when the number of items is greater than the max batch size" do
|
473
|
+
it "should batch items to the out queue" do
|
474
|
+
6.times do |i| @buffer.enq("#{i}") end
|
475
|
+
@buffer.in_batch.should eql(["5"])
|
476
|
+
@buffer.out_queue.deq(true).should eql(["0", "1", "2", "3", "4"])
|
477
|
+
@buffer.in_size.should eql(1)
|
478
|
+
@buffer.in_count.should eql(1)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
context "when the size of items is equal to the max batch size" do
|
483
|
+
it "should batch items to the out queue and current batch is empty" do
|
484
|
+
2.times do |i| @buffer.enq("abcd#{i}") end
|
485
|
+
@buffer.in_batch.should eql([])
|
486
|
+
@buffer.out_queue.deq(true).should eql(["abcd0", "abcd1"])
|
487
|
+
@buffer.in_size.should == 0
|
488
|
+
@buffer.in_count.should == 0
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
context "when the size of items is greater than max batch size" do
|
493
|
+
it "should batch items to the out queue" do
|
494
|
+
3.times do |i| @buffer.enq("abc#{i}") end
|
495
|
+
@buffer.in_batch.should eql(["abc2"])
|
496
|
+
@buffer.out_queue.deq(true).should eql(["abc0", "abc1"])
|
497
|
+
@buffer.in_size.should == 4
|
498
|
+
@buffer.in_count.should == 1
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
context "when batch is closed when it passes buffer_duration milliseconds" do
|
504
|
+
before :each do
|
505
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
506
|
+
max_batch_count: 5, max_batch_size: 10, buffer_duration: 1000,
|
507
|
+
out_queue_size: 5,
|
508
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
509
|
+
end
|
510
|
+
|
511
|
+
context "when the number of items is less than max batch count and size is less than batch size" do
|
512
|
+
it "should batch items to the out queue" do
|
513
|
+
@buffer.enq("ab")
|
514
|
+
sleep(2)
|
515
|
+
@buffer.in_batch.should eql([])
|
516
|
+
@buffer.out_queue.deq(true).should eql(["ab"])
|
517
|
+
@buffer.in_size.should == 0
|
518
|
+
@buffer.in_count.should == 0
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
context "when batching is determined by size/count/duration" do
|
524
|
+
context "when enough items are added within buffer_duration milliseconds" do
|
525
|
+
it "should not add batch whose size or count is less than the threshold to the out queue except the first and last batch" do
|
526
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
527
|
+
max_batch_count: 5, max_batch_size: 100, buffer_duration: 1000,
|
528
|
+
out_queue_size: 5,
|
529
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
530
|
+
20.times do |i|
|
531
|
+
@buffer.enq("#{i}")
|
532
|
+
# sleep less than buffer_duration, 5 items take 0.5 seconds to fill the buffer
|
533
|
+
sleep(0.1)
|
534
|
+
end
|
535
|
+
sleep(2)
|
536
|
+
# There will be 4 to 5 batches
|
537
|
+
batches = []
|
538
|
+
while !@buffer.out_queue.empty? do
|
539
|
+
batches << @buffer.out_queue.deq(true)
|
540
|
+
end
|
541
|
+
batches.size.should >= 4
|
542
|
+
batches.size.should <= 5
|
543
|
+
batches.shift
|
544
|
+
batches.pop
|
545
|
+
batches.each do |batch|
|
546
|
+
batch.size.should == 5
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
describe "#close" do
|
554
|
+
it "should add last batch to out queue" do
|
555
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
556
|
+
max_batch_count: 5, max_batch_size: 100, buffer_duration: 1000,
|
557
|
+
out_queue_size: 5,
|
558
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
559
|
+
consumer = Thread.new do
|
560
|
+
@buffer.deq {}
|
561
|
+
end
|
562
|
+
33.times do |i|
|
563
|
+
@buffer.enq("#{i}")
|
564
|
+
sleep(0.01)
|
565
|
+
end
|
566
|
+
@buffer.close
|
567
|
+
@buffer.in_count.should == 0
|
568
|
+
consumer.join
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
describe "#deq" do
|
573
|
+
it "should keep processing items until the buffer is closed" do
|
574
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
575
|
+
max_batch_count: 5, max_batch_size: 100, buffer_duration: 1000,
|
576
|
+
out_queue_size: 5,
|
577
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
578
|
+
item_count = 0
|
579
|
+
consumer = Thread.new do
|
580
|
+
@buffer.deq do |batch|
|
581
|
+
item_count += batch.size
|
582
|
+
end
|
583
|
+
end
|
584
|
+
33.times do |i|
|
585
|
+
@buffer.enq("#{i}")
|
586
|
+
sleep(0.01)
|
587
|
+
end
|
588
|
+
@buffer.close
|
589
|
+
consumer.join
|
590
|
+
item_count.should == 33
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
it "should not miss any item" do
|
595
|
+
@buffer = LogStash::Outputs::CloudWatchLogs::Buffer.new(
|
596
|
+
max_batch_count: 137, max_batch_size: 1000, buffer_duration: 5000,
|
597
|
+
out_queue_size: 50,
|
598
|
+
size_of_item_proc: Proc.new {|item| item.bytesize})
|
599
|
+
item_count = 0
|
600
|
+
consumer = Thread.new do
|
601
|
+
@buffer.deq do |batch|
|
602
|
+
item_count += batch.size
|
603
|
+
end
|
604
|
+
end
|
605
|
+
threads = []
|
606
|
+
num_of_threads = 100
|
607
|
+
num_of_items = 10000
|
608
|
+
# 100 threads plus one scheduled batcher thread
|
609
|
+
num_of_threads.times do |m|
|
610
|
+
threads << Thread.new do
|
611
|
+
num_of_items.times do |i|
|
612
|
+
@buffer.enq("#{i}")
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
# let producers complete the writes
|
617
|
+
threads.map(&:join)
|
618
|
+
# move all items to the out queue
|
619
|
+
@buffer.close
|
620
|
+
# let consumer complete the read
|
621
|
+
consumer.join
|
622
|
+
item_count.should == num_of_items * num_of_threads
|
623
|
+
end
|
624
|
+
end
|