logstash-output-googlecloudstorage 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-googlecloudstorage'
4
+ s.version = '2.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Writes events to Google cloud storage "
7
+ 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"
8
+ s.authors = ["Elastic"]
9
+ s.email = 'shailesh@kontext.in'
10
+ s.homepage = "http://www.elastic.co/guide/en/logstash/current/index.html"
11
+ s.require_paths = ["lib"]
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" => "output" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 2.0.0", "< 2.99"
24
+ s.add_runtime_dependency 'logstash-codec-json_lines'
25
+ s.add_runtime_dependency 'logstash-codec-line'
26
+ s.add_runtime_dependency 'google-api-client', '~> 0.8.7' # version 0.9.x works only with ruby 2.x
27
+
28
+ s.add_development_dependency 'logstash-devutils'
29
+ s.add_development_dependency 'flores'
30
+ s.add_development_dependency 'logstash-input-generator'
31
+ end
@@ -0,0 +1,467 @@
1
+ # encoding: UTF-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/googlecloudstorage"
4
+ require "logstash/codecs/line"
5
+ require "logstash/codecs/json_lines"
6
+ require "logstash/event"
7
+ require "logstash/json"
8
+ require "stud/temporary"
9
+ require "tempfile"
10
+ require "uri"
11
+ require "fileutils"
12
+ require "flores/random"
13
+
14
+ describe LogStash::Outputs::GoogleCloudStorage do
15
+ describe "ship lots of events to a file" do
16
+ tmp_file = Tempfile.new('logstash-spec-output-file')
17
+ event_count = 10000 + rand(500)
18
+
19
+ config <<-CONFIG
20
+ input {
21
+ generator {
22
+ message => "hello world"
23
+ count => #{event_count}
24
+ type => "generator"
25
+ }
26
+ }
27
+ output {
28
+ file {
29
+ path => "#{tmp_file.path}"
30
+ }
31
+ }
32
+ CONFIG
33
+
34
+ agent do
35
+ line_num = 0
36
+
37
+ # Now check all events for order and correctness.
38
+ events = tmp_file.map {|line| LogStash::Event.new(LogStash::Json.load(line))}
39
+ sorted = events.sort_by {|e| e.get('sequence')}
40
+ sorted.each do |event|
41
+ insist {event.get("message")} == "hello world"
42
+ insist {event.get("sequence")} == line_num
43
+ line_num += 1
44
+ end
45
+
46
+ insist {line_num} == event_count
47
+ end # agent
48
+ end
49
+
50
+ describe "ship lots of events to a file gzipped" do
51
+ Stud::Temporary.file('logstash-spec-output-file') do |tmp_file|
52
+ event_count = 100000 + rand(500)
53
+
54
+ config <<-CONFIG
55
+ input {
56
+ generator {
57
+ message => "hello world"
58
+ count => #{event_count}
59
+ type => "generator"
60
+ }
61
+ }
62
+ output {
63
+ file {
64
+ path => "#{tmp_file.path}"
65
+ gzip => true
66
+ }
67
+ }
68
+ CONFIG
69
+
70
+ agent do
71
+ line_num = 0
72
+ # Now check all events for order and correctness.
73
+ events = Zlib::GzipReader.open(tmp_file.path).map {|line| LogStash::Event.new(LogStash::Json.load(line)) }
74
+ sorted = events.sort_by {|e| e.get("sequence")}
75
+ sorted.each do |event|
76
+ insist {event.get("message")} == "hello world"
77
+ insist {event.get("sequence")} == line_num
78
+ line_num += 1
79
+ end
80
+ insist {line_num} == event_count
81
+ end # agent
82
+ end
83
+ end
84
+
85
+ describe "#register" do
86
+ let(:path) { '/%{name}' }
87
+ let(:output) { LogStash::Outputs::GoogleCloudStorage.new({ "path" => path }) }
88
+
89
+ it 'doesnt allow the path to start with a dynamic string' do
90
+ expect { output.register }.to raise_error(LogStash::ConfigurationError)
91
+ output.close
92
+ end
93
+
94
+ context 'doesnt allow the root directory to have some dynamic part' do
95
+ ['/a%{name}/',
96
+ '/a %{name}/',
97
+ '/a- %{name}/',
98
+ '/a- %{name}'].each do |test_path|
99
+ it "with path: #{test_path}" do
100
+ path = test_path
101
+ expect { output.register }.to raise_error(LogStash::ConfigurationError)
102
+ output.close
103
+ end
104
+ end
105
+ end
106
+
107
+ it 'allow to have dynamic part after the file root' do
108
+ path = '/tmp/%{name}'
109
+ output = LogStash::Outputs::GoogleCloudStorage.new({ "path" => path })
110
+ expect { output.register }.not_to raise_error
111
+ end
112
+ end
113
+
114
+ describe "receiving events" do
115
+
116
+ context "when write_behavior => 'overwrite'" do
117
+ let(:tmp) { Stud::Temporary.pathname }
118
+ let(:config) {
119
+ {
120
+ "write_behavior" => "overwrite",
121
+ "path" => tmp,
122
+ "codec" => LogStash::Codecs::JSONLines.new,
123
+ "flush_interval" => 0
124
+ }
125
+ }
126
+ let(:output) { LogStash::Outputs::GoogleCloudStorage.new(config) }
127
+
128
+ let(:count) { Flores::Random.integer(1..10) }
129
+ let(:events) do
130
+ Flores::Random.iterations(1..10).collect do |i|
131
+ LogStash::Event.new("value" => i)
132
+ end
133
+ end
134
+
135
+ before do
136
+ output.register
137
+ end
138
+
139
+ after do
140
+ File.unlink(tmp) if File.exist?(tmp)
141
+ end
142
+
143
+ it "should write only the last event of a batch" do
144
+ output.multi_receive(events)
145
+ result = LogStash::Json.load(File.read(tmp))
146
+ expect(result["value"]).to be == events.last.get("value")
147
+ end
148
+
149
+ context "the file" do
150
+ it "should only contain the last event received" do
151
+ events.each do |event|
152
+ output.multi_receive([event])
153
+ result = LogStash::Json.load(File.read(tmp))
154
+ expect(result["value"]).to be == event.get("value")
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ context "when the output file is deleted" do
161
+
162
+ let(:temp_file) { Tempfile.new('logstash-spec-output-file_deleted') }
163
+
164
+ let(:config) do
165
+ { "path" => temp_file.path, "flush_interval" => 0 }
166
+ end
167
+
168
+ it "should recreate the required file if deleted" do
169
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
170
+ output.register
171
+
172
+ 10.times do |i|
173
+ event = LogStash::Event.new("event_id" => i)
174
+ output.multi_receive([event])
175
+ end
176
+ FileUtils.rm(temp_file)
177
+ 10.times do |i|
178
+ event = LogStash::Event.new("event_id" => i+10)
179
+ output.multi_receive([event])
180
+ end
181
+
182
+ expect(FileTest.size(temp_file.path)).to be > 0
183
+ end
184
+
185
+ context "when appending to the error log" do
186
+
187
+ let(:config) do
188
+ { "path" => temp_file.path, "flush_interval" => 0, "create_if_deleted" => false }
189
+ end
190
+
191
+ it "should append the events to the filename_failure location" do
192
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
193
+ output.register
194
+
195
+ 10.times do |i|
196
+ event = LogStash::Event.new("event_id" => i)
197
+ output.multi_receive([event])
198
+ end
199
+ FileUtils.rm(temp_file)
200
+ 10.times do |i|
201
+ event = LogStash::Event.new("event_id" => i+10)
202
+ output.multi_receive([event])
203
+ end
204
+ expect(FileTest.exist?(temp_file.path)).to be_falsey
205
+ expect(FileTest.size(output.failure_path)).to be > 0
206
+ end
207
+
208
+ end
209
+
210
+ end
211
+
212
+ context "when using an interpolated path" do
213
+ context "when trying to write outside the files root directory" do
214
+ let(:bad_event) do
215
+ event = LogStash::Event.new
216
+ event.set('error', '../uncool/directory')
217
+ event
218
+ end
219
+
220
+ it 'writes the bad event in the specified error file' do
221
+ Stud::Temporary.directory('filepath_error') do |path|
222
+ config = {
223
+ "path" => "#{path}/%{error}",
224
+ "filename_failure" => "_error"
225
+ }
226
+
227
+ # Trying to write outside the file root
228
+ outside_path = "#{'../' * path.split(File::SEPARATOR).size}notcool"
229
+ bad_event.set("error", outside_path)
230
+
231
+
232
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
233
+ output.register
234
+ output.multi_receive([bad_event])
235
+
236
+ error_file = File.join(path, config["filename_failure"])
237
+
238
+ expect(File.exist?(error_file)).to eq(true)
239
+ output.close
240
+ end
241
+ end
242
+
243
+ it 'doesnt decode relatives paths urlencoded' do
244
+ Stud::Temporary.directory('filepath_error') do |path|
245
+ encoded_once = "%2E%2E%2ftest" # ../test
246
+ encoded_twice = "%252E%252E%252F%252E%252E%252Ftest" # ../../test
247
+
248
+ output = LogStash::Outputs::GoogleCloudStorage.new({ "path" => "/#{path}/%{error}"})
249
+ output.register
250
+
251
+ bad_event.set('error', encoded_once)
252
+ output.multi_receive([bad_event])
253
+
254
+ bad_event.set('error', encoded_twice)
255
+ output.multi_receive([bad_event])
256
+
257
+ expect(Dir.glob(File.join(path, "*")).size).to eq(2)
258
+ output.close
259
+ end
260
+ end
261
+
262
+ it 'doesnt write outside the file if the path is double escaped' do
263
+ Stud::Temporary.directory('filepath_error') do |path|
264
+ output = LogStash::Outputs::GoogleCloudStorage.new({ "path" => "/#{path}/%{error}"})
265
+ output.register
266
+
267
+ bad_event.set('error', '../..//test')
268
+ output.multi_receive([bad_event])
269
+
270
+ expect(Dir.glob(File.join(path, "*")).size).to eq(1)
271
+ output.close
272
+ end
273
+ end
274
+ end
275
+
276
+ context 'when trying to write inside the file root directory' do
277
+ it 'write the event to the generated filename' do
278
+ good_event = LogStash::Event.new
279
+ good_event.set('error', '42.txt')
280
+
281
+ Stud::Temporary.directory do |path|
282
+ config = { "path" => "#{path}/%{error}" }
283
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
284
+ output.register
285
+ output.multi_receive([good_event])
286
+
287
+ good_file = File.join(path, good_event.get('error'))
288
+ expect(File.exist?(good_file)).to eq(true)
289
+ output.close
290
+ end
291
+ end
292
+
293
+ it 'write the events to a file when some part of a folder or file is dynamic' do
294
+ t = Time.now.utc
295
+ good_event = LogStash::Event.new("@timestamp" => t)
296
+
297
+ Stud::Temporary.directory do |path|
298
+ dynamic_path = "#{path}/failed_syslog-%{+YYYY-MM-dd}"
299
+ expected_path = "#{path}/failed_syslog-#{t.strftime("%Y-%m-%d")}"
300
+
301
+ config = { "path" => dynamic_path }
302
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
303
+ output.register
304
+ output.multi_receive([good_event])
305
+
306
+ expect(File.exist?(expected_path)).to eq(true)
307
+ output.close
308
+ end
309
+ end
310
+
311
+ it 'write the events to the generated path containing multiples fieldref' do
312
+ t = Time.now.utc
313
+ good_event = LogStash::Event.new("error" => 42,
314
+ "@timestamp" => t,
315
+ "level" => "critical",
316
+ "weird_path" => '/inside/../deep/nested')
317
+
318
+ Stud::Temporary.directory do |path|
319
+ dynamic_path = "#{path}/%{error}/%{level}/%{weird_path}/failed_syslog-%{+YYYY-MM-dd}"
320
+ expected_path = "#{path}/42/critical/deep/nested/failed_syslog-#{t.strftime("%Y-%m-%d")}"
321
+
322
+ config = { "path" => dynamic_path }
323
+
324
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
325
+ output.register
326
+ output.multi_receive([good_event])
327
+
328
+ expect(File.exist?(expected_path)).to eq(true)
329
+ output.close
330
+ end
331
+ end
332
+
333
+ it 'write the event to the generated filename with multiple deep' do
334
+ good_event = LogStash::Event.new
335
+ good_event.set('error', '/inside/errors/42.txt')
336
+
337
+ Stud::Temporary.directory do |path|
338
+ config = { "path" => "#{path}/%{error}" }
339
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
340
+ output.register
341
+ output.multi_receive([good_event])
342
+
343
+ good_file = File.join(path, good_event.get('error'))
344
+ expect(File.exist?(good_file)).to eq(true)
345
+ output.close
346
+ end
347
+ end
348
+ end
349
+ end
350
+ context "output string format" do
351
+ context "when using default configuration" do
352
+ it 'write the event as a json line' do
353
+ good_event = LogStash::Event.new
354
+ good_event.set('message', 'hello world')
355
+
356
+ Stud::Temporary.directory do |path|
357
+ config = { "path" => "#{path}/output.txt" }
358
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
359
+ output.register
360
+ output.multi_receive([good_event])
361
+ good_file = File.join(path, 'output.txt')
362
+ expect(File.exist?(good_file)).to eq(true)
363
+ output.close #teardown first to allow reading the file
364
+ File.open(good_file) {|f|
365
+ event = LogStash::Event.new(LogStash::Json.load(f.readline))
366
+ expect(event.get("message")).to eq("hello world")
367
+ }
368
+ end
369
+ end
370
+ end
371
+ context "when using line codec" do
372
+ it 'writes event using specified format' do
373
+ good_event = LogStash::Event.new
374
+ good_event.set('message', "hello world")
375
+
376
+ Stud::Temporary.directory do |path|
377
+ config = { "path" => "#{path}/output.txt" }
378
+ output = LogStash::Outputs::GoogleCloudStorage.new(config.merge("codec" => LogStash::Codecs::Line.new({ "format" => "Custom format: %{message}"})))
379
+ output.register
380
+ output.multi_receive([good_event])
381
+ good_file = File.join(path, 'output.txt')
382
+ expect(File.exist?(good_file)).to eq(true)
383
+ output.close #teardown first to allow reading the file
384
+ File.open(good_file) {|f|
385
+ line = f.readline
386
+ expect(line).to eq("Custom format: hello world\n")
387
+ }
388
+ end
389
+ end
390
+ end
391
+ context "when using file and dir modes" do
392
+ it 'dirs and files are created with correct atypical permissions' do
393
+ good_event = LogStash::Event.new
394
+ good_event.set('message', "hello world")
395
+
396
+ Stud::Temporary.directory do |path|
397
+ config = {
398
+ "path" => "#{path}/is/nested/output.txt",
399
+ "dir_mode" => 0751,
400
+ "file_mode" => 0610,
401
+ }
402
+ output = LogStash::Outputs::GoogleCloudStorage.new(config)
403
+ output.register
404
+ output.multi_receive([good_event])
405
+ good_file = File.join(path, 'is/nested/output.txt')
406
+ expect(File.exist?(good_file)).to eq(true)
407
+ expect(File.stat(good_file).mode.to_s(8)[-3..-1]).to eq('610')
408
+ first_good_dir = File.join(path, 'is')
409
+ expect(File.stat(first_good_dir).mode.to_s(8)[-3..-1]).to eq('751')
410
+ second_good_dir = File.join(path, 'is/nested')
411
+ expect(File.stat(second_good_dir).mode.to_s(8)[-3..-1]).to eq('751')
412
+ output.close #teardown first to allow reading the file
413
+ File.open(good_file) {|f|
414
+ event = LogStash::Event.new(LogStash::Json.load(f.readline))
415
+ expect(event.get("message")).to eq("hello world")
416
+ }
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ context "with non-zero flush interval" do
423
+ let(:temporary_output_file) { Stud::Temporary.pathname }
424
+
425
+ let(:event_count) { 10 }
426
+ let(:flush_interval) { 5 }
427
+
428
+ let(:events) do
429
+ event_count.times.map do |idx|
430
+ LogStash::Event.new("value" => idx)
431
+ end
432
+ end
433
+
434
+ let(:config) {
435
+ {
436
+ "path" => temporary_output_file,
437
+ "codec" => LogStash::Codecs::JSONLines.new,
438
+ "flush_interval" => flush_interval
439
+ }
440
+ }
441
+ let(:output) { LogStash::Outputs::GoogleCloudStorage.new(config) }
442
+
443
+ before(:each) { output.register }
444
+ after(:each) do
445
+ output.close
446
+ File.exist?(temporary_output_file) && File.unlink(temporary_output_file)
447
+ end
448
+
449
+ it 'eventually flushes without receiving additional events' do
450
+ output.multi_receive(events)
451
+
452
+ # events should not all be flushed just yet...
453
+ expect(File.read(temporary_output_file)).to satisfy("have less than #{event_count} lines") do |contents|
454
+ contents && contents.lines.count < event_count
455
+ end
456
+
457
+ # wait for the flusher to run...
458
+ sleep(flush_interval + 1)
459
+
460
+ # events should all be flushed
461
+ expect(File.read(temporary_output_file)).to satisfy("have exactly #{event_count} lines") do |contents|
462
+ contents && contents.lines.count == event_count
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end