logstash-output-rados 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 973fd1939d1d97304d5bc37141b68ecbd78e9e83
4
+ data.tar.gz: 8e0d1a2cdf98b8233e008d661eef46925ed8fd4f
5
+ SHA512:
6
+ metadata.gz: 499d69316c3b4547281665a0c59d1f6d96ee472dce147ce42811e8aff979f8e14893a68697dbb1afdd0e450c85cb6f65bf366028e3f9f125dc95a244227619fa
7
+ data.tar.gz: 77fb5bfab3c8cfd1feb04ecf34bd251431f9fd3060ce1f900d7a8ea21ed326f626c887aafb6e816663a84f40da2c6f8e10869b439d286519c50f578f34dbe603
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # 1.0.0
2
+ - Initial release of the Rados output plugin
data/CONTRIBUTORS ADDED
@@ -0,0 +1,11 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Balint Csergo (deathowl)
6
+ * Thanks for the developers of the S3 plugin.
7
+
8
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
9
+ Logstash, and you aren't on the list above and want to be, please let us know
10
+ and we'll make sure you're here. Contributions from folks like you are what make
11
+ open source awesome.
data/DEVELOPER.md ADDED
@@ -0,0 +1,16 @@
1
+ [Missing the other part of the readme]
2
+
3
+ ## Running the tests
4
+
5
+ ```
6
+ bundle install
7
+ bundle rspec
8
+ ```
9
+
10
+ If you want to run the integration test you have to have access to a ceph cluster, and a real bucket
11
+
12
+ ```
13
+ RADOS_LOGSTASH_TEST_POOL=mytest bundle exec rspec spec/integration/rados_spec.rb --tag integration
14
+ RADOS_LOGSTASH_TEST_POOL=mytest bundle exec rspec spec/outputs/rados_spec.rb
15
+
16
+ ```
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012–2015 Elasticsearch <http://www.elastic.co>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/NOTICE.TXT ADDED
@@ -0,0 +1,5 @@
1
+ Elasticsearch
2
+ Copyright 2012-2015 Elasticsearch
3
+
4
+ This product includes software developed by The Apache Software
5
+ Foundation (http://www.apache.org/).
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Logstash Plugin
2
+
3
+
4
+
5
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
6
+
7
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
8
+
9
+ ## Documentation
10
+
11
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
12
+
13
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
14
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
15
+
16
+ ## Need Help?
17
+
18
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
19
+
20
+ ## Developing
21
+
22
+ ### 1. Plugin Developement and Testing
23
+
24
+ #### Code
25
+ - To get started, you'll need JRuby with the Bundler gem installed.
26
+
27
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
28
+
29
+ - Install dependencies
30
+ ```sh
31
+ bundle install
32
+ ```
33
+
34
+ #### Test
35
+
36
+ - Update your dependencies
37
+
38
+ ```sh
39
+ bundle install
40
+ ```
41
+
42
+ - Run tests
43
+
44
+ ```sh
45
+ bundle exec rspec
46
+ ```
47
+
48
+ ### 2. Running your unpublished Plugin in Logstash
49
+
50
+ #### 2.1 Run in a local Logstash clone
51
+
52
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
53
+ ```ruby
54
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
55
+ ```
56
+ - Install plugin
57
+ ```sh
58
+ bin/plugin install --no-verify
59
+ ```
60
+ - Run Logstash with your plugin
61
+ ```sh
62
+ bin/logstash -e 'filter {awesome {}}'
63
+ ```
64
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
65
+
66
+ #### 2.2 Run in an installed Logstash
67
+
68
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
69
+
70
+ - Build your plugin gem
71
+ ```sh
72
+ gem build logstash-filter-awesome.gemspec
73
+ ```
74
+ - Install the plugin from the Logstash home
75
+ ```sh
76
+ bin/plugin install /your/local/plugin/logstash-filter-awesome.gem
77
+ ```
78
+ - Start Logstash and proceed to test the plugin
79
+
80
+ ## Contributing
81
+
82
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
83
+
84
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
85
+
86
+ It is more important to the community that you are able to contribute.
87
+
88
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
@@ -0,0 +1,374 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/base"
3
+ require "logstash/namespace"
4
+ require "stud/temporary"
5
+ require "stud/task"
6
+ require "socket" # for Socket.gethostname
7
+ require "thread"
8
+ require "tmpdir"
9
+ require "fileutils"
10
+
11
+
12
+ # INFORMATION:
13
+ #
14
+ # This plugin sends logstash events to Ceph.
15
+ # To use it you need to have a properly configured librados and a valid ceph cluster.
16
+ # Make sure you have permissions to write files on Ceph. Also be sure to run logstash as super user to establish a connection.
17
+ #
18
+ #
19
+ # This plugin outputs temporary files to "/opt/logstash/rados_temp/". If you want, you can change the path at the start of register method.
20
+ # These files have a special name, for example:
21
+ #
22
+ # ls.rados.ip-10-228-27-95.2013-04-18T10.00.tag_hello.part0.txt
23
+ #
24
+ # ls.rados : indicate logstash plugin rados
25
+ #
26
+ # "ip-10-228-27-95" : indicates the ip of your machine.
27
+ # "2013-04-18T10.00" : represents the time whenever you specify time_file.
28
+ # "tag_hello" : this indicates the event's tag.
29
+ # "part0" : this means if you indicate size_file then it will generate more parts if you file.size > size_file.
30
+ # When a file is full it will be pushed to the pool and will be deleted from the temporary directory.
31
+ # If a file is empty is not pushed, it is not deleted.
32
+ #
33
+ # This plugin have a system to restore the previous temporary files if something crash.
34
+ #
35
+ ##[Note] :
36
+ #
37
+ ## If you specify size_file and time_file then it will create file for each tag (if specified), when time_file or
38
+ ## their size > size_file, it will be triggered then they will be pushed on Rados pool and will delete from local disk.
39
+ ## If you don't specify size_file, but time_file then it will create only one file for each tag (if specified).
40
+ ## When time_file it will be triggered then the files will be pushed on Rados and delete from local disk.
41
+ #
42
+ ## If you don't specify time_file, but size_file then it will create files for each tag (if specified),
43
+ ## that will be triggered when their size > size_file, then they will be pushed on Rados pool and will delete from local disk.
44
+ #
45
+ ## If you don't specific size_file and time_file you have a curios mode. It will create only one file for each tag (if specified).
46
+ ## Then the file will be rest on temporary directory and don't will be pushed on pool until we will restart logstash.
47
+ #
48
+ #
49
+ # #### Usage:
50
+ # This is an example of logstash config:
51
+ # [source,ruby]
52
+ # output {
53
+ # rados{
54
+ # mypool => "mypool" (required)
55
+ # size_file => 2048 (optional)
56
+ # time_file => 5 (optional)
57
+ # }
58
+ #
59
+ class LogStash::Outputs::Rados < LogStash::Outputs::Base
60
+
61
+ TEMPFILE_EXTENSION = "txt"
62
+ RADOS_INVALID_CHARACTERS = /[\^`><]/
63
+
64
+
65
+ config_name "rados"
66
+ default :codec, 'line'
67
+
68
+ # Rados pool
69
+ config :pool, :validate => :string, :default => 'logstash'
70
+
71
+ # Set the size of file in bytes, this means that files on pool when have dimension > file_size, they are stored in two or more file.
72
+ # If you have tags then it will generate a specific size file for every tags
73
+ ##NOTE: define size of file is the better thing, because generate a local temporary file on disk and then put it in pool.
74
+ config :size_file, :validate => :number, :default => 0
75
+
76
+ # Set the time, in minutes, to close the current sub_time_section of pool.
77
+ # If you define file_size you have a number of files in consideration of the section and the current tag.
78
+ # 0 stay all time on listerner, beware if you specific 0 and size_file 0, because you will not put the file on pool,
79
+ # for now the only thing this plugin can do is to put the file when logstash restart.
80
+ config :time_file, :validate => :number, :default => 0
81
+
82
+ # Set the directory where logstash will store the tmp files before sending it to Rados
83
+ # default to the current OS temporary directory in linux /tmp/logstash
84
+ config :temporary_directory, :validate => :string, :default => File.join(Dir.tmpdir, "logstash")
85
+
86
+ # Specify a prefix to the uploaded filename, this can simulate directories on rados
87
+ config :prefix, :validate => :string, :default => ''
88
+
89
+ # Specify how many workers to use to upload the files to Rados
90
+ config :upload_workers_count, :validate => :number, :default => 1
91
+
92
+ # Define tags to be appended to the file on the Rados pool.
93
+ #
94
+ # Example:
95
+ # tags => ["elasticsearch", "logstash", "kibana"]
96
+ #
97
+ # Will generate this file:
98
+ # "ls.rados.logstash.local.2015-01-01T00.00.tag_elasticsearch.logstash.kibana.part0.txt"
99
+ #
100
+ config :tags, :validate => :array, :default => []
101
+
102
+
103
+ config :use_ssl, :validate => :boolean, :default => true
104
+
105
+ # Exposed attributes for testing purpose.
106
+ attr_accessor :tempfile
107
+ attr_reader :page_counter
108
+ # Exposed attributes for testing purpose.
109
+ attr_reader :cluster
110
+
111
+
112
+ public
113
+ def write_on_pool(file)
114
+ rados_pool = @cluster.pool(@pool)
115
+ rados_pool.open
116
+ remote_filename = "#{@prefix}#{File.basename(file)}"
117
+
118
+ @logger.debug("RADOS: ready to write file in pool", :remote_filename => remote_filename, :pool => @pool)
119
+
120
+ File.open(file, 'r') do |fileIO|
121
+ begin
122
+ # prepare for write the file
123
+ object = rados_pool.rados_object(remote_filename)
124
+ object.write(0, fileIO.read)
125
+ rescue SystemCallError => error
126
+ @logger.error("RADOS: CEPH error", :error => error)
127
+ raise LogStash::Error, "CEPH Configuration Error, #{error}"
128
+ ensure
129
+ rados_pool.close
130
+ end
131
+ end
132
+
133
+ @logger.debug("RADOS: has written remote file in pool", :remote_filename => remote_filename, :pool => @pool)
134
+ end
135
+
136
+ # This method is used for create new empty temporary files for use. Flag is needed for indicate new subsection time_file.
137
+ public
138
+ def create_temporary_file
139
+ filename = File.join(@temporary_directory, get_temporary_filename(@page_counter))
140
+
141
+ @logger.debug("RADOS: Creating a new temporary file", :filename => filename)
142
+
143
+ @file_rotation_lock.synchronize do
144
+ unless @tempfile.nil?
145
+ @tempfile.close
146
+ end
147
+
148
+ @tempfile = File.open(filename, "a")
149
+ end
150
+ end
151
+
152
+ public
153
+ def register
154
+ require "ceph-ruby"
155
+ # required if using ruby version < 2.0
156
+ # http://ruby.awsblog.com/post/Tx16QY1CI5GVBFT/Threading-with-the-AWS-SDK-for-Ruby
157
+ workers_not_supported
158
+
159
+ @cluster = CephRuby::Cluster.new
160
+ @upload_queue = Queue.new
161
+ @file_rotation_lock = Mutex.new
162
+
163
+ if @prefix && @prefix =~ RADOS_INVALID_CHARACTERS
164
+ @logger.error("RADOS: prefix contains invalid characters", :prefix => @prefix, :contains => RADOS_INVALID_CHARACTERS)
165
+ raise LogStash::ConfigurationError, "RADOS: prefix contains invalid characters"
166
+ end
167
+
168
+ if !Dir.exist?(@temporary_directory)
169
+ FileUtils.mkdir_p(@temporary_directory)
170
+ end
171
+ restore_from_crashes if @restore == true
172
+ reset_page_counter
173
+ create_temporary_file
174
+ configure_periodic_rotation if time_file != 0
175
+ configure_upload_workers
176
+
177
+ @codec.on_event do |event, encoded_event|
178
+ handle_event(encoded_event)
179
+ end
180
+ end
181
+
182
+ public
183
+ def restore_from_crashes
184
+ @logger.debug("RADOS: is attempting to verify previous crashes...")
185
+
186
+ Dir[File.join(@temporary_directory, "*.#{TEMPFILE_EXTENSION}")].each do |file|
187
+ name_file = File.basename(file)
188
+ @logger.warn("RADOS: have found temporary file the upload process crashed, uploading file to Rados.", :filename => name_file)
189
+ move_file_to_pool_async(file)
190
+ end
191
+ end
192
+
193
+ public
194
+ def move_file_to_pool(file)
195
+ if !File.zero?(file)
196
+ write_on_pool(file)
197
+ @logger.debug("RADOS: file was put on the upload thread", :filename => File.basename(file), :pool => @pool)
198
+ end
199
+
200
+ begin
201
+ File.delete(file)
202
+ rescue Errno::ENOENT
203
+ # Something else deleted the file, logging but not raising the issue
204
+ @logger.warn("RADOS: Cannot delete the temporary file since it doesn't exist on disk", :filename => File.basename(file))
205
+ rescue Errno::EACCES
206
+ @logger.error("RADOS: Logstash doesnt have the permission to delete the file in the temporary directory.", :filename => File.basename(file), :temporary_directory => @temporary_directory)
207
+ end
208
+ end
209
+
210
+ public
211
+ def periodic_interval
212
+ @time_file * 60
213
+ end
214
+
215
+ public
216
+ def get_temporary_filename(page_counter = 0)
217
+ current_time = Time.now
218
+ filename = "ls.rados.#{Socket.gethostname}.#{current_time.strftime("%Y-%m-%dT%H.%M")}"
219
+
220
+ if @tags.size > 0
221
+ return "#{filename}.tag_#{@tags.join('.')}.part#{page_counter}.#{TEMPFILE_EXTENSION}"
222
+ else
223
+ return "#{filename}.part#{page_counter}.#{TEMPFILE_EXTENSION}"
224
+ end
225
+ end
226
+
227
+ public
228
+ def receive(event)
229
+
230
+ @codec.encode(event)
231
+ end
232
+
233
+ public
234
+ def rotate_events_log?
235
+ @file_rotation_lock.synchronize do
236
+ @tempfile.size > @size_file
237
+ end
238
+ end
239
+
240
+ public
241
+ def write_events_to_multiple_files?
242
+ @size_file > 0
243
+ end
244
+
245
+ public
246
+ def write_to_tempfile(event)
247
+ begin
248
+ @logger.debug("RADOS: put event into tempfile ", :tempfile => File.basename(@tempfile))
249
+
250
+ @file_rotation_lock.synchronize do
251
+ @tempfile.syswrite(event)
252
+ end
253
+ rescue Errno::ENOSPC
254
+ @logger.error("RADOS: No space left in temporary directory", :temporary_directory => @temporary_directory)
255
+ close
256
+ end
257
+ end
258
+
259
+ public # Specify how many workers to use to upload the files to Rados
260
+ config :upload_workers_count, :validate => :number, :default => 1
261
+ def close
262
+ shutdown_upload_workers
263
+ @periodic_rotation_thread.stop! if @periodic_rotation_thread
264
+
265
+ @file_rotation_lock.synchronize do
266
+ @tempfile.close unless @tempfile.nil? && @tempfile.closed?
267
+ end
268
+ @cluster.shutdown
269
+ end
270
+
271
+ private
272
+ def shutdown_upload_workers
273
+ @logger.debug("RADOS: Gracefully shutdown the upload workers")
274
+ @upload_queue << LogStash::ShutdownEvent
275
+ end
276
+
277
+ private
278
+ def handle_event(encoded_event)
279
+ if write_events_to_multiple_files?
280
+ if rotate_events_log?
281
+ @logger.debug("RADOS: tempfile is too large, let's upload it and create new file", :tempfile => File.basename(@tempfile))
282
+
283
+ move_file_to_pool_async(@tempfile.path)
284
+ next_page
285
+ create_temporary_file
286
+ else
287
+ @logger.debug("RADOS: tempfile file size report.", :tempfile_size => @tempfile.size, :size_file => @size_file)
288
+ end
289
+ end
290
+
291
+ write_to_tempfile(encoded_event)
292
+ end
293
+
294
+ private
295
+ def configure_periodic_rotation
296
+ @periodic_rotation_thread = Stud::Task.new do
297
+ LogStash::Util::set_thread_name("<RADOS periodic uploader")
298
+
299
+ Stud.interval(periodic_interval, :sleep_then_run => true) do
300
+ @logger.debug("RADOS: time_file triggered, uploading the file", :filename => @tempfile.path)
301
+
302
+ move_file_to_pool_async(@tempfile.path)
303
+ next_page
304
+ create_temporary_file
305
+ end
306
+ end
307
+ end
308
+
309
+ private
310
+ def configure_upload_workers
311
+ @logger.debug("RADOS: Configure upload workers")
312
+
313
+ @upload_workers = @upload_workers_count.times.map do |worker_id|
314
+ Stud::Task.new do
315
+ LogStash::Util::set_thread_name("<RADOS upload worker #{worker_id}")
316
+
317
+ while true do
318
+ @logger.debug("RADOS: upload worker is waiting for a new file to upload.", :worker_id => worker_id)
319
+
320
+ upload_worker
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ private
327
+ def upload_worker
328
+ file = @upload_queue.deq
329
+
330
+ case file
331
+ when LogStash::ShutdownEvent
332
+ @logger.debug("RADOS: upload worker is shutting down gracefuly")
333
+ @upload_queue.enq(LogStash::ShutdownEvent)
334
+ else
335
+ @logger.debug("RADOS: upload worker is uploading a new file", :filename => File.basename(file))
336
+ move_file_to_pool(file)
337
+ end
338
+ end
339
+
340
+ private
341
+ def next_page
342
+ @page_counter += 1
343
+ end
344
+
345
+ private
346
+ def reset_page_counter
347
+ @page_counter = 0
348
+ end
349
+
350
+ private
351
+ def delete_on_pool(filename)
352
+ rados_pool = @cluster.pool(@pool)
353
+ rados_pool.open
354
+ remote_filename = "#{@prefix}#{File.basename(filename)}"
355
+
356
+ @logger.debug("RADOS: delete file from pool", :remote_filename => remote_filename, :pool => @pool)
357
+
358
+ begin
359
+ object = rados_pool.rados_object(remote_filename)
360
+ object.destroy
361
+ rescue SystemCallError => error
362
+ @logger.error("RADOS: CEPH error", :error => error)
363
+ raise LogStash::Error, "CEPH Configuration Error, #{error}"
364
+ ensure
365
+ rados_pool.close
366
+ end
367
+ end
368
+
369
+ private
370
+ def move_file_to_pool_async(file)
371
+ @logger.debug("RADOS: Sending the file to the upload queue.", :filename => File.basename(file))
372
+ @upload_queue.enq(file)
373
+ end
374
+ end
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-rados'
4
+ s.version = '1.0.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "This plugin was created for store the logstash's events into Ceph's Rados"
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 = ["Deathowl"]
9
+ s.email = 'bagoly@endticket.com'
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/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
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.beta2", "< 3.0.0"
24
+ s.add_runtime_dependency 'stud', '~> 0.0.22'
25
+ s.add_runtime_dependency 'ceph-ruby', '~> 1.1'
26
+ s.add_development_dependency 'logstash-devutils'
27
+ s.add_development_dependency 'logstash-input-generator'
28
+ s.add_development_dependency 'logstash-codec-line'
29
+ end
@@ -0,0 +1,87 @@
1
+ require "logstash/devutils/rspec/spec_helper"
2
+ require "logstash/outputs/rados"
3
+ require 'socket'
4
+ require "fileutils"
5
+ require "stud/temporary"
6
+ require_relative "../supports/helpers"
7
+
8
+ describe LogStash::Outputs::Rados, :integration => true, :rados => true do
9
+ before do
10
+ Thread.abort_on_exception = true
11
+ end
12
+
13
+ let!(:minimal_settings) { { "pool" => ENV['RADOS_LOGSTASH_TEST_POOL'],
14
+ "temporary_directory" => Stud::Temporary.pathname('temporary_directory') }}
15
+
16
+ let!(:rados_object) do
17
+ radosoutput = LogStash::Outputs::Rados.new(minimal_settings)
18
+ radosoutput.register
19
+ radosoutput.cluster
20
+ end
21
+
22
+ describe "#register" do
23
+ it "write a file on the pool to check permissions" do
24
+ rados = LogStash::Outputs::Rados.new(minimal_settings)
25
+ expect(rados.register).not_to raise_error
26
+ end
27
+ end
28
+
29
+ describe "#write_on_pool" do
30
+ after(:each) do
31
+ File.unlink(fake_data.path)
32
+ end
33
+
34
+ let!(:fake_data) { Stud::Temporary.file }
35
+
36
+ it "should prefix the file on the pool if a prefix is specified" do
37
+ prefix = "my-prefix"
38
+
39
+ config = minimal_settings.merge({
40
+ "prefix" => prefix,
41
+ })
42
+
43
+ rados = LogStash::Outputs::Rados.new(config)
44
+ rados.register
45
+ rados.write_on_pool(fake_data)
46
+
47
+ expect(key_exists_on_pool?("#{prefix}#{File.basename(fake_data.path)}")).to eq(true)
48
+ end
49
+
50
+ it 'should use the same local filename if no prefix is specified' do
51
+ rados = LogStash::Outputs::Rados.new(minimal_settings)
52
+ rados.register
53
+ rados.write_on_pool(fake_data)
54
+
55
+ expect(key_exists_on_pool?(File.basename(fake_data.path))).to eq(true)
56
+ end
57
+ end
58
+
59
+ describe "#move_file_to_pool" do
60
+ let!(:rados) { LogStash::Outputs::Rados.new(minimal_settings) }
61
+
62
+ before do
63
+ rados.register
64
+ end
65
+
66
+ it "should upload the file if the size > 0" do
67
+ tmp = Stud::Temporary.file
68
+ allow(File).to receive(:zero?).and_return(false)
69
+ rados.move_file_to_pool(tmp)
70
+ expect(key_exists_on_pool?(File.basename(tmp.path))).to eq(true)
71
+ end
72
+ end
73
+
74
+ describe "#restore_from_crashes" do
75
+ it "read the temp directory and upload the matching file to rados" do
76
+ Stud::Temporary.pathname do |temp_path|
77
+ tempfile = File.open(File.join(temp_path, 'A'), 'w+') { |f| f.write('test')}
78
+
79
+ rados = LogStash::Outputs::Rados.new(minimal_settings.merge({ "temporary_directory" => temp_path }))
80
+ rados.restore_from_crashes
81
+
82
+ expect(File.exist?(tempfile.path)).to eq(false)
83
+ expect(key_exists_on_pool?(File.basename(tempfile.path))).to eq(true)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,279 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/rados"
4
+ require "logstash/codecs/line"
5
+ require "logstash/pipeline"
6
+ require "fileutils"
7
+ require_relative "../supports/helpers"
8
+
9
+ describe LogStash::Outputs::Rados do
10
+ before do
11
+ # We stub all the calls from Rad, for more information see:
12
+ # http://ruby.awsblog.com/post/Tx2SU6TYJWQQLC3/Stubbing-AWS-Responses
13
+ Thread.abort_on_exception = true
14
+ end
15
+
16
+ let(:minimal_settings) { {"pool" => ENV['RADOS_LOGSTASH_TEST_POOL'] } }
17
+
18
+ describe "#register" do
19
+ it "should create the tmp directory if it doesn't exist" do
20
+ temporary_directory = Stud::Temporary.pathname("temporary_directory")
21
+
22
+ config = {
23
+ "pool" => "logstash",
24
+ "size_file" => 10,
25
+ "temporary_directory" => temporary_directory
26
+ }
27
+
28
+ rados = LogStash::Outputs::Rados.new(config)
29
+ rados.register
30
+
31
+ expect(Dir.exist?(temporary_directory)).to eq(true)
32
+ rados.close
33
+ FileUtils.rm_r(temporary_directory)
34
+ end
35
+
36
+ it "should raise a ConfigurationError if the prefix contains one or more '\^`><' characters" do
37
+ config = {
38
+ "prefix" => "`no\><^"
39
+ }
40
+
41
+ rados = LogStash::Outputs::Rados.new(config)
42
+
43
+ expect {
44
+ rados.register
45
+ }.to raise_error(LogStash::ConfigurationError)
46
+ end
47
+ end
48
+
49
+ describe "#generate_temporary_filename" do
50
+ before do
51
+ allow(Socket).to receive(:gethostname) { "logstash.local" }
52
+ end
53
+
54
+ it "should add tags to the filename if present" do
55
+ config = minimal_settings.merge({ "tags" => ["elasticsearch", "logstash", "kibana"], "temporary_directory" => "/tmp/logstash"})
56
+ rados = LogStash::Outputs::Rados.new(config)
57
+ expect(rados.get_temporary_filename).to match(/^ls\.rados\.logstash\.local\.\d{4}-\d{2}\-\d{2}T\d{2}\.\d{2}\.tag_#{config["tags"].join("\.")}\.part0\.txt\Z/)
58
+ end
59
+
60
+ it "should not add the tags to the filename" do
61
+ config = minimal_settings.merge({ "tags" => [], "temporary_directory" => "/tmp/logstash" })
62
+ rados = LogStash::Outputs::Rados.new(config)
63
+ expect(rados.get_temporary_filename(3)).to match(/^ls\.rados\.logstash\.local\.\d{4}-\d{2}\-\d{2}T\d{2}\.\d{2}\.part3\.txt\Z/)
64
+ end
65
+
66
+ it "normalized the temp directory to include the trailing slash if missing" do
67
+ rados = LogStash::Outputs::Rados.new(minimal_settings.merge({ "temporary_directory" => "/tmp/logstash" }))
68
+ expect(rados.get_temporary_filename).to match(/^ls\.rados\.logstash\.local\.\d{4}-\d{2}\-\d{2}T\d{2}\.\d{2}\.part0\.txt\Z/)
69
+ end
70
+ end
71
+
72
+
73
+ describe "#write_events_to_multiple_files?" do
74
+ it 'returns true if the size_file is != 0 ' do
75
+ rados = LogStash::Outputs::Rados.new(minimal_settings.merge({ "size_file" => 200 }))
76
+ expect(rados.write_events_to_multiple_files?).to eq(true)
77
+ end
78
+
79
+ it 'returns false if size_file is zero or not set' do
80
+ rados = LogStash::Outputs::Rados.new(minimal_settings)
81
+ expect(rados.write_events_to_multiple_files?).to eq(false)
82
+ end
83
+ end
84
+
85
+ describe "#write_to_tempfile" do
86
+ it "should append the event to a file" do
87
+ Stud::Temporary.file("logstash", "a+") do |tmp|
88
+ rados = LogStash::Outputs::Rados.new(minimal_settings)
89
+ rados.register
90
+ rados.tempfile = tmp
91
+ rados.write_to_tempfile("test-write")
92
+ tmp.rewind
93
+ expect(tmp.read).to eq("test-write")
94
+ end
95
+ end
96
+ end
97
+
98
+ describe "#rotate_events_log" do
99
+
100
+ context "having a single worker" do
101
+ let(:rados) { LogStash::Outputs::Rados.new(minimal_settings.merge({ "size_file" => 1024 })) }
102
+
103
+ before(:each) do
104
+ rados.register
105
+ end
106
+
107
+ it "returns true if the tempfile is over the file_size limit" do
108
+ Stud::Temporary.file do |tmp|
109
+ allow(tmp).to receive(:size) { 2024001 }
110
+
111
+ rados.tempfile = tmp
112
+ expect(rados.rotate_events_log?).to be(true)
113
+ end
114
+ end
115
+
116
+ it "returns false if the tempfile is under the file_size limit" do
117
+ Stud::Temporary.file do |tmp|
118
+ allow(tmp).to receive(:size) { 100 }
119
+
120
+ rados.tempfile = tmp
121
+ expect(rados.rotate_events_log?).to eq(false)
122
+ end
123
+ end
124
+ end
125
+
126
+ context "having periodic rotations" do
127
+ let(:rados) { LogStash::Outputs::Rados.new(minimal_settings.merge({ "size_file" => 1024, "time_file" => 6e-10 })) }
128
+ let(:tmp) { Tempfile.new('rados_rotation_temp_file') }
129
+
130
+ before(:each) do
131
+ rados.tempfile = tmp
132
+ rados.register
133
+ end
134
+
135
+ after(:each) do
136
+ rados.close
137
+ tmp.close
138
+ tmp.unlink
139
+ end
140
+
141
+ it "raises no error when periodic rotation happen" do
142
+ 1000.times do
143
+ expect { rados.rotate_events_log? }.not_to raise_error
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ describe "#move_file_to_pool" do
150
+ subject { LogStash::Outputs::Rados.new(minimal_settings) }
151
+
152
+ it "should always delete the source file" do
153
+ tmp = Stud::Temporary.file
154
+
155
+ allow(File).to receive(:zero?).and_return(true)
156
+ expect(File).to receive(:delete).with(tmp)
157
+
158
+ subject.move_file_to_pool(tmp)
159
+ end
160
+
161
+ it 'should not upload the file if the size of the file is zero' do
162
+ temp_file = Stud::Temporary.file
163
+ allow(temp_file).to receive(:zero?).and_return(true)
164
+
165
+ expect(subject).not_to receive(:write_on_pool)
166
+ subject.move_file_to_pool(temp_file)
167
+ end
168
+
169
+ it "should upload the file if the size > 0" do
170
+ tmp = Stud::Temporary.file
171
+
172
+ allow(File).to receive(:zero?).and_return(false)
173
+ expect(subject).to receive(:write_on_pool)
174
+
175
+ subject.move_file_to_pool(tmp)
176
+ end
177
+ end
178
+
179
+ describe "#restore_from_crashes" do
180
+ it "read the temp directory and upload the matching file to rados" do
181
+ subject = LogStash::Outputs::Rados.new(minimal_settings.merge({ "temporary_directory" => "/tmp/logstash/" }))
182
+
183
+ expect(Dir).to receive(:[]).with("/tmp/logstash/*.txt").and_return(["/tmp/logstash/01.txt"])
184
+ expect(subject).to receive(:move_file_to_pool_async).with("/tmp/logstash/01.txt")
185
+
186
+
187
+ subject.restore_from_crashes
188
+ end
189
+ end
190
+
191
+ describe "#receive" do
192
+ it "should send the event through the codecs" do
193
+ data = {"foo" => "bar", "baz" => {"bah" => ["a","b","c"]}, "@timestamp" => "2014-05-30T02:52:17.929Z"}
194
+ event = LogStash::Event.new(data)
195
+
196
+ expect_any_instance_of(LogStash::Codecs::Line).to receive(:encode).with(event)
197
+
198
+ subject = LogStash::Outputs::Rados.new(minimal_settings)
199
+ subject.register
200
+
201
+ subject.receive(event)
202
+ end
203
+ end
204
+
205
+ describe "when rotating the temporary file" do
206
+ before { allow(File).to receive(:delete) }
207
+
208
+ it "doesn't skip events if using the size_file option" do
209
+ Stud::Temporary.directory do |temporary_directory|
210
+ size_file = rand(200..20000)
211
+ event_count = rand(300..15000)
212
+
213
+ config = %Q[
214
+ input {
215
+ generator {
216
+ count => #{event_count}
217
+ }
218
+ }
219
+ output {
220
+ rados {
221
+ size_file => #{size_file}
222
+ codec => line
223
+ temporary_directory => '#{temporary_directory}'
224
+ pool => '#{ENV['RADOS_LOGSTASH_TEST_POOL']}'
225
+ }
226
+ }
227
+ ]
228
+
229
+ pipeline = LogStash::Pipeline.new(config)
230
+
231
+ pipeline_thread = Thread.new { pipeline.run }
232
+ sleep 0.1 while !pipeline.ready?
233
+ pipeline_thread.join
234
+
235
+ events_written_count = events_in_files(Dir[File.join(temporary_directory, 'ls.*.txt')])
236
+ expect(events_written_count).to eq(event_count)
237
+ end
238
+ end
239
+
240
+ it "doesn't skip events if using the time_file option", :tag => :slow do
241
+ Stud::Temporary.directory do |temporary_directory|
242
+ time_file = rand(1..2)
243
+ number_of_rotation = rand(2..5)
244
+
245
+ config = {
246
+ "time_file" => time_file,
247
+ "codec" => "line",
248
+ "temporary_directory" => temporary_directory,
249
+ "pool" => "#{ENV['RADOS_LOGSTASH_TEST_POOL']}"
250
+ }
251
+
252
+ rados = LogStash::Outputs::Rados.new(minimal_settings.merge(config))
253
+ # Make the test run in seconds intead of minutes..
254
+ expect(rados).to receive(:periodic_interval).and_return(time_file)
255
+ rados.register
256
+
257
+ # Force to have a few files rotation
258
+ stop_time = Time.now + (number_of_rotation * time_file)
259
+ event_count = 0
260
+
261
+ event = LogStash::Event.new("message" => "Hello World")
262
+
263
+ until Time.now > stop_time do
264
+ rados.receive(event)
265
+ event_count += 1
266
+ end
267
+ rados.close
268
+
269
+ generated_files = Dir[File.join(temporary_directory, 'ls.*.txt')]
270
+
271
+ events_written_count = events_in_files(generated_files)
272
+
273
+ # Skew times can affect the number of rotation..
274
+ expect(generated_files.count).to be_within(number_of_rotation).of(number_of_rotation + 1)
275
+ expect(events_written_count).to eq(event_count)
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,11 @@
1
+ def key_exists_on_pool?(key)
2
+ pool = rados_object.pool(minimal_settings["pool"])
3
+ pool.open
4
+ obj = pool.rados_object(key)
5
+ obj.exists?
6
+ end
7
+
8
+ def events_in_files(files)
9
+ files.collect { |file| File.foreach(file).count }.inject(&:+)
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-output-rados
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Deathowl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '>='
17
+ - !ruby/object:Gem::Version
18
+ version: 2.0.0.beta2
19
+ - - <
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ name: logstash-core
23
+ prerelease: false
24
+ type: :runtime
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0.beta2
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 0.0.22
39
+ name: stud
40
+ prerelease: false
41
+ type: :runtime
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.22
47
+ - !ruby/object:Gem::Dependency
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ version: '1.1'
53
+ name: ceph-ruby
54
+ prerelease: false
55
+ type: :runtime
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '1.1'
61
+ - !ruby/object:Gem::Dependency
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ name: logstash-devutils
68
+ prerelease: false
69
+ type: :development
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ name: logstash-input-generator
82
+ prerelease: false
83
+ type: :development
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ name: logstash-codec-line
96
+ prerelease: false
97
+ type: :development
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ 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
104
+ email: bagoly@endticket.com
105
+ executables: []
106
+ extensions: []
107
+ extra_rdoc_files: []
108
+ files:
109
+ - CHANGELOG.md
110
+ - CONTRIBUTORS
111
+ - DEVELOPER.md
112
+ - Gemfile
113
+ - LICENSE
114
+ - NOTICE.TXT
115
+ - README.md
116
+ - lib/logstash/outputs/rados.rb
117
+ - logstash-output-rados.gemspec
118
+ - spec/integration/rados_spec.rb
119
+ - spec/outputs/rados_spec.rb
120
+ - spec/supports/helpers.rb
121
+ homepage: http://www.elastic.co/guide/en/logstash/current/index.html
122
+ licenses:
123
+ - Apache License (2.0)
124
+ metadata:
125
+ logstash_plugin: 'true'
126
+ logstash_group: output
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.4.5
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: This plugin was created for store the logstash's events into Ceph's Rados
147
+ test_files:
148
+ - spec/integration/rados_spec.rb
149
+ - spec/outputs/rados_spec.rb
150
+ - spec/supports/helpers.rb