logstash-output-s3 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README +15 -0
- data/lib/logstash/outputs/s3.rb +302 -240
- data/logstash-output-s3.gemspec +5 -1
- data/spec/integration/s3_spec.rb +96 -0
- data/spec/outputs/s3_spec.rb +319 -1
- data/spec/supports/helpers.rb +14 -0
- metadata +72 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7a88994c7ff548d4ebacbaf908bc786cccfb129
|
4
|
+
data.tar.gz: b87149529c6a84c903c5f87bb831d1b3db6623f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a99e7218cd168e2030fc6d9fe6a6c7920ef9c76e5057f333c6f6992e39306b441f230498d02311802a4dc8978012a24d0ee065541d26e63b52617a620d6e757
|
7
|
+
data.tar.gz: 1e9b592b3203375975a97e3c76a2f2bf4f9804529ce87c2d1ec2663e67028f988702f0404738c681afceac9f90ca91158b1bf5a4e276b5f1baeb15d81ffddd73
|
data/README
ADDED
@@ -0,0 +1,15 @@
|
|
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 against a real bucket you need to pass
|
11
|
+
your aws credentials to the test runner or declare it in your environment.
|
12
|
+
|
13
|
+
```
|
14
|
+
AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=123 AWS_SECRET_ACCESS_KEY=secret AWS_LOGSTASH_TEST_BUCKET=mytest bundle exec rspec spec/integration/s3_spec.rb --tag integration
|
15
|
+
```
|
data/lib/logstash/outputs/s3.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "logstash/outputs/base"
|
3
3
|
require "logstash/namespace"
|
4
|
+
require "logstash/plugin_mixins/aws_config"
|
5
|
+
require "stud/temporary"
|
4
6
|
require "socket" # for Socket.gethostname
|
7
|
+
require "thread"
|
8
|
+
require "tmpdir"
|
9
|
+
require "fileutils"
|
10
|
+
|
5
11
|
|
6
|
-
# TODO integrate aws_config in the future
|
7
|
-
#require "logstash/plugin_mixins/aws_config"
|
8
|
-
#
|
9
12
|
# INFORMATION:
|
10
13
|
#
|
11
14
|
# This plugin was created for store the logstash's events into Amazon Simple Storage Service (Amazon S3).
|
@@ -34,7 +37,6 @@ require "socket" # for Socket.gethostname
|
|
34
37
|
#
|
35
38
|
## If you specify size_file and time_file then it will create file for each tag (if specified), when time_file or
|
36
39
|
## their size > size_file, it will be triggered then they will be pushed on s3's bucket and will delete from local disk.
|
37
|
-
#
|
38
40
|
## If you don't specify size_file, but time_file then it will create only one file for each tag (if specified).
|
39
41
|
## When time_file it will be triggered then the files will be pushed on s3's bucket and delete from local disk.
|
40
42
|
#
|
@@ -44,22 +46,8 @@ require "socket" # for Socket.gethostname
|
|
44
46
|
## 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).
|
45
47
|
## Then the file will be rest on temporary directory and don't will be pushed on bucket until we will restart logstash.
|
46
48
|
#
|
47
|
-
# INFORMATION ABOUT CLASS:
|
48
|
-
#
|
49
|
-
# I tried to comment the class at best i could do.
|
50
|
-
# I think there are much thing to improve, but if you want some points to develop here a list:
|
51
|
-
#
|
52
|
-
# TODO Integrate aws_config in the future
|
53
|
-
# TODO Find a method to push them all files when logtstash close the session.
|
54
|
-
# TODO Integrate @field on the path file
|
55
|
-
# TODO Permanent connection or on demand? For now on demand, but isn't a good implementation.
|
56
|
-
# Use a while or a thread to try the connection before break a time_out and signal an error.
|
57
|
-
# TODO If you have bugs report or helpful advice contact me, but remember that this code is much mine as much as yours,
|
58
|
-
# try to work on it if you want :)
|
59
|
-
#
|
60
|
-
#
|
61
|
-
# USAGE:
|
62
49
|
#
|
50
|
+
# #### Usage:
|
63
51
|
# This is an example of logstash config:
|
64
52
|
# [source,ruby]
|
65
53
|
# output {
|
@@ -73,285 +61,359 @@ require "socket" # for Socket.gethostname
|
|
73
61
|
# format => "plain" (optional)
|
74
62
|
# canned_acl => "private" (optional. Options are "private", "public_read", "public_read_write", "authenticated_read". Defaults to "private" )
|
75
63
|
# }
|
76
|
-
# }
|
77
|
-
#
|
78
|
-
# We analize this:
|
79
|
-
#
|
80
|
-
# access_key_id => "crazy_key"
|
81
|
-
# Amazon will give you the key for use their service if you buy it or try it. (not very much open source anyway)
|
82
|
-
#
|
83
|
-
# secret_access_key => "monkey_access_key"
|
84
|
-
# Amazon will give you the secret_access_key for use their service if you buy it or try it . (not very much open source anyway).
|
85
|
-
#
|
86
|
-
# endpoint_region => "eu-west-1"
|
87
|
-
# When you make a contract with Amazon, you should know where the services you use.
|
88
|
-
#
|
89
|
-
# bucket => "boss_please_open_your_bucket"
|
90
|
-
# Be careful you have the permission to write on bucket and know the name.
|
91
|
-
#
|
92
|
-
# size_file => 2048
|
93
|
-
# Means the size, in KB, of files who can store on temporary directory before you will be pushed on bucket.
|
94
|
-
# Is useful if you have a little server with poor space on disk and you don't want blow up the server with unnecessary temporary log files.
|
95
|
-
#
|
96
|
-
# time_file => 5
|
97
|
-
# Means, in minutes, the time before the files will be pushed on bucket. Is useful if you want to push the files every specific time.
|
98
|
-
#
|
99
|
-
# format => "plain"
|
100
|
-
# Means the format of events you want to store in the files
|
101
|
-
#
|
102
|
-
# canned_acl => "private"
|
103
|
-
# The S3 canned ACL to use when putting the file. Defaults to "private".
|
104
|
-
#
|
105
|
-
# LET'S ROCK AND ROLL ON THE CODE!
|
106
64
|
#
|
107
65
|
class LogStash::Outputs::S3 < LogStash::Outputs::Base
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
66
|
+
include LogStash::PluginMixins::AwsConfig
|
67
|
+
|
68
|
+
TEMPFILE_EXTENSION = "txt"
|
69
|
+
S3_INVALID_CHARACTERS = /[\^`><]/
|
70
|
+
|
71
|
+
config_name "s3"
|
72
|
+
milestone 1
|
73
|
+
default :codec, 'line'
|
74
|
+
|
75
|
+
# S3 bucket
|
76
|
+
config :bucket, :validate => :string
|
77
|
+
|
78
|
+
# AWS endpoint_region
|
79
|
+
config :endpoint_region, :validate => ["us-east-1", "us-west-1", "us-west-2",
|
80
|
+
"eu-west-1", "ap-southeast-1", "ap-southeast-2",
|
81
|
+
"ap-northeast-1", "sa-east-1", "us-gov-west-1"], :deprecated => 'Deprecated, use region instead.'
|
82
|
+
|
83
|
+
# Set the size of file in bytes, this means that files on bucket when have dimension > file_size, they are stored in two or more file.
|
84
|
+
# If you have tags then it will generate a specific size file for every tags
|
85
|
+
##NOTE: define size of file is the better thing, because generate a local temporary file on disk and then put it in bucket.
|
86
|
+
config :size_file, :validate => :number, :default => 0
|
87
|
+
|
88
|
+
# Set the time, in minutes, to close the current sub_time_section of bucket.
|
89
|
+
# If you define file_size you have a number of files in consideration of the section and the current tag.
|
90
|
+
# 0 stay all time on listerner, beware if you specific 0 and size_file 0, because you will not put the file on bucket,
|
91
|
+
# for now the only thing this plugin can do is to put the file when logstash restart.
|
92
|
+
config :time_file, :validate => :number, :default => 0
|
93
|
+
|
94
|
+
## IMPORTANT: if you use multiple instance of s3, you should specify on one of them the "restore=> true" and on the others "restore => false".
|
95
|
+
## This is hack for not destroy the new files after restoring the initial files.
|
96
|
+
## If you do not specify "restore => true" when logstash crashes or is restarted, the files are not sent into the bucket,
|
97
|
+
## for example if you have single Instance.
|
98
|
+
config :restore, :validate => :boolean, :default => false
|
99
|
+
|
100
|
+
# The S3 canned ACL to use when putting the file. Defaults to "private".
|
101
|
+
config :canned_acl, :validate => ["private", "public_read", "public_read_write", "authenticated_read"],
|
102
|
+
:default => "private"
|
103
|
+
|
104
|
+
# Set the directory where logstash will store the tmp files before sending it to S3
|
105
|
+
# default to the current OS temporary directory in linux /tmp/logstash
|
106
|
+
config :temporary_directory, :validate => :string, :default => File.join(Dir.tmpdir, "logstash")
|
107
|
+
|
108
|
+
# Specify a prefix to the uploaded filename, this can simulate directories on S3
|
109
|
+
config :prefix, :validate => :string, :default => ''
|
110
|
+
|
111
|
+
# Specify how many workers to use to upload the files to S3
|
112
|
+
config :upload_workers_count, :validate => :number, :default => 1
|
113
|
+
|
114
|
+
# Exposed attributes for testing purpose.
|
115
|
+
attr_accessor :tempfile
|
116
|
+
attr_reader :page_counter
|
117
|
+
attr_reader :s3
|
118
|
+
|
119
|
+
def aws_s3_config
|
120
|
+
@logger.info("Registering s3 output", :bucket => @bucket, :endpoint_region => @region)
|
121
|
+
@s3 = AWS::S3.new(aws_options_hash)
|
122
|
+
end
|
113
123
|
|
114
|
-
|
115
|
-
|
124
|
+
def aws_service_endpoint(region)
|
125
|
+
# Make the deprecated endpoint_region work
|
126
|
+
# TODO: (ph) Remove this after deprecation.
|
127
|
+
|
128
|
+
if @endpoint_region
|
129
|
+
region_to_use = @endpoint_region
|
130
|
+
else
|
131
|
+
region_to_use = @region
|
132
|
+
end
|
116
133
|
|
117
|
-
|
118
|
-
|
134
|
+
return {
|
135
|
+
:s3_endpoint => region_to_use == 'us-east-1' ? 's3.amazonaws.com' : "s3-#{region_to_use}.amazonaws.com"
|
136
|
+
}
|
137
|
+
end
|
119
138
|
|
120
|
-
|
121
|
-
|
139
|
+
public
|
140
|
+
def write_on_bucket(file)
|
141
|
+
# find and use the bucket
|
142
|
+
bucket = @s3.buckets[@bucket]
|
122
143
|
|
123
|
-
#
|
124
|
-
config :endpoint_region, :validate => ["us-east-1", "us-west-1", "us-west-2",
|
125
|
-
"eu-west-1", "ap-southeast-1", "ap-southeast-2",
|
126
|
-
"ap-northeast-1", "sa-east-1", "us-gov-west-1"], :default => "us-east-1"
|
144
|
+
remote_filename = "#{@prefix}#{File.basename(file)}"
|
127
145
|
|
128
|
-
|
129
|
-
# If you have tags then it will generate a specific size file for every tags
|
130
|
-
##NOTE: define size of file is the better thing, because generate a local temporary file on disk and then put it in bucket.
|
131
|
-
config :size_file, :validate => :number, :default => 0
|
146
|
+
@logger.debug("S3: ready to write file in bucket", :remote_filename => remote_filename, :bucket => @bucket)
|
132
147
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
148
|
+
begin
|
149
|
+
# prepare for write the file
|
150
|
+
object = bucket.objects[remote_filename]
|
151
|
+
object.write(:file => file, :acl => @canned_acl)
|
152
|
+
rescue AWS::Errors::Base => error
|
153
|
+
@logger.error("S3: AWS error", :error => error)
|
154
|
+
raise LogStash::Error, "AWS Configuration Error, #{error}"
|
155
|
+
end
|
138
156
|
|
139
|
-
|
140
|
-
|
157
|
+
@logger.debug("S3: has written remote file in bucket with canned ACL", :remote_filename => remote_filename, :bucket => @bucket, :canned_acl => @canned_acl)
|
158
|
+
end
|
141
159
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
config :restore, :validate => :boolean, :default => false
|
160
|
+
# This method is used for create new empty temporary files for use. Flag is needed for indicate new subsection time_file.
|
161
|
+
public
|
162
|
+
def create_temporary_file
|
163
|
+
filename = File.join(@temporary_directory, get_temporary_filename(@page_counter))
|
147
164
|
|
148
|
-
|
149
|
-
config :canned_acl, :validate => ["private", "public_read", "public_read_write", "authenticated_read"],
|
150
|
-
:default => "private"
|
165
|
+
@logger.debug("S3: Creating a new temporary file", :filename => filename)
|
151
166
|
|
152
|
-
|
153
|
-
|
167
|
+
@file_rotation_lock.synchronize do
|
168
|
+
unless @tempfile.nil?
|
169
|
+
@tempfile.close
|
170
|
+
end
|
154
171
|
|
155
|
-
|
172
|
+
@tempfile = File.open(filename, "a")
|
173
|
+
end
|
174
|
+
end
|
156
175
|
|
157
|
-
|
176
|
+
public
|
177
|
+
def register
|
178
|
+
require "aws-sdk"
|
179
|
+
# required if using ruby version < 2.0
|
180
|
+
# http://ruby.awsblog.com/post/Tx16QY1CI5GVBFT/Threading-with-the-AWS-SDK-for-Ruby
|
181
|
+
AWS.eager_autoload!(AWS::S3)
|
158
182
|
|
159
|
-
|
160
|
-
:access_key_id => @access_key_id,
|
161
|
-
:secret_access_key => @secret_access_key,
|
162
|
-
:s3_endpoint => @endpoint_region
|
163
|
-
)
|
164
|
-
@s3 = AWS::S3.new
|
183
|
+
workers_not_supported
|
165
184
|
|
166
|
-
|
185
|
+
@s3 = aws_s3_config
|
186
|
+
@upload_queue = Queue.new
|
187
|
+
@file_rotation_lock = Mutex.new
|
167
188
|
|
168
|
-
|
169
|
-
|
189
|
+
if @prefix && @prefix =~ S3_INVALID_CHARACTERS
|
190
|
+
@logger.error("S3: prefix contains invalid characters", :prefix => @prefix, :contains => S3_INVALID_CHARACTERS)
|
191
|
+
raise LogStash::ConfigurationError, "S3: prefix contains invalid characters"
|
192
|
+
end
|
170
193
|
|
171
|
-
|
172
|
-
|
173
|
-
start_time = Time.now
|
174
|
-
yield
|
175
|
-
elapsed = Time.now - start_time
|
176
|
-
sleep([interval - elapsed, 0].max)
|
194
|
+
if !Dir.exist?(@temporary_directory)
|
195
|
+
FileUtils.mkdir_p(@temporary_directory)
|
177
196
|
end
|
178
|
-
end
|
179
197
|
|
180
|
-
|
198
|
+
test_s3_write
|
181
199
|
|
182
|
-
|
183
|
-
|
200
|
+
restore_from_crashes if @restore == true
|
201
|
+
reset_page_counter
|
202
|
+
create_temporary_file
|
203
|
+
configure_periodic_rotation if time_file != 0
|
204
|
+
configure_upload_workers
|
184
205
|
|
185
|
-
|
186
|
-
|
187
|
-
|
206
|
+
@codec.on_event do |event, encoded_event|
|
207
|
+
handle_event(encoded_event)
|
208
|
+
end
|
188
209
|
end
|
189
210
|
|
190
|
-
# find and use the bucket
|
191
|
-
bucket = @s3.buckets[@bucket]
|
192
|
-
|
193
|
-
@logger.debug "S3: ready to write "+file_basename+" in bucket "+@bucket+", Fire in the hole!"
|
194
211
|
|
195
|
-
#
|
196
|
-
|
197
|
-
|
212
|
+
# Use the same method that Amazon use to check
|
213
|
+
# permission on the user bucket by creating a small file
|
214
|
+
public
|
215
|
+
def test_s3_write
|
216
|
+
@logger.debug("S3: Creating a test file on S3")
|
198
217
|
|
199
|
-
|
218
|
+
test_filename = File.join(@temporary_directory,
|
219
|
+
"logstash-programmatic-access-test-object-#{Time.now.to_i}")
|
200
220
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
def getFinalPath
|
221
|
+
File.open(test_filename, 'a') do |file|
|
222
|
+
file.write('test')
|
223
|
+
end
|
205
224
|
|
206
|
-
|
207
|
-
|
225
|
+
begin
|
226
|
+
write_on_bucket(test_filename)
|
227
|
+
delete_on_bucket(test_filename)
|
228
|
+
ensure
|
229
|
+
File.delete(test_filename)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
public
|
234
|
+
def restore_from_crashes
|
235
|
+
@logger.debug("S3: is attempting to verify previous crashes...")
|
236
|
+
|
237
|
+
Dir[File.join(@temporary_directory, "*.#{TEMPFILE_EXTENSION}")].each do |file|
|
238
|
+
name_file = File.basename(file)
|
239
|
+
@logger.warn("S3: have found temporary file the upload process crashed, uploading file to S3.", :filename => name_file)
|
240
|
+
move_file_to_bucket_async(file)
|
241
|
+
end
|
242
|
+
end
|
208
243
|
|
209
|
-
|
244
|
+
public
|
245
|
+
def move_file_to_bucket(file)
|
246
|
+
if !File.zero?(file)
|
247
|
+
write_on_bucket(file)
|
248
|
+
@logger.debug("S3: file was put on the upload thread", :filename => File.basename(file), :bucket => @bucket)
|
249
|
+
end
|
210
250
|
|
211
|
-
|
212
|
-
|
213
|
-
|
251
|
+
begin
|
252
|
+
File.delete(file)
|
253
|
+
rescue Errno::ENOENT
|
254
|
+
# Something else deleted the file, logging but not raising the issue
|
255
|
+
@logger.warn("S3: Cannot delete the temporary file since it doesn't exist on disk", :filename => File.basename(file))
|
256
|
+
rescue Errno::EACCES
|
257
|
+
@logger.error("S3: Logstash doesnt have the permission to delete the file in the temporary directory.", :filename => File.basename, :temporary_directory => @temporary_directory)
|
258
|
+
end
|
259
|
+
end
|
214
260
|
|
215
|
-
|
216
|
-
|
261
|
+
public
|
262
|
+
def periodic_interval
|
263
|
+
@time_file * 60
|
264
|
+
end
|
217
265
|
|
218
|
-
|
219
|
-
|
220
|
-
|
266
|
+
public
|
267
|
+
def get_temporary_filename(page_counter = 0)
|
268
|
+
current_time = Time.now
|
269
|
+
filename = "ls.s3.#{Socket.gethostname}.#{current_time.strftime("%Y-%m-%dT%H.%M")}"
|
221
270
|
|
222
|
-
|
223
|
-
|
271
|
+
if @tags.size > 0
|
272
|
+
return "#{filename}.tag_#{@tags.join('.')}.part#{page_counter}.#{TEMPFILE_EXTENSION}"
|
273
|
+
else
|
274
|
+
return "#{filename}.part#{page_counter}.#{TEMPFILE_EXTENSION}"
|
275
|
+
end
|
276
|
+
end
|
224
277
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
end
|
278
|
+
public
|
279
|
+
def receive(event)
|
280
|
+
return unless output?(event)
|
281
|
+
@codec.encode(event)
|
282
|
+
end
|
231
283
|
|
232
|
-
|
284
|
+
public
|
285
|
+
def rotate_events_log?
|
286
|
+
@tempfile.size > @size_file
|
287
|
+
end
|
233
288
|
|
234
|
-
|
235
|
-
|
289
|
+
public
|
290
|
+
def write_events_to_multiple_files?
|
291
|
+
@size_file > 0
|
292
|
+
end
|
236
293
|
|
237
|
-
|
238
|
-
|
294
|
+
public
|
295
|
+
def write_to_tempfile(event)
|
296
|
+
begin
|
297
|
+
@logger.debug("S3: put event into tempfile ", :tempfile => File.basename(@tempfile))
|
298
|
+
|
299
|
+
@file_rotation_lock.synchronize do
|
300
|
+
@tempfile.syswrite(event)
|
301
|
+
end
|
302
|
+
rescue Errno::ENOSPC
|
303
|
+
@logger.error("S3: No space left in temporary directory", :temporary_directory => @temporary_directory)
|
304
|
+
teardown
|
305
|
+
end
|
306
|
+
end
|
239
307
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
308
|
+
public
|
309
|
+
def teardown
|
310
|
+
shutdown_upload_workers
|
311
|
+
@periodic_rotation_thread.stop! if @periodic_rotation_thread
|
244
312
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
@tempFile = File.new(@current_final_path+".part"+@sizeCounter.to_s+".txt", "w")
|
249
|
-
end
|
313
|
+
@tempfile.close
|
314
|
+
finished
|
315
|
+
end
|
250
316
|
|
251
|
-
|
317
|
+
private
|
318
|
+
def shutdown_upload_workers
|
319
|
+
@logger.debug("S3: Gracefully shutdown the upload workers")
|
320
|
+
@upload_queue << LogStash::ShutdownEvent
|
321
|
+
end
|
252
322
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
323
|
+
private
|
324
|
+
def handle_event(encoded_event)
|
325
|
+
if write_events_to_multiple_files?
|
326
|
+
if rotate_events_log?
|
327
|
+
@logger.debug("S3: tempfile is too large, let's bucket it and create new file", :tempfile => File.basename(@tempfile))
|
328
|
+
|
329
|
+
move_file_to_bucket_async(@tempfile.path)
|
330
|
+
next_page
|
331
|
+
create_temporary_file
|
332
|
+
else
|
333
|
+
@logger.debug("S3: tempfile file size report.", :tempfile_size => @tempfile.size, :size_file => @size_file)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
write_to_tempfile(encoded_event)
|
338
|
+
end
|
257
339
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
end
|
263
|
-
end
|
340
|
+
private
|
341
|
+
def configure_periodic_rotation
|
342
|
+
@periodic_rotation_thread = Stud::Task.new do
|
343
|
+
LogStash::Util::set_thread_name("<S3 periodic uploader")
|
264
344
|
|
265
|
-
|
266
|
-
|
267
|
-
Dir.mkdir(@temp_directory)
|
268
|
-
else
|
269
|
-
@logger.debug "S3: Directory "+@temp_directory+" exist, nothing to do"
|
270
|
-
end
|
345
|
+
Stud.interval(periodic_interval, :sleep_then_run => true) do
|
346
|
+
@logger.debug("S3: time_file triggered, bucketing the file", :filename => @tempfile.path)
|
271
347
|
|
272
|
-
|
273
|
-
|
348
|
+
move_file_to_bucket_async(@tempfile.path)
|
349
|
+
next_page
|
350
|
+
create_temporary_file
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
274
354
|
|
275
|
-
|
276
|
-
|
355
|
+
private
|
356
|
+
def configure_upload_workers
|
357
|
+
@logger.debug("S3: Configure upload workers")
|
277
358
|
|
278
|
-
|
359
|
+
@upload_workers = @upload_workers_count.times.map do |worker_id|
|
360
|
+
Stud::Task.new do
|
361
|
+
LogStash::Util::set_thread_name("<S3 upload worker #{worker_id}")
|
279
362
|
|
280
|
-
|
281
|
-
|
282
|
-
@thread = time_alert(@time_file*60) do
|
283
|
-
if (first_time == false)
|
284
|
-
@logger.debug "S3: time_file triggered, let's bucket the file if dosen't empty and create new file "
|
285
|
-
upFile(false, File.basename(@tempFile))
|
286
|
-
newFile(true)
|
287
|
-
else
|
288
|
-
first_time = false
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
363
|
+
while true do
|
364
|
+
@logger.debug("S3: upload worker is waiting for a new file to upload.", :worker_id => worker_id)
|
292
365
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
return unless output?(event)
|
298
|
-
|
299
|
-
# Prepare format of Events
|
300
|
-
if (@format == "plain")
|
301
|
-
message = self.class.format_message(event)
|
302
|
-
elsif (@format == "json")
|
303
|
-
message = event.to_json
|
304
|
-
else
|
305
|
-
message = event.to_s
|
366
|
+
upload_worker
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
306
370
|
end
|
307
371
|
|
308
|
-
|
309
|
-
|
372
|
+
private
|
373
|
+
def upload_worker
|
374
|
+
file = @upload_queue.deq
|
375
|
+
|
376
|
+
case file
|
377
|
+
when LogStash::ShutdownEvent
|
378
|
+
@logger.debug("S3: upload worker is shutting down gracefuly")
|
379
|
+
@upload_queue.enq(LogStash::ShutdownEvent)
|
380
|
+
else
|
381
|
+
@logger.debug("S3: upload working is uploading a new file", :filename => File.basename(file))
|
382
|
+
move_file_to_bucket(file)
|
383
|
+
end
|
310
384
|
end
|
311
385
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
@logger.debug "S3: File have size: "+@tempFile.size.to_s+" and size_file is: "+ @size_file.to_s
|
318
|
-
@logger.debug "S3: put event into: "+File.basename(@tempFile)
|
319
|
-
|
320
|
-
# Put the event in the file, now!
|
321
|
-
File.open(@tempFile, 'a') do |file|
|
322
|
-
file.puts message
|
323
|
-
file.write "\n"
|
324
|
-
end
|
386
|
+
private
|
387
|
+
def next_page
|
388
|
+
@page_counter += 1
|
389
|
+
end
|
325
390
|
|
326
|
-
|
391
|
+
private
|
392
|
+
def reset_page_counter
|
393
|
+
@page_counter = 0
|
394
|
+
end
|
327
395
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
newFile(false)
|
396
|
+
private
|
397
|
+
def delete_on_bucket(filename)
|
398
|
+
bucket = @s3.buckets[@bucket]
|
332
399
|
|
333
|
-
|
400
|
+
remote_filename = "#{@prefix}#{File.basename(filename)}"
|
334
401
|
|
335
|
-
|
336
|
-
else
|
402
|
+
@logger.debug("S3: delete file from bucket", :remote_filename => remote_filename, :bucket => @bucket)
|
337
403
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
404
|
+
begin
|
405
|
+
# prepare for write the file
|
406
|
+
object = bucket.objects[remote_filename]
|
407
|
+
object.delete
|
408
|
+
rescue AWS::Errors::Base => e
|
409
|
+
@logger.error("S3: AWS error", :error => e)
|
410
|
+
raise LogStash::ConfigurationError, "AWS Configuration Error"
|
342
411
|
end
|
343
412
|
end
|
344
413
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
message << "Tags: #{event["tags"].join(', ')}\n"
|
351
|
-
message << "Fields: #{event.to_hash.inspect}\n"
|
352
|
-
message << "Message: #{event["message"]}"
|
353
|
-
end
|
354
|
-
|
414
|
+
private
|
415
|
+
def move_file_to_bucket_async(file)
|
416
|
+
@logger.debug("S3: Sending the file to the upload queue.", :filename => File.basename(file))
|
417
|
+
@upload_queue.enq(file)
|
418
|
+
end
|
355
419
|
end
|
356
|
-
|
357
|
-
# Enjoy it, by Bistic:)
|
data/logstash-output-s3.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-output-s3'
|
4
|
-
s.version = '0.1.
|
4
|
+
s.version = '0.1.2'
|
5
5
|
s.licenses = ['Apache License (2.0)']
|
6
6
|
s.summary = "This plugin was created for store the logstash's events into Amazon Simple Storage Service (Amazon S3)"
|
7
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"
|
@@ -23,6 +23,10 @@ Gem::Specification.new do |s|
|
|
23
23
|
s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
|
24
24
|
s.add_runtime_dependency 'logstash-mixin-aws'
|
25
25
|
s.add_runtime_dependency 'aws-sdk'
|
26
|
+
s.add_runtime_dependency 'stud', '~> 0.0.18'
|
26
27
|
s.add_development_dependency 'logstash-devutils'
|
28
|
+
s.add_development_dependency 'logstash-input-generator'
|
29
|
+
s.add_development_dependency 'logstash-input-stdin'
|
30
|
+
s.add_development_dependency 'logstash-codec-line'
|
27
31
|
end
|
28
32
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "logstash/devutils/rspec/spec_helper"
|
2
|
+
require "logstash/outputs/s3"
|
3
|
+
require 'socket'
|
4
|
+
require "aws-sdk"
|
5
|
+
require "fileutils"
|
6
|
+
require "stud/temporary"
|
7
|
+
require_relative "../supports/helpers"
|
8
|
+
|
9
|
+
describe LogStash::Outputs::S3, :integration => true, :s3 => true do
|
10
|
+
before do
|
11
|
+
Thread.abort_on_exception = true
|
12
|
+
end
|
13
|
+
|
14
|
+
let!(:minimal_settings) { { "access_key_id" => ENV['AWS_ACCESS_KEY_ID'],
|
15
|
+
"secret_access_key" => ENV['AWS_SECRET_ACCESS_KEY'],
|
16
|
+
"bucket" => ENV['AWS_LOGSTASH_TEST_BUCKET'],
|
17
|
+
"region" => ENV["AWS_REGION"] || "us-east-1",
|
18
|
+
"temporary_directory" => Stud::Temporary.pathname('temporary_directory') }}
|
19
|
+
|
20
|
+
let!(:s3_object) do
|
21
|
+
s3output = LogStash::Outputs::S3.new(minimal_settings)
|
22
|
+
s3output.register
|
23
|
+
s3output.s3
|
24
|
+
end
|
25
|
+
|
26
|
+
after(:all) do
|
27
|
+
delete_matching_keys_on_bucket('studtmp')
|
28
|
+
delete_matching_keys_on_bucket('my-prefix')
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#register" do
|
32
|
+
it "write a file on the bucket to check permissions" do
|
33
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
34
|
+
expect(s3.register).not_to raise_error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#write_on_bucket" do
|
39
|
+
after(:all) do
|
40
|
+
File.unlink(fake_data.path)
|
41
|
+
end
|
42
|
+
|
43
|
+
let!(:fake_data) { Stud::Temporary.file }
|
44
|
+
|
45
|
+
it "should prefix the file on the bucket if a prefix is specified" do
|
46
|
+
prefix = "my-prefix"
|
47
|
+
|
48
|
+
config = minimal_settings.merge({
|
49
|
+
"prefix" => prefix,
|
50
|
+
})
|
51
|
+
|
52
|
+
s3 = LogStash::Outputs::S3.new(config)
|
53
|
+
s3.register
|
54
|
+
s3.write_on_bucket(fake_data)
|
55
|
+
|
56
|
+
expect(key_exists_on_bucket?("#{prefix}#{File.basename(fake_data.path)}")).to eq(true)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should use the same local filename if no prefix is specified' do
|
60
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
61
|
+
s3.register
|
62
|
+
s3.write_on_bucket(fake_data)
|
63
|
+
|
64
|
+
expect(key_exists_on_bucket?(File.basename(fake_data.path))).to eq(true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#move_file_to_bucket" do
|
69
|
+
let!(:s3) { LogStash::Outputs::S3.new(minimal_settings) }
|
70
|
+
|
71
|
+
before do
|
72
|
+
s3.register
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should upload the file if the size > 0" do
|
76
|
+
tmp = Stud::Temporary.file
|
77
|
+
allow(File).to receive(:zero?).and_return(false)
|
78
|
+
s3.move_file_to_bucket(tmp)
|
79
|
+
expect(key_exists_on_bucket?(File.basename(tmp.path))).to eq(true)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#restore_from_crashes" do
|
84
|
+
it "read the temp directory and upload the matching file to s3" do
|
85
|
+
Stud::Temporary.pathname do |temp_path|
|
86
|
+
tempfile = File.open(File.join(temp_path, 'A'), 'w+') { |f| f.write('test')}
|
87
|
+
|
88
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings.merge({ "temporary_directory" => temp_path }))
|
89
|
+
s3.restore_from_crashes
|
90
|
+
|
91
|
+
expect(File.exist?(tempfile.path)).to eq(false)
|
92
|
+
expect(key_exists_on_bucket?(File.basename(tempfile.path))).to eq(true)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/spec/outputs/s3_spec.rb
CHANGED
@@ -1,6 +1,324 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "logstash/devutils/rspec/spec_helper"
|
3
|
-
require
|
3
|
+
require "logstash/outputs/s3"
|
4
|
+
require "logstash/codecs/line"
|
5
|
+
require "logstash/pipeline"
|
6
|
+
require "aws-sdk"
|
7
|
+
require "fileutils"
|
8
|
+
require_relative "../supports/helpers"
|
4
9
|
|
5
10
|
describe LogStash::Outputs::S3 do
|
11
|
+
before do
|
12
|
+
# We stub all the calls from S3, for more information see:
|
13
|
+
# http://ruby.awsblog.com/post/Tx2SU6TYJWQQLC3/Stubbing-AWS-Responses
|
14
|
+
AWS.stub!
|
15
|
+
|
16
|
+
Thread.abort_on_exception = true
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:minimal_settings) { { "access_key_id" => "1234",
|
20
|
+
"secret_access_key" => "secret",
|
21
|
+
"bucket" => "my-bucket" } }
|
22
|
+
|
23
|
+
describe "configuration" do
|
24
|
+
let!(:config) { { "endpoint_region" => "sa-east-1" } }
|
25
|
+
|
26
|
+
it "should support the deprecated endpoint_region as a configuration option" do
|
27
|
+
s3 = LogStash::Outputs::S3.new(config)
|
28
|
+
expect(s3.aws_options_hash[:s3_endpoint]).to eq("s3-sa-east-1.amazonaws.com")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should fallback to region if endpoint_region isnt defined" do
|
32
|
+
s3 = LogStash::Outputs::S3.new(config.merge({ "region" => 'sa-east-1' }))
|
33
|
+
expect(s3.aws_options_hash).to include(:s3_endpoint => "s3-sa-east-1.amazonaws.com")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#register" do
|
38
|
+
it "should create the tmp directory if it doesn't exist" do
|
39
|
+
temporary_directory = Stud::Temporary.pathname("temporary_directory")
|
40
|
+
|
41
|
+
config = {
|
42
|
+
"access_key_id" => "1234",
|
43
|
+
"secret_access_key" => "secret",
|
44
|
+
"bucket" => "logstash",
|
45
|
+
"size_file" => 10,
|
46
|
+
"temporary_directory" => temporary_directory
|
47
|
+
}
|
48
|
+
|
49
|
+
s3 = LogStash::Outputs::S3.new(config)
|
50
|
+
allow(s3).to receive(:test_s3_write)
|
51
|
+
s3.register
|
52
|
+
|
53
|
+
expect(Dir.exist?(temporary_directory)).to eq(true)
|
54
|
+
FileUtils.rm_r(temporary_directory)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should raise a ConfigurationError if the prefix contains one or more '\^`><' characters" do
|
58
|
+
config = {
|
59
|
+
"prefix" => "`no\><^"
|
60
|
+
}
|
61
|
+
|
62
|
+
s3 = LogStash::Outputs::S3.new(config)
|
63
|
+
|
64
|
+
expect {
|
65
|
+
s3.register
|
66
|
+
}.to raise_error(LogStash::ConfigurationError)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#generate_temporary_filename" do
|
71
|
+
before do
|
72
|
+
Socket.stub(:gethostname) { "logstash.local" }
|
73
|
+
Time.stub(:now) { Time.new('2015-10-09-09:00') }
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should add tags to the filename if present" do
|
77
|
+
config = minimal_settings.merge({ "tags" => ["elasticsearch", "logstash", "kibana"], "temporary_directory" => "/tmp/logstash"})
|
78
|
+
s3 = LogStash::Outputs::S3.new(config)
|
79
|
+
expect(s3.get_temporary_filename).to eq("ls.s3.logstash.local.2015-01-01T00.00.tag_elasticsearch.logstash.kibana.part0.txt")
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not add the tags to the filename" do
|
83
|
+
config = minimal_settings.merge({ "tags" => [], "temporary_directory" => "/tmp/logstash" })
|
84
|
+
s3 = LogStash::Outputs::S3.new(config)
|
85
|
+
expect(s3.get_temporary_filename(3)).to eq("ls.s3.logstash.local.2015-01-01T00.00.part3.txt")
|
86
|
+
end
|
87
|
+
|
88
|
+
it "normalized the temp directory to include the trailing slash if missing" do
|
89
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings.merge({ "temporary_directory" => "/tmp/logstash" }))
|
90
|
+
expect(s3.get_temporary_filename).to eq("ls.s3.logstash.local.2015-01-01T00.00.part0.txt")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#write_on_bucket" do
|
95
|
+
after(:all) do
|
96
|
+
File.unlink(fake_data.path)
|
97
|
+
end
|
98
|
+
|
99
|
+
let!(:fake_data) { Stud::Temporary.file }
|
100
|
+
|
101
|
+
let(:fake_bucket) do
|
102
|
+
s3 = double('S3Object')
|
103
|
+
s3.stub(:write)
|
104
|
+
s3
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should prefix the file on the bucket if a prefix is specified" do
|
108
|
+
prefix = "my-prefix"
|
109
|
+
|
110
|
+
config = minimal_settings.merge({
|
111
|
+
"prefix" => prefix,
|
112
|
+
"bucket" => "my-bucket"
|
113
|
+
})
|
114
|
+
|
115
|
+
expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:[]).with("#{prefix}#{File.basename(fake_data)}") { fake_bucket }
|
116
|
+
|
117
|
+
s3 = LogStash::Outputs::S3.new(config)
|
118
|
+
allow(s3).to receive(:test_s3_write)
|
119
|
+
s3.register
|
120
|
+
s3.write_on_bucket(fake_data)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should use the same local filename if no prefix is specified' do
|
124
|
+
config = minimal_settings.merge({
|
125
|
+
"bucket" => "my-bucket"
|
126
|
+
})
|
127
|
+
|
128
|
+
expect_any_instance_of(AWS::S3::ObjectCollection).to receive(:[]).with(File.basename(fake_data)) { fake_bucket }
|
129
|
+
|
130
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
131
|
+
allow(s3).to receive(:test_s3_write)
|
132
|
+
s3.register
|
133
|
+
s3.write_on_bucket(fake_data)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "#write_events_to_multiple_files?" do
|
138
|
+
it 'returns true if the size_file is != 0 ' do
|
139
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings.merge({ "size_file" => 200 }))
|
140
|
+
expect(s3.write_events_to_multiple_files?).to eq(true)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'returns false if size_file is zero or not set' do
|
144
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
145
|
+
expect(s3.write_events_to_multiple_files?).to eq(false)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#write_to_tempfile" do
|
150
|
+
it "should append the event to a file" do
|
151
|
+
Stud::Temporary.file("logstash", "a+") do |tmp|
|
152
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
153
|
+
allow(s3).to receive(:test_s3_write)
|
154
|
+
s3.register
|
155
|
+
s3.tempfile = tmp
|
156
|
+
s3.write_to_tempfile("test-write")
|
157
|
+
tmp.rewind
|
158
|
+
expect(tmp.read).to eq("test-write")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "#rotate_events_log" do
|
164
|
+
let(:s3) { LogStash::Outputs::S3.new(minimal_settings.merge({ "size_file" => 1024 })) }
|
165
|
+
|
166
|
+
it "returns true if the tempfile is over the file_size limit" do
|
167
|
+
Stud::Temporary.file do |tmp|
|
168
|
+
tmp.stub(:size) { 2024001 }
|
169
|
+
|
170
|
+
s3.tempfile = tmp
|
171
|
+
expect(s3.rotate_events_log?).to be(true)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
it "returns false if the tempfile is under the file_size limit" do
|
176
|
+
Stud::Temporary.file do |tmp|
|
177
|
+
tmp.stub(:size) { 100 }
|
178
|
+
|
179
|
+
s3.tempfile = tmp
|
180
|
+
expect(s3.rotate_events_log?).to eq(false)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "#move_file_to_bucket" do
|
186
|
+
let!(:s3) { LogStash::Outputs::S3.new(minimal_settings) }
|
187
|
+
|
188
|
+
before do
|
189
|
+
# Assume the AWS test credentials pass.
|
190
|
+
allow(s3).to receive(:test_s3_write)
|
191
|
+
s3.register
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should always delete the source file" do
|
195
|
+
tmp = Stud::Temporary.file
|
196
|
+
|
197
|
+
allow(File).to receive(:zero?).and_return(true)
|
198
|
+
expect(File).to receive(:delete).with(tmp)
|
199
|
+
|
200
|
+
s3.move_file_to_bucket(tmp)
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should not upload the file if the size of the file is zero' do
|
204
|
+
temp_file = Stud::Temporary.file
|
205
|
+
allow(temp_file).to receive(:zero?).and_return(true)
|
206
|
+
|
207
|
+
expect(s3).not_to receive(:write_on_bucket)
|
208
|
+
s3.move_file_to_bucket(temp_file)
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should upload the file if the size > 0" do
|
212
|
+
tmp = Stud::Temporary.file
|
213
|
+
|
214
|
+
allow(File).to receive(:zero?).and_return(false)
|
215
|
+
expect(s3).to receive(:write_on_bucket)
|
216
|
+
|
217
|
+
s3.move_file_to_bucket(tmp)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "#restore_from_crashes" do
|
222
|
+
it "read the temp directory and upload the matching file to s3" do
|
223
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings.merge({ "temporary_directory" => "/tmp/logstash/" }))
|
224
|
+
|
225
|
+
expect(Dir).to receive(:[]).with("/tmp/logstash/*.txt").and_return(["/tmp/logstash/01.txt"])
|
226
|
+
expect(s3).to receive(:move_file_to_bucket_async).with("/tmp/logstash/01.txt")
|
227
|
+
|
228
|
+
|
229
|
+
s3.restore_from_crashes
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
describe "#receive" do
|
234
|
+
it "should send the event through the codecs" do
|
235
|
+
data = {"foo" => "bar", "baz" => {"bah" => ["a","b","c"]}, "@timestamp" => "2014-05-30T02:52:17.929Z"}
|
236
|
+
event = LogStash::Event.new(data)
|
237
|
+
|
238
|
+
expect_any_instance_of(LogStash::Codecs::Line).to receive(:encode).with(event)
|
239
|
+
|
240
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings)
|
241
|
+
allow(s3).to receive(:test_s3_write)
|
242
|
+
s3.register
|
243
|
+
|
244
|
+
s3.receive(event)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "when rotating the temporary file" do
|
249
|
+
before { allow(File).to receive(:delete) }
|
250
|
+
|
251
|
+
it "doesn't skip events if using the size_file option" do
|
252
|
+
Stud::Temporary.directory do |temporary_directory|
|
253
|
+
size_file = rand(200..20000)
|
254
|
+
event_count = rand(300..15000)
|
255
|
+
|
256
|
+
config = %Q[
|
257
|
+
input {
|
258
|
+
generator {
|
259
|
+
count => #{event_count}
|
260
|
+
}
|
261
|
+
}
|
262
|
+
output {
|
263
|
+
s3 {
|
264
|
+
access_key_id => "1234"
|
265
|
+
secret_access_key => "secret"
|
266
|
+
size_file => #{size_file}
|
267
|
+
codec => line
|
268
|
+
temporary_directory => '#{temporary_directory}'
|
269
|
+
bucket => 'testing'
|
270
|
+
}
|
271
|
+
}
|
272
|
+
]
|
273
|
+
|
274
|
+
pipeline = LogStash::Pipeline.new(config)
|
275
|
+
|
276
|
+
pipeline_thread = Thread.new { pipeline.run }
|
277
|
+
sleep 0.1 while !pipeline.ready?
|
278
|
+
pipeline_thread.join
|
279
|
+
|
280
|
+
events_written_count = events_in_files(Dir[File.join(temporary_directory, 'ls.*.txt')])
|
281
|
+
expect(events_written_count).to eq(event_count)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
it "doesn't skip events if using the time_file option", :tag => :slow do
|
286
|
+
Stud::Temporary.directory do |temporary_directory|
|
287
|
+
time_file = rand(5..10)
|
288
|
+
number_of_rotation = rand(4..10)
|
289
|
+
|
290
|
+
config = {
|
291
|
+
"time_file" => time_file,
|
292
|
+
"codec" => "line",
|
293
|
+
"temporary_directory" => temporary_directory,
|
294
|
+
"bucket" => "testing"
|
295
|
+
}
|
296
|
+
|
297
|
+
s3 = LogStash::Outputs::S3.new(minimal_settings.merge(config))
|
298
|
+
# Make the test run in seconds intead of minutes..
|
299
|
+
allow(s3).to receive(:periodic_interval).and_return(time_file)
|
300
|
+
s3.register
|
301
|
+
|
302
|
+
# Force to have a few files rotation
|
303
|
+
stop_time = Time.now + (number_of_rotation * time_file)
|
304
|
+
event_count = 0
|
305
|
+
|
306
|
+
event = LogStash::Event.new("message" => "Hello World")
|
307
|
+
|
308
|
+
until Time.now > stop_time do
|
309
|
+
s3.receive(event)
|
310
|
+
event_count += 1
|
311
|
+
end
|
312
|
+
s3.teardown
|
313
|
+
|
314
|
+
generated_files = Dir[File.join(temporary_directory, 'ls.*.txt')]
|
315
|
+
|
316
|
+
events_written_count = events_in_files(generated_files)
|
317
|
+
|
318
|
+
# Skew times can affect the number of rotation..
|
319
|
+
expect(generated_files.count).to be_within(number_of_rotation).of(number_of_rotation + 1)
|
320
|
+
expect(events_written_count).to eq(event_count)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
6
324
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
def delete_matching_keys_on_bucket(prefix)
|
2
|
+
s3_object.buckets[minimal_settings["bucket"]].objects.with_prefix(prefix).each do |obj|
|
3
|
+
obj.delete
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
def key_exists_on_bucket?(key)
|
8
|
+
s3_object.buckets[minimal_settings["bucket"]].objects[key].exists?
|
9
|
+
end
|
10
|
+
|
11
|
+
def events_in_files(files)
|
12
|
+
files.collect { |file| File.foreach(file).count }.inject(&:+)
|
13
|
+
end
|
14
|
+
|
metadata
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-s3
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elasticsearch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
|
14
|
+
name: logstash
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
15
16
|
requirements:
|
16
17
|
- - '>='
|
17
18
|
- !ruby/object:Gem::Version
|
@@ -19,10 +20,7 @@ dependencies:
|
|
19
20
|
- - <
|
20
21
|
- !ruby/object:Gem::Version
|
21
22
|
version: 2.0.0
|
22
|
-
|
23
|
-
prerelease: false
|
24
|
-
type: :runtime
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirement: !ruby/object:Gem::Requirement
|
26
24
|
requirements:
|
27
25
|
- - '>='
|
28
26
|
- !ruby/object:Gem::Version
|
@@ -30,48 +28,106 @@ dependencies:
|
|
30
28
|
- - <
|
31
29
|
- !ruby/object:Gem::Version
|
32
30
|
version: 2.0.0
|
31
|
+
prerelease: false
|
32
|
+
type: :runtime
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
+
name: logstash-mixin-aws
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
34
40
|
requirement: !ruby/object:Gem::Requirement
|
35
41
|
requirements:
|
36
42
|
- - '>='
|
37
43
|
- !ruby/object:Gem::Version
|
38
44
|
version: '0'
|
39
|
-
name: logstash-mixin-aws
|
40
45
|
prerelease: false
|
41
46
|
type: :runtime
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: aws-sdk
|
42
49
|
version_requirements: !ruby/object:Gem::Requirement
|
43
50
|
requirements:
|
44
51
|
- - '>='
|
45
52
|
- !ruby/object:Gem::Version
|
46
53
|
version: '0'
|
47
|
-
- !ruby/object:Gem::Dependency
|
48
54
|
requirement: !ruby/object:Gem::Requirement
|
49
55
|
requirements:
|
50
56
|
- - '>='
|
51
57
|
- !ruby/object:Gem::Version
|
52
58
|
version: '0'
|
53
|
-
name: aws-sdk
|
54
59
|
prerelease: false
|
55
60
|
type: :runtime
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: stud
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.0.18
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ~>
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 0.0.18
|
73
|
+
prerelease: false
|
74
|
+
type: :runtime
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: logstash-devutils
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
prerelease: false
|
88
|
+
type: :development
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: logstash-input-generator
|
56
91
|
version_requirements: !ruby/object:Gem::Requirement
|
57
92
|
requirements:
|
58
93
|
- - '>='
|
59
94
|
- !ruby/object:Gem::Version
|
60
95
|
version: '0'
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
prerelease: false
|
102
|
+
type: :development
|
61
103
|
- !ruby/object:Gem::Dependency
|
104
|
+
name: logstash-input-stdin
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
62
110
|
requirement: !ruby/object:Gem::Requirement
|
63
111
|
requirements:
|
64
112
|
- - '>='
|
65
113
|
- !ruby/object:Gem::Version
|
66
114
|
version: '0'
|
67
|
-
name: logstash-devutils
|
68
115
|
prerelease: false
|
69
116
|
type: :development
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: logstash-codec-line
|
70
119
|
version_requirements: !ruby/object:Gem::Requirement
|
71
120
|
requirements:
|
72
121
|
- - '>='
|
73
122
|
- !ruby/object:Gem::Version
|
74
123
|
version: '0'
|
124
|
+
requirement: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - '>='
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
prerelease: false
|
130
|
+
type: :development
|
75
131
|
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
|
76
132
|
email: info@elasticsearch.com
|
77
133
|
executables: []
|
@@ -81,10 +137,13 @@ files:
|
|
81
137
|
- .gitignore
|
82
138
|
- Gemfile
|
83
139
|
- LICENSE
|
140
|
+
- README
|
84
141
|
- Rakefile
|
85
142
|
- lib/logstash/outputs/s3.rb
|
86
143
|
- logstash-output-s3.gemspec
|
144
|
+
- spec/integration/s3_spec.rb
|
87
145
|
- spec/outputs/s3_spec.rb
|
146
|
+
- spec/supports/helpers.rb
|
88
147
|
homepage: http://www.elasticsearch.org/guide/en/logstash/current/index.html
|
89
148
|
licenses:
|
90
149
|
- Apache License (2.0)
|
@@ -112,4 +171,6 @@ signing_key:
|
|
112
171
|
specification_version: 4
|
113
172
|
summary: This plugin was created for store the logstash's events into Amazon Simple Storage Service (Amazon S3)
|
114
173
|
test_files:
|
174
|
+
- spec/integration/s3_spec.rb
|
115
175
|
- spec/outputs/s3_spec.rb
|
176
|
+
- spec/supports/helpers.rb
|