logstash-output-newrelic 2.0.0.pre.beta-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 +2 -0
- data/CONTRIBUTORS +10 -0
- data/DEVELOPER.md +37 -0
- data/Gemfile +18 -0
- data/LICENSE +201 -0
- data/README.md +96 -0
- data/lib/logstash/outputs/config/bigdecimal_patch.rb +24 -0
- data/lib/logstash/outputs/exception/error.rb +14 -0
- data/lib/logstash/outputs/newrelic.rb +276 -0
- data/lib/logstash/outputs/newrelic_version/version.rb +7 -0
- data/logstash-output-newrelic.gemspec +36 -0
- data/spec/outputs/input_17997_messages_resulting_in_2680KB_compressed_payload.json +17997 -0
- data/spec/outputs/input_5000_messages_resulting_in_740KB_compressed_payload.json +5000 -0
- data/spec/outputs/newrelic_spec.rb +603 -0
- data/spec/outputs/single_input_message_exceeeding_1MB_once_compressed.json +1 -0
- metadata +175 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require "logstash/devutils/rspec/spec_helper"
|
|
3
|
+
require "logstash/outputs/newrelic"
|
|
4
|
+
require "logstash/outputs/newrelic_version/version"
|
|
5
|
+
require "logstash/codecs/plain"
|
|
6
|
+
require "logstash/event"
|
|
7
|
+
require "thread"
|
|
8
|
+
require "manticore"
|
|
9
|
+
require "webmock/rspec"
|
|
10
|
+
require "zlib"
|
|
11
|
+
require "rspec/wait"
|
|
12
|
+
|
|
13
|
+
# Configure WebMock to work with Manticore
|
|
14
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
|
15
|
+
|
|
16
|
+
# Monkey-patch Manticore to capture request bodies for testing
|
|
17
|
+
module ManticoreRequestCapture
|
|
18
|
+
@@captured_bodies = []
|
|
19
|
+
|
|
20
|
+
def self.captured_bodies
|
|
21
|
+
@@captured_bodies
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.clear
|
|
25
|
+
@@captured_bodies.clear
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.last_body
|
|
29
|
+
@@captured_bodies.last
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Patch Manticore::Client to capture bodies before WebMock intercepts
|
|
34
|
+
if defined?(::Manticore)
|
|
35
|
+
Manticore::Client.class_eval do
|
|
36
|
+
alias_method :original_post, :post
|
|
37
|
+
|
|
38
|
+
def post(url, options = {})
|
|
39
|
+
# Capture the body before the request, preserving binary encoding
|
|
40
|
+
if options[:body]
|
|
41
|
+
body = options[:body].dup
|
|
42
|
+
# Force binary encoding to prevent corruption of gzipped data
|
|
43
|
+
body.force_encoding('BINARY') if body.respond_to?(:force_encoding)
|
|
44
|
+
ManticoreRequestCapture.captured_bodies << body
|
|
45
|
+
end
|
|
46
|
+
original_post(url, options)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe LogStash::Outputs::NewRelic do
|
|
52
|
+
let (:base_uri) { "https://testing-example-collector.com" }
|
|
53
|
+
let (:retry_seconds) { 0 }
|
|
54
|
+
# Don't sleep in tests, to keep tests fast. We have a test for the method that produces the sleep duration between retries.
|
|
55
|
+
let (:max_delay) { 0 }
|
|
56
|
+
let (:retries) { 3 }
|
|
57
|
+
let (:license_key) { 'cool-guy' }
|
|
58
|
+
let (:simple_config) {
|
|
59
|
+
{
|
|
60
|
+
"base_uri" => base_uri,
|
|
61
|
+
"license_key" => license_key
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
before(:each) do
|
|
66
|
+
ManticoreRequestCapture.clear
|
|
67
|
+
@newrelic_output = LogStash::Plugin.lookup("output", "newrelic").new(simple_config)
|
|
68
|
+
@newrelic_output.register
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
after(:each) do
|
|
72
|
+
if @newrelic_output
|
|
73
|
+
@newrelic_output.shutdown
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context "license key tests" do
|
|
78
|
+
it "sets license key when given in the header" do
|
|
79
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
80
|
+
|
|
81
|
+
event = LogStash::Event.new({:message => "Test message" })
|
|
82
|
+
@newrelic_output.multi_receive([event])
|
|
83
|
+
|
|
84
|
+
wait_for(a_request(:post, base_uri)
|
|
85
|
+
.with(headers: {
|
|
86
|
+
"X-License-Key" => license_key,
|
|
87
|
+
"X-Event-Source" => "logs",
|
|
88
|
+
"Content-Encoding" => "gzip",
|
|
89
|
+
})).to have_been_made
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "check https connection scheme" do
|
|
94
|
+
it "uses https by default" do
|
|
95
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
96
|
+
|
|
97
|
+
event = LogStash::Event.new({:message => "Test message" })
|
|
98
|
+
@newrelic_output.multi_receive([event])
|
|
99
|
+
|
|
100
|
+
wait_for(a_request(:post, base_uri)
|
|
101
|
+
.with(headers: {
|
|
102
|
+
"X-License-Key" => license_key,
|
|
103
|
+
"X-Event-Source" => "logs",
|
|
104
|
+
"Content-Encoding" => "gzip",
|
|
105
|
+
})).to have_been_made
|
|
106
|
+
|
|
107
|
+
# Check if the requests were made using HTTPS
|
|
108
|
+
expect(WebMock).to have_requested(:post, base_uri).with { |req| req.uri.scheme == 'https' }
|
|
109
|
+
expect(WebMock).to have_requested(:post, base_uri).with { |req| req.uri.port == 443 }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context "check http connection scheme" do
|
|
114
|
+
it "uses http when http config is set" do
|
|
115
|
+
stub_request(:any, "http://localhost:5000/").to_return(status: 200)
|
|
116
|
+
@newrelic_output = LogStash::Plugin.lookup("output", "newrelic").new({
|
|
117
|
+
"base_uri" => "http://localhost:5000/",
|
|
118
|
+
"license_key" => license_key
|
|
119
|
+
})
|
|
120
|
+
@newrelic_output.register
|
|
121
|
+
event = LogStash::Event.new({:message => "Test message" })
|
|
122
|
+
@newrelic_output.multi_receive([event])
|
|
123
|
+
|
|
124
|
+
wait_for(a_request(:post, "http://localhost:5000/")
|
|
125
|
+
.with(headers: {
|
|
126
|
+
"X-License-Key" => license_key,
|
|
127
|
+
"X-Event-Source" => "logs",
|
|
128
|
+
"Content-Encoding" => "gzip",
|
|
129
|
+
})).to have_been_made
|
|
130
|
+
|
|
131
|
+
# Check if the requests were made using HTTP to this endpoint
|
|
132
|
+
expect(WebMock).to have_requested(:post, "http://localhost:5000/").with { |req| req.uri.scheme == 'http' }
|
|
133
|
+
expect(WebMock).to have_requested(:post, "http://localhost:5000/").with { |req| req.uri.port == 5000 }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe LogStash::Outputs::NewRelic do
|
|
139
|
+
let (:api_key) { "someAccountKey" }
|
|
140
|
+
let (:base_uri) { "https://testing-example-collector.com" }
|
|
141
|
+
let (:retry_seconds) { 0 }
|
|
142
|
+
# Don't sleep in tests, to keep tests fast. We have a test for the method that produces the sleep duration between retries.
|
|
143
|
+
let (:max_delay) { 0 }
|
|
144
|
+
let (:retries) { 3 }
|
|
145
|
+
let (:simple_config) {
|
|
146
|
+
{
|
|
147
|
+
"api_key" => api_key,
|
|
148
|
+
"base_uri" => base_uri,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# An arbitrary time to use in these tests, with different representations
|
|
153
|
+
class FixedTime
|
|
154
|
+
MILLISECONDS = 1562888528123
|
|
155
|
+
ISO_8601_STRING_TIME = '2019-07-11T23:42:08.123Z'
|
|
156
|
+
LOGSTASH_TIMESTAMP = LogStash::Timestamp.coerce(ISO_8601_STRING_TIME)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def gunzip(bytes)
|
|
160
|
+
return bytes if bytes.nil? || bytes.empty?
|
|
161
|
+
|
|
162
|
+
# For Manticore, the body might come as a Java byte array or InputStream
|
|
163
|
+
# Convert to string if needed
|
|
164
|
+
if bytes.respond_to?(:java_class)
|
|
165
|
+
# Handle Java types (byte array, InputStream, etc)
|
|
166
|
+
bytes = String.from_java_bytes(bytes) if bytes.respond_to?(:to_a)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
bytes = bytes.to_s unless bytes.is_a?(String)
|
|
170
|
+
|
|
171
|
+
# Ensure binary encoding
|
|
172
|
+
bytes.force_encoding('BINARY')
|
|
173
|
+
|
|
174
|
+
byte0 = bytes.getbyte(0)
|
|
175
|
+
byte1 = bytes.getbyte(1)
|
|
176
|
+
|
|
177
|
+
# Check for gzip magic bytes (0x1f 0x8b)
|
|
178
|
+
if bytes.length >= 2 && byte0 == 0x1f && byte1 == 0x8b
|
|
179
|
+
sio = StringIO.new(bytes)
|
|
180
|
+
gz = Zlib::GzipReader.new(sio)
|
|
181
|
+
result = gz.read
|
|
182
|
+
gz.close
|
|
183
|
+
result
|
|
184
|
+
else
|
|
185
|
+
# Not gzipped, return as-is
|
|
186
|
+
bytes
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def single_gzipped_message(body)
|
|
191
|
+
decompressed = gunzip(body)
|
|
192
|
+
message = JSON.parse(decompressed)[0]['logs']
|
|
193
|
+
expect(message.length).to equal(1)
|
|
194
|
+
message[0]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def multiple_gzipped_messages(body)
|
|
198
|
+
JSON.parse(gunzip(body))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def now_in_milliseconds()
|
|
202
|
+
(Time.now.to_f * 1000).to_i # to_f gives seconds with a fractional portion
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def within_five_seconds_of(time_in_millis, expected_in_millis)
|
|
206
|
+
five_seconds_in_millis = 5 * 1000
|
|
207
|
+
(time_in_millis - expected_in_millis).abs < five_seconds_in_millis
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
before(:each) do
|
|
212
|
+
ManticoreRequestCapture.clear
|
|
213
|
+
@newrelic_output = LogStash::Plugin.lookup("output", "newrelic").new(simple_config)
|
|
214
|
+
@newrelic_output.register
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
after(:each) do
|
|
218
|
+
if @newrelic_output
|
|
219
|
+
@newrelic_output.shutdown
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
context "validation of config" do
|
|
224
|
+
it "requires api_key" do
|
|
225
|
+
no_api_key_config = {
|
|
226
|
+
}
|
|
227
|
+
output = LogStash::Plugin.lookup("output", "newrelic").new(no_api_key_config)
|
|
228
|
+
expect { output.register }.to raise_error LogStash::ConfigurationError
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
context "request headers" do
|
|
233
|
+
it "all present" do
|
|
234
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
235
|
+
|
|
236
|
+
event = LogStash::Event.new({:message => "Test message" })
|
|
237
|
+
@newrelic_output.multi_receive([event])
|
|
238
|
+
|
|
239
|
+
wait_for(a_request(:post, base_uri)
|
|
240
|
+
.with(headers: {
|
|
241
|
+
"X-Insert-Key" => api_key,
|
|
242
|
+
"X-Event-Source" => "logs",
|
|
243
|
+
"Content-Encoding" => "gzip",
|
|
244
|
+
})).to have_been_made
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
context "request body" do
|
|
249
|
+
|
|
250
|
+
it "message contains plugin information" do
|
|
251
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
252
|
+
|
|
253
|
+
event = LogStash::Event.new({ :message => "Test message" })
|
|
254
|
+
@newrelic_output.multi_receive([event])
|
|
255
|
+
|
|
256
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
257
|
+
data = multiple_gzipped_messages(ManticoreRequestCapture.last_body)[0]
|
|
258
|
+
expect(data['common']['attributes']['plugin']['type']).to eq('logstash')
|
|
259
|
+
expect(data['common']['attributes']['plugin']['version']).to eq(LogStash::Outputs::NewRelicVersion::VERSION)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "all other fields passed through as is" do
|
|
263
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
264
|
+
|
|
265
|
+
event = LogStash::Event.new({ :message => "Test message", :other => "Other value" })
|
|
266
|
+
@newrelic_output.multi_receive([event])
|
|
267
|
+
|
|
268
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
269
|
+
captured_body = ManticoreRequestCapture.last_body
|
|
270
|
+
|
|
271
|
+
puts "DEBUG: Captured body class: #{captured_body.class}"
|
|
272
|
+
puts "DEBUG: Captured body length: #{captured_body.length rescue 'N/A'}"
|
|
273
|
+
puts "DEBUG: First 20 bytes: #{captured_body.to_s[0..19].bytes.map{|b| "\\x%02X" % b}.join rescue 'N/A'}"
|
|
274
|
+
|
|
275
|
+
message = single_gzipped_message(captured_body)
|
|
276
|
+
expect(message['message']).to eq('Test message')
|
|
277
|
+
expect(message['attributes']['other']).to eq('Other value')
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "JSON object 'message' field is not parsed" do
|
|
281
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
282
|
+
|
|
283
|
+
message_json = '{ "in-json-1": "1", "in-json-2": "2", "sub-object": {"in-json-3": "3"} }'
|
|
284
|
+
event = LogStash::Event.new({ :message => message_json, :other => "Other value" })
|
|
285
|
+
@newrelic_output.multi_receive([event])
|
|
286
|
+
|
|
287
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
288
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
289
|
+
expect(message['message']).to eq(message_json)
|
|
290
|
+
expect(message['attributes']['other']).to eq('Other value')
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it "JSON array 'message' field is not parsed, left as is" do
|
|
294
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
295
|
+
|
|
296
|
+
message_json_array = '[{ "in-json-1": "1", "in-json-2": "2", "sub-object": {"in-json-3": "3"} }]'
|
|
297
|
+
event = LogStash::Event.new({ :message => message_json_array, :other => "Other value" })
|
|
298
|
+
@newrelic_output.multi_receive([event])
|
|
299
|
+
|
|
300
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
301
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
302
|
+
expect(message['message']).to eq(message_json_array)
|
|
303
|
+
expect(message['attributes']['other']).to eq('Other value')
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it "JSON string 'message' field is not parsed, left as is" do
|
|
307
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
308
|
+
|
|
309
|
+
message_json_string = '"I can be parsed as JSON"'
|
|
310
|
+
event = LogStash::Event.new({ :message => message_json_string, :other => "Other value" })
|
|
311
|
+
@newrelic_output.multi_receive([event])
|
|
312
|
+
|
|
313
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
314
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
315
|
+
expect(message['message']).to eq(message_json_string)
|
|
316
|
+
expect(message['attributes']['other']).to eq('Other value')
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it "other JSON fields are not parsed" do
|
|
320
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
321
|
+
|
|
322
|
+
other_json = '{ "key": "value" }'
|
|
323
|
+
event = LogStash::Event.new({ :message => "Test message", :other => other_json })
|
|
324
|
+
@newrelic_output.multi_receive([event])
|
|
325
|
+
|
|
326
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
327
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
328
|
+
expect(message['message']).to eq('Test message')
|
|
329
|
+
expect(message['attributes']['other']).to eq(other_json)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "handles messages without a 'message' field" do
|
|
333
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
334
|
+
|
|
335
|
+
event = LogStash::Event.new({ :other => 'Other value' })
|
|
336
|
+
@newrelic_output.multi_receive([event])
|
|
337
|
+
|
|
338
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
339
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
340
|
+
expect(message['attributes']['other']).to eq('Other value')
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "zero events should not cause an HTTP call" do
|
|
344
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
345
|
+
|
|
346
|
+
@newrelic_output.multi_receive([])
|
|
347
|
+
|
|
348
|
+
# Shut down the plugin so that it has the chance to send a request
|
|
349
|
+
# (since we're verifying that nothing is sent)
|
|
350
|
+
@newrelic_output.shutdown
|
|
351
|
+
|
|
352
|
+
expect(a_request(:post, base_uri))
|
|
353
|
+
.not_to have_been_made
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it "multiple events" do
|
|
357
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
358
|
+
|
|
359
|
+
event1 = LogStash::Event.new({ "message" => "Test message 1" })
|
|
360
|
+
event2 = LogStash::Event.new({ "message" => "Test message 2" })
|
|
361
|
+
@newrelic_output.multi_receive([event1, event2])
|
|
362
|
+
|
|
363
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
364
|
+
messages = multiple_gzipped_messages(ManticoreRequestCapture.last_body)[0]['logs']
|
|
365
|
+
expect(messages.length).to eq(2)
|
|
366
|
+
expect(messages[0]['message']).to eq('Test message 1')
|
|
367
|
+
expect(messages[1]['message']).to eq('Test message 2')
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
context "error handling and retry logic" do
|
|
372
|
+
it "continues through errors, future calls should still succeed" do
|
|
373
|
+
stub_request(:any, base_uri)
|
|
374
|
+
.to_raise(StandardError.new("from test"))
|
|
375
|
+
.to_return(status: 200)
|
|
376
|
+
|
|
377
|
+
event1 = LogStash::Event.new({ "message" => "Test message 1" })
|
|
378
|
+
event2 = LogStash::Event.new({ "message" => "Test message 2" })
|
|
379
|
+
@newrelic_output.multi_receive([event1])
|
|
380
|
+
@newrelic_output.multi_receive([event2])
|
|
381
|
+
|
|
382
|
+
wait_for { ManticoreRequestCapture.captured_bodies.length }.to be >= 2
|
|
383
|
+
message = single_gzipped_message(ManticoreRequestCapture.captured_bodies.last)
|
|
384
|
+
expect(message['message']).to eq('Test message 2')
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
[
|
|
388
|
+
{ "returned_status_code" => 200, "expected_to_retry" => false },
|
|
389
|
+
{ "returned_status_code" => 202, "expected_to_retry" => false },
|
|
390
|
+
{ "returned_status_code" => 400, "expected_to_retry" => false },
|
|
391
|
+
{ "returned_status_code" => 404, "expected_to_retry" => false },
|
|
392
|
+
{ "returned_status_code" => 408, "expected_to_retry" => true },
|
|
393
|
+
{ "returned_status_code" => 429, "expected_to_retry" => true },
|
|
394
|
+
{ "returned_status_code" => 500, "expected_to_retry" => true },
|
|
395
|
+
{ "returned_status_code" => 502, "expected_to_retry" => true },
|
|
396
|
+
{ "returned_status_code" => 503, "expected_to_retry" => true },
|
|
397
|
+
{ "returned_status_code" => 504, "expected_to_retry" => true },
|
|
398
|
+
{ "returned_status_code" => 599, "expected_to_retry" => true }
|
|
399
|
+
].each do |test_case|
|
|
400
|
+
returned_status_code = test_case["returned_status_code"]
|
|
401
|
+
expected_to_retry = test_case["expected_to_retry"]
|
|
402
|
+
|
|
403
|
+
it "should #{expected_to_retry ? "" : "not"} retry on status code #{returned_status_code}" do
|
|
404
|
+
request_count = 0
|
|
405
|
+
stub_request(:any, base_uri)
|
|
406
|
+
.to_return do |request|
|
|
407
|
+
request_count += 1
|
|
408
|
+
{ status: request_count == 1 ? returned_status_code : 200 }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
logstash_event = LogStash::Event.new({ "message" => "Test message" })
|
|
412
|
+
@newrelic_output.multi_receive([logstash_event])
|
|
413
|
+
|
|
414
|
+
expected_retries = expected_to_retry ? 2 : 1
|
|
415
|
+
wait_for { request_count }.to eq(expected_retries)
|
|
416
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
417
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
418
|
+
expect(message['message']).to eq('Test message')
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it "does not retry when max_retries is set to 0" do
|
|
423
|
+
@newrelic_output = LogStash::Plugin.lookup("output", "newrelic").new(
|
|
424
|
+
{ "base_uri" => base_uri, "license_key" => api_key, "max_retries" => '0' }
|
|
425
|
+
)
|
|
426
|
+
@newrelic_output.register
|
|
427
|
+
request_count = 0
|
|
428
|
+
stub_request(:any, base_uri).to_return do |request|
|
|
429
|
+
request_count += 1
|
|
430
|
+
{ status: 500 }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
event1 = LogStash::Event.new({ "message" => "Test message 1" })
|
|
434
|
+
@newrelic_output.multi_receive([event1])
|
|
435
|
+
# Due the async behavior we need to wait to be sure that the method was not called more than 1 time
|
|
436
|
+
sleep(2)
|
|
437
|
+
expect(request_count).to eq(1)
|
|
438
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
439
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
440
|
+
expect(message['message']).to eq('Test message 1')
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it "retries when receive a not expected exception" do
|
|
444
|
+
request_count = 0
|
|
445
|
+
stub_request(:any, base_uri).to_return do |request|
|
|
446
|
+
request_count += 1
|
|
447
|
+
raise StandardError.new("from test") if request_count == 1
|
|
448
|
+
{ status: 200 }
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
event1 = LogStash::Event.new({ "message" => "Test message 1" })
|
|
452
|
+
@newrelic_output.multi_receive([event1])
|
|
453
|
+
wait_for { request_count }.to eq(2)
|
|
454
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
455
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
456
|
+
expect(message['message']).to eq('Test message 1')
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "performs the configured amount of retries, no more, no less" do
|
|
460
|
+
@newrelic_output = LogStash::Plugin.lookup("output", "newrelic").new(
|
|
461
|
+
{ "base_uri" => base_uri, "license_key" => api_key, "max_retries" => '3' }
|
|
462
|
+
)
|
|
463
|
+
@newrelic_output.register
|
|
464
|
+
request_count = 0
|
|
465
|
+
stub_request(:any, base_uri).to_return do |request|
|
|
466
|
+
request_count += 1
|
|
467
|
+
{ status: request_count <= 3 ? 500 : 200 }
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
event1 = LogStash::Event.new({ "message" => "Test message" })
|
|
471
|
+
@newrelic_output.multi_receive([event1])
|
|
472
|
+
|
|
473
|
+
wait_for { request_count }.to eq(3)
|
|
474
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
475
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
476
|
+
expect(message['message']).to eq('Test message')
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
context "JSON serialization" do
|
|
481
|
+
it "serializes floating point numbers as floating point numbers" do
|
|
482
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
483
|
+
|
|
484
|
+
event = LogStash::Event.new({ "floatingpoint" => 0.12345 })
|
|
485
|
+
@newrelic_output.multi_receive([event])
|
|
486
|
+
|
|
487
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
488
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
489
|
+
expect(message['attributes']['floatingpoint']).to eq(0.12345)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
it "serializes BigDecimals as floating point numbers" do
|
|
493
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
494
|
+
|
|
495
|
+
event = LogStash::Event.new({ "bigdecimal" => BigDecimal('0.12345') })
|
|
496
|
+
@newrelic_output.multi_receive([event])
|
|
497
|
+
|
|
498
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
499
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
500
|
+
expect(message['attributes']['bigdecimal']).to eq(0.12345)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
it "serializes NaN as null" do
|
|
504
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
505
|
+
|
|
506
|
+
event = LogStash::Event.new({ "nan" => BigDecimal('NaN') })
|
|
507
|
+
@newrelic_output.multi_receive([event])
|
|
508
|
+
|
|
509
|
+
wait_for { ManticoreRequestCapture.last_body }.not_to be_nil
|
|
510
|
+
message = single_gzipped_message(ManticoreRequestCapture.last_body)
|
|
511
|
+
expect(message['attributes']['nan']).to be_nil
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
context "payload splitting" do
|
|
516
|
+
|
|
517
|
+
def collect_msg_ids_from_captured_bodies
|
|
518
|
+
ManticoreRequestCapture.captured_bodies.flat_map do |body|
|
|
519
|
+
extract_msg_ids(body)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Tests using this method expect log messages to contain a field "msgId" in their logs
|
|
524
|
+
def extract_msg_ids(body)
|
|
525
|
+
JSON.parse(gunzip(body))[0]['logs'].map do |log|
|
|
526
|
+
log['attributes']['msgId']
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def expect_msg_ids(captured_msg_ids, msgIdsCount)
|
|
531
|
+
wait_for { captured_msg_ids.length }.to eq(msgIdsCount), "Expected a total of #{msgIdsCount} logs, but found #{captured_msg_ids.length}"
|
|
532
|
+
sorted_captured_msg_ids = captured_msg_ids.sort
|
|
533
|
+
for i in 0...msgIdsCount do
|
|
534
|
+
expect(sorted_captured_msg_ids[i]).to eq(i), "msgId #{i} not found"
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
it "splits logs into up to 1MB payloads" do
|
|
539
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
540
|
+
|
|
541
|
+
# This file contains 17997 random log record messages that upon compressed ends up being about 2.68MB
|
|
542
|
+
# Each log record is pretty small (no single log exceeds 1MB alone), so, we expect it to perform 4 requests to the API
|
|
543
|
+
# with
|
|
544
|
+
file_path = 'spec/outputs/input_17997_messages_resulting_in_2680KB_compressed_payload.json'
|
|
545
|
+
|
|
546
|
+
logstash_events = File.readlines(file_path).map do |line|
|
|
547
|
+
LogStash::Event.new(JSON.parse(line))
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
@newrelic_output.multi_receive(logstash_events)
|
|
551
|
+
|
|
552
|
+
# Verify number of requests matches exactly 4. Note that .times() unexpectedly behaves as .at_least_times(), so we
|
|
553
|
+
# are forced to do this double verification to check the exact number of calls.
|
|
554
|
+
wait_for(a_request(:post, base_uri)).to have_been_made.at_least_times(4)
|
|
555
|
+
wait_for(a_request(:post, base_uri)).to have_been_made.at_most_times(4)
|
|
556
|
+
|
|
557
|
+
# Verify all expected msgIds were received
|
|
558
|
+
captured_msg_ids = collect_msg_ids_from_captured_bodies
|
|
559
|
+
expect_msg_ids(captured_msg_ids, 17997)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it "does not split a log and does not perform any request if it exceeds 1MB once compressed" do
|
|
563
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
564
|
+
|
|
565
|
+
# This file contains a SINGLE random log record that upon compressed ends up being about 1.8MB
|
|
566
|
+
# This payload cannot be further split, so we expect no call being made to the Logs API
|
|
567
|
+
file_path = 'spec/outputs/single_input_message_exceeeding_1MB_once_compressed.json'
|
|
568
|
+
|
|
569
|
+
logstash_events = []
|
|
570
|
+
File.foreach(file_path) do |line|
|
|
571
|
+
logstash_events << LogStash::Event.new(JSON.parse(line))
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
@newrelic_output.multi_receive(logstash_events)
|
|
575
|
+
|
|
576
|
+
wait_for(a_request(:post, base_uri)).not_to have_been_made
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
it "does a single request when the payload is below 1MB" do
|
|
580
|
+
stub_request(:any, base_uri).to_return(status: 200)
|
|
581
|
+
|
|
582
|
+
# This file contains 5000 random log record messages that upon compressed ends up being about 0.74MB
|
|
583
|
+
# Given that this is below the 1MB allowed by the Logs API, a single request will be made
|
|
584
|
+
file_path = 'spec/outputs/input_5000_messages_resulting_in_740KB_compressed_payload.json'
|
|
585
|
+
|
|
586
|
+
logstash_events = []
|
|
587
|
+
File.foreach(file_path) do |line|
|
|
588
|
+
logstash_events << LogStash::Event.new(JSON.parse(line))
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
@newrelic_output.multi_receive(logstash_events)
|
|
592
|
+
|
|
593
|
+
# Verify number of requests matches exactly 1. Note that .times() unexpectedly behaves as .at_least_times(), so we
|
|
594
|
+
# are forced to do this double verification to check the exact number of calls.
|
|
595
|
+
wait_for(a_request(:post, base_uri)).to have_been_made.at_least_times(1)
|
|
596
|
+
wait_for(a_request(:post, base_uri)).to have_been_made.at_most_times(1)
|
|
597
|
+
|
|
598
|
+
# Verify all expected msgIds were received
|
|
599
|
+
captured_msg_ids = collect_msg_ids_from_captured_bodies
|
|
600
|
+
expect_msg_ids(captured_msg_ids, 5000)
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
end
|