logstash-output-qingstor 0.2.8 → 0.3.1
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/CHANGELOG.md +6 -0
- data/lib/logstash/outputs/qingstor/multipart_uploader.rb +94 -0
- data/lib/logstash/outputs/qingstor/rotation_policy.rb +59 -44
- data/lib/logstash/outputs/qingstor/temporary_file.rb +1 -1
- data/lib/logstash/outputs/qingstor/uploader.rb +20 -9
- data/lib/logstash/outputs/qingstor.rb +3 -5
- data/logstash-output-qingstor.gemspec +1 -1
- data/spec/outputs/qingstor_spec.rb +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b634b7aee3754a9bfc4f88269a0ef6fc4135249d1143440ccae417c67852b47f
|
4
|
+
data.tar.gz: 05c469d151a6647bb985b15a368dc3c607d69bf6eee496e6ba1d5841185b2b75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2579b496ce9f0da3c9831d4de53f70eb23c7f51febe23b7c82052df1cb2d99337c23724b34a3f8d66ddd385efe468e17f158bb8d786b4ceb5a6ee309ace88a2c
|
7
|
+
data.tar.gz: 545228916a49aa445d47b8242ef062c1d78834590352c7d32f81fa6cb4f93acc7208c78d3bbed7b133f5a3fbee2abd46aa6ab2dc5e7912cacb2db303dc25005e
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'logstash/outputs/qingstor'
|
4
|
+
|
5
|
+
module LogStash
|
6
|
+
module Outputs
|
7
|
+
class Qingstor
|
8
|
+
class Uploader
|
9
|
+
class MultipartUploader
|
10
|
+
attr_reader :bucket, :logger
|
11
|
+
attr_accessor :upload_headers
|
12
|
+
|
13
|
+
# According to QingStor API doc, the minimal part size is 4MB.
|
14
|
+
# the maximal part size is 1GB.
|
15
|
+
# Here the minimal size would never be used and the maximal part size is 50MB.
|
16
|
+
# Overlarge file size would consume java heap space and throw Exceptions.
|
17
|
+
MINIMAL_PART_SIZE = 4 * 1024 * 1024
|
18
|
+
MAXIMAL_PART_SIZE = 50 * 1024 * 1024
|
19
|
+
|
20
|
+
def initialize(bucket, logger, object, upload_headers)
|
21
|
+
@bucket = bucket
|
22
|
+
@logger = logger
|
23
|
+
@object = object
|
24
|
+
@upload_headers = upload_headers
|
25
|
+
end
|
26
|
+
|
27
|
+
def upload
|
28
|
+
initiate_multipart_upload
|
29
|
+
segment_size = calculate_segment
|
30
|
+
upload_multipart(segment_size)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initiate_multipart_upload
|
34
|
+
res = @bucket.initiate_multipart_upload(@object.key, @upload_headers)
|
35
|
+
@upload_headers['upload_id'] = res.fetch(:upload_id) do
|
36
|
+
raise(QingStor::Error, :error_msg => res)
|
37
|
+
end
|
38
|
+
@logger.debug('initiated upload id', :file => @object.key,
|
39
|
+
:upload_id => res[:upload_id])
|
40
|
+
res[:upload_id]
|
41
|
+
end
|
42
|
+
|
43
|
+
def upload_multipart(part_number = 0, segment_size)
|
44
|
+
::File.open(@object.path) do |f|
|
45
|
+
f.seek(part_number * segment_size) if part_number != 0
|
46
|
+
while data = f.read(segment_size)
|
47
|
+
content_md5 = Digest::MD5.hexdigest(data)
|
48
|
+
@upload_headers['body'] = data
|
49
|
+
@upload_headers['content_length'] = segment_size
|
50
|
+
@upload_headers['content_md5'] = content_md5
|
51
|
+
@upload_headers['part_number'] = part_number
|
52
|
+
@logger.debug('multipart uploading: ',
|
53
|
+
:file => @object.key,
|
54
|
+
:part_number => part_number)
|
55
|
+
@bucket.upload_multipart(@object.key, @upload_headers)
|
56
|
+
part_number += 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
complete_multipart_upload(part_number - 1)
|
61
|
+
end
|
62
|
+
|
63
|
+
def complete_multipart_upload(last_part_number)
|
64
|
+
object_parts = (0..last_part_number).to_a.map {|x| {'part_number' => x}}
|
65
|
+
@upload_headers['object_parts'] = object_parts
|
66
|
+
res = @bucket.complete_multipart_upload(@object.key, @upload_headers)
|
67
|
+
@logger.debug('multipart uploading completed', :file => @object.key)
|
68
|
+
end
|
69
|
+
|
70
|
+
def calculate_segment
|
71
|
+
segment_size = @object.size
|
72
|
+
|
73
|
+
while segment_size >= MAXIMAL_PART_SIZE
|
74
|
+
segment_size /= 2.0
|
75
|
+
segment_size = segment_size.ceil
|
76
|
+
end
|
77
|
+
|
78
|
+
segment_size
|
79
|
+
end
|
80
|
+
|
81
|
+
def resume_multipart_upload(upload_id)
|
82
|
+
@logger.debug('resume multipart uploading', :file => @object.key,
|
83
|
+
:upload_id => upload_id)
|
84
|
+
@upload_headers['upload_id'] = upload_id
|
85
|
+
res = @bucket.list_multipart(@object.key, upload_headers)
|
86
|
+
segment_size = res[:object_parts][0][:size]
|
87
|
+
part_number = res[:object_parts].count
|
88
|
+
upload_multipart(part_number, segment_size)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -6,71 +6,86 @@ module LogStash
|
|
6
6
|
module Outputs
|
7
7
|
class Qingstor
|
8
8
|
class RotationPolicy
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
when 'time'
|
13
|
-
init_time(file_time)
|
14
|
-
when 'size'
|
15
|
-
init_size(file_size)
|
16
|
-
when 'size_and_time'
|
17
|
-
init_size(file_size)
|
18
|
-
init_time(file_time)
|
9
|
+
class Policy
|
10
|
+
def to_s
|
11
|
+
name
|
19
12
|
end
|
20
|
-
end
|
21
13
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
def name
|
15
|
+
self.class.name.split('::').last.downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def needs_periodic?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def positive_check(*arg)
|
23
|
+
arg.each do |x|
|
24
|
+
raise(LogStash::ConfigurationError,
|
25
|
+
"#{name} policy needs positive arguments") if x <= 0
|
26
|
+
end
|
26
27
|
end
|
27
|
-
@file_size = file_size
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
class Time < Policy
|
31
|
+
def initialize(file_size, file_time)
|
32
|
+
@file_time = file_time
|
33
|
+
positive_check(@file_time)
|
34
|
+
end
|
35
|
+
|
36
|
+
def rotate?(file)
|
37
|
+
!file.empty? && (::Time.now - file.ctime) >= @file_time
|
34
38
|
end
|
35
|
-
|
39
|
+
end
|
40
|
+
|
41
|
+
class Size < Policy
|
42
|
+
def initialize(file_size, file_time)
|
43
|
+
@file_size = file_size
|
44
|
+
positive_check(@file_size)
|
45
|
+
end
|
46
|
+
|
47
|
+
def rotate?(file)
|
48
|
+
file.size >= @file_size
|
49
|
+
end
|
50
|
+
|
51
|
+
def needs_periodic?; false; end
|
36
52
|
end
|
37
53
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
54
|
+
class SizeAndTime < Policy
|
55
|
+
def initialize(file_size, file_time)
|
56
|
+
@file_size, @file_time = file_size, file_time
|
57
|
+
positive_check(file_size, file_time)
|
58
|
+
end
|
59
|
+
|
60
|
+
def rotate?(file)
|
61
|
+
(!file.empty? && (::Time.now - file.ctime) >= @file_time) ||
|
62
|
+
(file.size >= @file_size)
|
46
63
|
end
|
47
64
|
end
|
48
65
|
|
49
|
-
def
|
50
|
-
|
66
|
+
def Policy(policy, file_size, file_time)
|
67
|
+
case policy
|
68
|
+
when Policy then policy
|
69
|
+
else
|
70
|
+
self.class.const_get(policy.to_s.split('_').map(&:capitalize).join)
|
71
|
+
.new(file_size, file_time)
|
72
|
+
end
|
51
73
|
end
|
52
74
|
|
53
|
-
def
|
54
|
-
|
75
|
+
def initialize(policy, file_size, file_time)
|
76
|
+
@policy = Policy(policy, file_size, file_time)
|
55
77
|
end
|
56
78
|
|
57
|
-
def
|
58
|
-
|
79
|
+
def rotate?(file)
|
80
|
+
@policy.rotate?(file)
|
59
81
|
end
|
60
82
|
|
61
83
|
def needs_periodic?
|
62
|
-
|
63
|
-
when 'time' then
|
64
|
-
true
|
65
|
-
when 'size_and_time' then
|
66
|
-
true
|
67
|
-
else
|
68
|
-
false
|
69
|
-
end
|
84
|
+
@policy.needs_periodic?
|
70
85
|
end
|
71
86
|
|
72
87
|
def to_s
|
73
|
-
|
88
|
+
@policy.to_s
|
74
89
|
end
|
75
90
|
end
|
76
91
|
end
|
@@ -10,6 +10,8 @@ module LogStash
|
|
10
10
|
module Outputs
|
11
11
|
class Qingstor
|
12
12
|
class Uploader
|
13
|
+
require 'logstash/outputs/qingstor/multipart_uploader'
|
14
|
+
|
13
15
|
TIME_BEFORE_RETRYING_SECONDS = 1
|
14
16
|
DEFAULT_THREADPOOL = Concurrent::ThreadPoolExecutor.new(
|
15
17
|
:min_thread => 1,
|
@@ -34,29 +36,38 @@ module LogStash
|
|
34
36
|
|
35
37
|
def upload(file, options = {})
|
36
38
|
upload_options = options.fetch(:upload_options, {})
|
39
|
+
upload_headers = process_encrypt_options(upload_options)
|
40
|
+
|
41
|
+
if file.size > 50 * 1024 * 1024
|
42
|
+
@logger.debug('multipart uploading file', :file => file.key)
|
43
|
+
multipart_uploader = MultipartUploader.new(@bucket, @logger, file, upload_headers)
|
44
|
+
multipart_uploader.upload
|
45
|
+
else
|
46
|
+
upload_headers['content_md5'] = Digest::MD5.file(file.path).to_s
|
47
|
+
upload_headers['body'] = ::File.read(file.path)
|
48
|
+
@logger.debug('uploading file', :file => file.key)
|
49
|
+
@bucket.put_object(file.key, upload_headers)
|
50
|
+
end
|
37
51
|
|
38
|
-
|
52
|
+
options[:on_complete].call(file) unless options[:on_complete].nil?
|
53
|
+
end
|
39
54
|
|
40
|
-
|
41
|
-
|
42
|
-
'body' => ::File.read(file.path)
|
43
|
-
}
|
55
|
+
def process_encrypt_options(upload_options)
|
56
|
+
res = {}
|
44
57
|
|
45
58
|
unless upload_options[:server_side_encryption_algorithm].nil?
|
46
59
|
base64_key = Base64.strict_encode64(upload_options[:customer_key])
|
47
60
|
key_md5 = Digest::MD5.hexdigest(upload_options[:customer_key])
|
48
61
|
base64_key_md5 = Base64.strict_encode64(key_md5)
|
49
|
-
|
62
|
+
res.merge!(
|
50
63
|
'x_qs_encryption_customer_algorithm' =>
|
51
64
|
upload_options[:server_side_encryption_algorithm],
|
52
65
|
'x_qs_encryption_customer_key' => base64_key,
|
53
66
|
'x_qs_encryption_customer_key_md5' => base64_key_md5
|
54
67
|
)
|
55
68
|
end
|
56
|
-
@logger.debug('uploading file', :file => file.key)
|
57
|
-
bucket.put_object(file.key, upload_headers)
|
58
69
|
|
59
|
-
|
70
|
+
res
|
60
71
|
end
|
61
72
|
|
62
73
|
def stop
|
@@ -256,11 +256,9 @@ class LogStash::Outputs::Qingstor < LogStash::Outputs::Base
|
|
256
256
|
.each do |file|
|
257
257
|
temp_file = TemporaryFile.create_from_existing_file(file,
|
258
258
|
temp_folder_path)
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
temp_file.key = 'Restored/' + Time.new.strftime('%Y-%m-%d/')
|
263
|
-
+ temp_file.key
|
259
|
+
# Now multipart uploader supports file size up to 500GB
|
260
|
+
if temp_file.size > 0
|
261
|
+
temp_file.key = 'Restored/' + Time.new.strftime('%Y-%m-%d/') + temp_file.key
|
264
262
|
@logger.debug('Recoving from crash and uploading',
|
265
263
|
:file => temp_file.key)
|
266
264
|
@crash_uploader.upload_async(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-output-qingstor'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.3.1'
|
4
4
|
s.licenses = ['Apache License (2.0)']
|
5
5
|
s.summary = 'logstash output plugin for qingstor'
|
6
6
|
s.description = 'Collect the outputs of logstash and store into QingStor'
|
@@ -28,6 +28,10 @@ describe LogStash::Outputs::Qingstor do
|
|
28
28
|
clean_remote_files
|
29
29
|
end
|
30
30
|
|
31
|
+
before do
|
32
|
+
FileUtils.mkdir_p(tmpdir) unless File.exist?(tmpdir)
|
33
|
+
end
|
34
|
+
|
31
35
|
it 'done work with minimal options' do
|
32
36
|
fetch_event(options, events_and_encoded)
|
33
37
|
expect(list_remote_file.size).to eq(1)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-qingstor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Zhao
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,6 +111,7 @@ files:
|
|
111
111
|
- README_zh_CN.md
|
112
112
|
- lib/logstash/outputs/qingstor.rb
|
113
113
|
- lib/logstash/outputs/qingstor/file_repository.rb
|
114
|
+
- lib/logstash/outputs/qingstor/multipart_uploader.rb
|
114
115
|
- lib/logstash/outputs/qingstor/qingstor_validator.rb
|
115
116
|
- lib/logstash/outputs/qingstor/rotation_policy.rb
|
116
117
|
- lib/logstash/outputs/qingstor/temporary_file.rb
|