fluent-plugin-aliyun-oss 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/AUTHORS +1 -0
- data/ChangeLog +2 -0
- data/Gemfile +6 -0
- data/README.md +342 -0
- data/Rakefile +0 -0
- data/VERSION +1 -0
- data/fluent-plugin-oss.gemspec +24 -0
- data/lib/fluent/plugin/in_oss.rb +348 -0
- data/lib/fluent/plugin/mns/message.rb +52 -0
- data/lib/fluent/plugin/mns/request.rb +81 -0
- data/lib/fluent/plugin/oss_compressor_gzip_command.rb +55 -0
- data/lib/fluent/plugin/oss_compressor_lzma2.rb +45 -0
- data/lib/fluent/plugin/oss_compressor_lzo.rb +45 -0
- data/lib/fluent/plugin/oss_decompressor_gzip_command.rb +41 -0
- data/lib/fluent/plugin/oss_decompressor_lzma2.rb +36 -0
- data/lib/fluent/plugin/oss_decompressor_lzo.rb +36 -0
- data/lib/fluent/plugin/out_oss.rb +423 -0
- data/test/plugin/test_in_oss.rb +166 -0
- data/test/plugin/test_out_oss.rb +175 -0
- metadata +159 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module Fluent
|
4
|
+
module Plugin
|
5
|
+
module MNS
|
6
|
+
# Class for Aliyun MNS Message.
|
7
|
+
class Message
|
8
|
+
include REXML
|
9
|
+
|
10
|
+
attr_reader :queue, :id, :body_md5, :body, :receipt_handle, :enqueue_at,
|
11
|
+
:first_enqueue_at, :next_visible_at, :dequeue_count, :priority
|
12
|
+
|
13
|
+
def initialize(queue, content)
|
14
|
+
@queue = queue
|
15
|
+
|
16
|
+
doc = Document.new(content)
|
17
|
+
doc.elements[1].each do |e|
|
18
|
+
if e.node_type == :element
|
19
|
+
if e.name == 'MessageId'
|
20
|
+
@id = e.text
|
21
|
+
elsif e.name == 'MessageBodyMD5'
|
22
|
+
@body_md5 = e.text
|
23
|
+
elsif e.name == 'MessageBody'
|
24
|
+
@body = e.text
|
25
|
+
elsif e.name == 'EnqueueTime'
|
26
|
+
@enqueue_at = e.text.to_i
|
27
|
+
elsif e.name == 'FirstDequeueTime'
|
28
|
+
@first_enqueue_at = e.text.to_i
|
29
|
+
elsif e.name == 'DequeueCount'
|
30
|
+
@dequeue_count = e.text.to_i
|
31
|
+
elsif e.name == 'Priority'
|
32
|
+
@priority = e.text.to_i
|
33
|
+
elsif e.name == 'ReceiptHandle'
|
34
|
+
@receipt_handle = e.text
|
35
|
+
elsif e.name == 'NextVisibleTime'
|
36
|
+
@next_visible_at = e.text.to_i
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# verify body
|
42
|
+
md5 = Digest::MD5.hexdigest(body).upcase
|
43
|
+
unless md5 == body_md5
|
44
|
+
raise Exception,
|
45
|
+
'Invalid MNS Body, MD5 does not match, '\
|
46
|
+
"MD5 #{body_md5}, expect MD5 #{md5}, Body: #{body}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module Fluent
|
5
|
+
module Plugin
|
6
|
+
module MNS
|
7
|
+
# Class for Aliyun MNS Request.
|
8
|
+
class Request
|
9
|
+
include REXML
|
10
|
+
|
11
|
+
attr_reader :log, :uri, :method, :body, :content_md5, :content_type,
|
12
|
+
:content_length, :mns_headers, :access_key_id,
|
13
|
+
:access_key_secret, :endpoint
|
14
|
+
|
15
|
+
def initialize(opts, headers, params)
|
16
|
+
@log = opts[:log]
|
17
|
+
conf = {
|
18
|
+
host: opts[:endpoint],
|
19
|
+
path: opts[:path]
|
20
|
+
}
|
21
|
+
|
22
|
+
conf[:query] = URI.encode_www_form(params) unless params.empty?
|
23
|
+
@uri = URI::HTTP.build(conf)
|
24
|
+
@method = opts[:method].to_s.downcase
|
25
|
+
@mns_headers = headers.merge('x-mns-version' => '2015-06-06')
|
26
|
+
@access_key_id = opts[:access_key_id]
|
27
|
+
@access_key_secret = opts[:access_key_secret]
|
28
|
+
|
29
|
+
log.info uri.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def content(type, values = {})
|
33
|
+
ns = 'http://mns.aliyuncs.com/doc/v1/'
|
34
|
+
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
35
|
+
xml.send(type.to_sym, xmlns: ns) do |b|
|
36
|
+
values.each { |k, v| b.send k.to_sym, v }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@body = builder.to_xml
|
40
|
+
@content_md5 = Base64.encode64(Digest::MD5.hexdigest(body)).chop
|
41
|
+
@content_length = body.size
|
42
|
+
@content_type = 'text/xml;charset=utf-8'
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute
|
46
|
+
date = DateTime.now.httpdate
|
47
|
+
headers = {
|
48
|
+
'Authorization' => authorization(date),
|
49
|
+
'Content-Length' => content_length || 0,
|
50
|
+
'Content-Type' => content_type,
|
51
|
+
'Content-MD5' => content_md5,
|
52
|
+
'Date' => date,
|
53
|
+
'Host' => uri.host
|
54
|
+
}.merge(@mns_headers).reject { |k, v| v.nil? }
|
55
|
+
|
56
|
+
begin
|
57
|
+
RestClient.send *[method, uri.to_s, headers, body].compact
|
58
|
+
rescue RestClient::Exception => e
|
59
|
+
doc = Document.new(e.response.to_s)
|
60
|
+
doc.elements[1].each do |e|
|
61
|
+
next unless e.node_type == :element
|
62
|
+
return nil if (e.name == 'Code') && (e.text == 'MessageNotExist')
|
63
|
+
end
|
64
|
+
|
65
|
+
log.error e.response
|
66
|
+
|
67
|
+
raise e
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def authorization(date)
|
72
|
+
canonical_resource = [uri.path, uri.query].compact.join('?')
|
73
|
+
canonical_headers = mns_headers.sort.collect { |k, v| "#{k.downcase}:#{v}" }.join("\n")
|
74
|
+
signature = [method.to_s.upcase, content_md5 || '', content_type || '', date, canonical_headers, canonical_resource].join("\n")
|
75
|
+
sha1 = OpenSSL::HMAC.digest('sha1', access_key_secret, signature)
|
76
|
+
"MNS #{access_key_id}:#{Base64.encode64(sha1).chop}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSOutput
|
4
|
+
# This class uses gzip command to compress chunks.
|
5
|
+
class GzipCommandCompressor < Compressor
|
6
|
+
OSSOutput.register_compressor('gzip_command', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: ''
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
|
13
|
+
check_command('gzip')
|
14
|
+
end
|
15
|
+
|
16
|
+
def ext
|
17
|
+
'gz'.freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
def content_type
|
21
|
+
'application/x-gzip'.freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def compress(chunk, file)
|
25
|
+
path = if @buffer_type == 'file'
|
26
|
+
chunk.path
|
27
|
+
else
|
28
|
+
out = Tempfile.new('chunk-gzip-out-')
|
29
|
+
out.binmode
|
30
|
+
chunk.write_to(out)
|
31
|
+
out.close
|
32
|
+
out.path
|
33
|
+
end
|
34
|
+
|
35
|
+
res = system "gzip #{@command_parameter} -c #{path} > #{file.path}"
|
36
|
+
|
37
|
+
unless res
|
38
|
+
log.warn "failed to execute gzip command. Fallback to GzipWriter. status = #{$?}"
|
39
|
+
begin
|
40
|
+
file.truncate(0)
|
41
|
+
gw = Zlib::GzipWriter.new(file)
|
42
|
+
chunk.write_to(gw)
|
43
|
+
gw.close
|
44
|
+
ensure
|
45
|
+
gw.close rescue nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
ensure
|
50
|
+
out.close(true) rescue nil unless @buffer_type == 'file'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSOutput
|
4
|
+
# This class uses xz command to compress chunks.
|
5
|
+
class LZMA2Compressor < Compressor
|
6
|
+
OSSOutput.register_compressor('lzma2', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: '-qf0'
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
check_command('xz', 'LZMA2')
|
13
|
+
end
|
14
|
+
|
15
|
+
def ext
|
16
|
+
'xz'.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
'application/x-xz'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def compress(chunk, file)
|
24
|
+
path = if @buffer_type == 'file'
|
25
|
+
chunk.path
|
26
|
+
else
|
27
|
+
out = Tempfile.new('chunk-xz-out-')
|
28
|
+
out.binmode
|
29
|
+
chunk.write_to(out)
|
30
|
+
out.close
|
31
|
+
out.path
|
32
|
+
end
|
33
|
+
|
34
|
+
res = system "xz #{@command_parameter} -c #{path} > #{file.path}"
|
35
|
+
unless res
|
36
|
+
log.warn "failed to execute xz command, status = #{$?}"
|
37
|
+
raise Fluent::Exception, "failed to execute xz command, status = #{$?}"
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
out.close(true) rescue nil unless @buffer_type == 'file'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSOutput
|
4
|
+
# This class uses lzop command to compress chunks.
|
5
|
+
class LZOCompressor < Compressor
|
6
|
+
OSSOutput.register_compressor('lzo', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: '-qf1'
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
check_command('lzop', 'LZO')
|
13
|
+
end
|
14
|
+
|
15
|
+
def ext
|
16
|
+
'lzo'.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
'application/x-lzop'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def compress(chunk, file)
|
24
|
+
path = if @buffer_type == 'file'
|
25
|
+
chunk.path
|
26
|
+
else
|
27
|
+
out = Tempfile.new('chunk-lzo-out-')
|
28
|
+
out.binmode
|
29
|
+
chunk.write_to(out)
|
30
|
+
out.close
|
31
|
+
out.path
|
32
|
+
end
|
33
|
+
|
34
|
+
res = system "lzop #{@command_parameter} -c #{path} > #{file.path}"
|
35
|
+
unless res
|
36
|
+
log.warn "failed to execute lzop command, status = #{$?}"
|
37
|
+
raise Fluent::Exception, "failed to execute lzop command, status = #{$?}"
|
38
|
+
end
|
39
|
+
ensure
|
40
|
+
out.close(true) rescue nil unless @buffer_type == 'file'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSInput
|
4
|
+
# This class uses gzip command to decompress chunks.
|
5
|
+
class GzipCommandDecompressor < Decompressor
|
6
|
+
OSSInput.register_decompressor('gzip_command', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: '-dc'
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
check_command('gzip')
|
13
|
+
end
|
14
|
+
|
15
|
+
def ext
|
16
|
+
'gz'.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
'application/x-gzip'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def decompress(io)
|
24
|
+
path = io.path
|
25
|
+
|
26
|
+
out, err, status = Open3.capture3("gzip #{@command_parameter} #{path}")
|
27
|
+
if status.success?
|
28
|
+
out
|
29
|
+
else
|
30
|
+
log.warn "failed to execute gzip command, #{err.to_s.gsub("\n",'')}, fallback to GzipReader."
|
31
|
+
|
32
|
+
begin
|
33
|
+
io.rewind
|
34
|
+
Zlib::GzipReader.wrap(io)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSInput
|
4
|
+
# This class uses xz command to decompress chunks.
|
5
|
+
class LZMA2Decompressor < Decompressor
|
6
|
+
OSSInput.register_decompressor('lzma2', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: '-qdc'
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
check_command('xz', 'LZMA')
|
13
|
+
end
|
14
|
+
|
15
|
+
def ext
|
16
|
+
'xz'.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
'application/x-xz'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def decompress(io)
|
24
|
+
path = io.path
|
25
|
+
|
26
|
+
out, err, status = Open3.capture3("xz #{@command_parameter} #{path}")
|
27
|
+
if status.success?
|
28
|
+
out
|
29
|
+
else
|
30
|
+
raise err.to_s.chomp
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fluent
|
2
|
+
module Plugin
|
3
|
+
class OSSInput
|
4
|
+
# This class uses lzop command to decompress chunks.
|
5
|
+
class LZODecompressor < Decompressor
|
6
|
+
OSSInput.register_decompressor('lzo', self)
|
7
|
+
|
8
|
+
config_param :command_parameter, :string, default: '-qdc'
|
9
|
+
|
10
|
+
def configure(conf)
|
11
|
+
super
|
12
|
+
check_command('lzop', 'LZO')
|
13
|
+
end
|
14
|
+
|
15
|
+
def ext
|
16
|
+
'lzo'.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def content_type
|
20
|
+
'application/x-lzop'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def decompress(io)
|
24
|
+
path = io.path
|
25
|
+
|
26
|
+
out, err, status = Open3.capture3("lzop #{@command_parameter} #{path}")
|
27
|
+
if status.success?
|
28
|
+
out
|
29
|
+
else
|
30
|
+
raise err.to_s.chomp
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,423 @@
|
|
1
|
+
require 'fluent/plugin/output'
|
2
|
+
require 'aliyun/oss'
|
3
|
+
require 'aliyun/sts'
|
4
|
+
|
5
|
+
# This is Fluent OSS Output Plugin
|
6
|
+
# Usage:
|
7
|
+
# In order to write output data to OSS, you should add configurations like below
|
8
|
+
# <match pattern>
|
9
|
+
# @type oss
|
10
|
+
# endpoint <OSS endpoint to connect to>
|
11
|
+
# bucket <Your bucket name>
|
12
|
+
# access_key_id <Your access key id>
|
13
|
+
# access_key_secret <Your access secret key>
|
14
|
+
# path <Path prefix of the files on OSS>
|
15
|
+
# key_format %{path}%{time_slice}_%{index}.%{file_extension}
|
16
|
+
# if you want to use ${tag} or %Y/%m/%d/ like syntax in path/key_format,
|
17
|
+
# need to specify tag for ${tag} and time for %Y/%m/%d in <buffer> argument.
|
18
|
+
# <buffer tag,time>
|
19
|
+
# @type file
|
20
|
+
# path /var/log/fluent/oss
|
21
|
+
# timekey 3600 # 1 hour partition
|
22
|
+
# timekey_wait 10m
|
23
|
+
# timekey_use_utc true # use utc
|
24
|
+
# </buffer>
|
25
|
+
# <format>
|
26
|
+
# @type json
|
27
|
+
# </format>
|
28
|
+
# </match>
|
29
|
+
module Fluent
|
30
|
+
# Fluent OSS Plugin
|
31
|
+
module Plugin
|
32
|
+
# OSSOutput class implementation
|
33
|
+
class OSSOutput < Output
|
34
|
+
Fluent::Plugin.register_output('oss', self)
|
35
|
+
|
36
|
+
helpers :compat_parameters, :formatter, :inject
|
37
|
+
|
38
|
+
desc 'OSS endpoint to connect to'
|
39
|
+
config_param :endpoint, :string
|
40
|
+
desc 'Your bucket name'
|
41
|
+
config_param :bucket, :string
|
42
|
+
desc 'Your access key id'
|
43
|
+
config_param :access_key_id, :string
|
44
|
+
desc 'Your access secret key'
|
45
|
+
config_param :access_key_secret, :string
|
46
|
+
desc 'Path prefix of the files on OSS'
|
47
|
+
config_param :path, :string, default: 'fluent/logs'
|
48
|
+
config_param :upload_crc_enable, :bool, default: true
|
49
|
+
config_param :download_crc_enable, :bool, default: true
|
50
|
+
desc 'Timeout for open connections'
|
51
|
+
config_param :open_timeout, :integer, default: 10
|
52
|
+
desc 'Timeout for read response'
|
53
|
+
config_param :read_timeout, :integer, default: 120
|
54
|
+
|
55
|
+
desc 'OSS SDK log directory'
|
56
|
+
config_param :oss_sdk_log_dir, :string, default: '/var/log/td-agent'
|
57
|
+
|
58
|
+
desc 'The format of OSS object keys'
|
59
|
+
config_param :key_format, :string, default: '%{path}/%{time_slice}_%{index}_%{thread_id}.%{file_extension}'
|
60
|
+
desc 'Archive format on OSS'
|
61
|
+
config_param :store_as, :string, default: 'gzip'
|
62
|
+
desc 'Create OSS bucket if it does not exists'
|
63
|
+
config_param :auto_create_bucket, :bool, default: false
|
64
|
+
desc 'Overwrite already existing path'
|
65
|
+
config_param :overwrite, :bool, default: false
|
66
|
+
desc 'Check bucket if exists or not'
|
67
|
+
config_param :check_bucket, :bool, default: true
|
68
|
+
desc 'Check object before creation'
|
69
|
+
config_param :check_object, :bool, default: true
|
70
|
+
desc 'The length of `%{hex_random}` placeholder(4-16)'
|
71
|
+
config_param :hex_random_length, :integer, default: 4
|
72
|
+
desc '`sprintf` format for `%{index}`'
|
73
|
+
config_param :index_format, :string, default: '%d'
|
74
|
+
desc 'Given a threshold to treat events as delay, output warning logs if delayed events were put into OSS'
|
75
|
+
config_param :warn_for_delay, :time, default: nil
|
76
|
+
|
77
|
+
DEFAULT_FORMAT_TYPE = 'out_file'.freeze
|
78
|
+
|
79
|
+
config_section :format do
|
80
|
+
config_set_default :@type, DEFAULT_FORMAT_TYPE
|
81
|
+
end
|
82
|
+
|
83
|
+
config_section :buffer do
|
84
|
+
config_set_default :chunk_keys, ['time']
|
85
|
+
config_set_default :timekey, (60 * 60 * 24)
|
86
|
+
end
|
87
|
+
|
88
|
+
MAX_HEX_RANDOM_LENGTH = 16
|
89
|
+
|
90
|
+
def configure(conf)
|
91
|
+
compat_parameters_convert(conf, :buffer, :formatter, :inject)
|
92
|
+
|
93
|
+
super
|
94
|
+
|
95
|
+
raise Fluent::ConfigError, 'Invalid oss endpoint' if @endpoint.nil?
|
96
|
+
|
97
|
+
if @hex_random_length > MAX_HEX_RANDOM_LENGTH
|
98
|
+
raise Fluent::ConfigError, 'hex_random_length parameter must be '\
|
99
|
+
"less than or equal to #{MAX_HEX_RANDOM_LENGTH}"
|
100
|
+
end
|
101
|
+
|
102
|
+
unless @index_format =~ /^%(0\d*)?[dxX]$/
|
103
|
+
raise Fluent::ConfigError, 'index_format parameter should follow '\
|
104
|
+
'`%[flags][width]type`. `0` is the only supported flag, '\
|
105
|
+
'and is mandatory if width is specified. '\
|
106
|
+
'`d`, `x` and `X` are supported types'
|
107
|
+
end
|
108
|
+
|
109
|
+
begin
|
110
|
+
@compressor = COMPRESSOR_REGISTRY.lookup(@store_as).new(buffer_type: @buffer_config[:@type], log: log)
|
111
|
+
rescue StandardError => e
|
112
|
+
log.warn "'#{@store_as}' not supported. Use 'text' instead: error = #{e.message}"
|
113
|
+
@compressor = TextCompressor.new
|
114
|
+
end
|
115
|
+
|
116
|
+
@compressor.configure(conf)
|
117
|
+
|
118
|
+
@formatter = formatter_create
|
119
|
+
|
120
|
+
process_key_format
|
121
|
+
|
122
|
+
unless @check_object
|
123
|
+
if config.has_key?('key_format')
|
124
|
+
log.warn "set 'check_object false' and key_format is "\
|
125
|
+
'specified. Check key_format is unique in each '\
|
126
|
+
'write. If not, existing file will be overwritten.'
|
127
|
+
else
|
128
|
+
log.warn "set 'check_object false' and key_format is "\
|
129
|
+
'not specified. Use '\
|
130
|
+
"'%{path}/%{time_slice}_%{hms_slice}_%{thread_id}.%{file_extension}' "\
|
131
|
+
'for key_format'
|
132
|
+
@key_format = '%{path}/%{time_slice}_%{hms_slice}_%{thread_id}.%{file_extension}'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
@configured_time_slice_format = conf['time_slice_format']
|
137
|
+
@values_for_oss_object_chunk = {}
|
138
|
+
@time_slice_with_tz = Fluent::Timezone.formatter(
|
139
|
+
@timekey_zone,
|
140
|
+
@configured_time_slice_format || timekey_to_timeformat(@buffer_config['timekey']))
|
141
|
+
end
|
142
|
+
|
143
|
+
def timekey_to_timeformat(timekey)
|
144
|
+
case timekey
|
145
|
+
when nil then ''
|
146
|
+
when 0...60 then '%Y%m%d-%H_%M_%S' # 60 exclusive
|
147
|
+
when 60...3600 then '%Y%m%d-%H_%M'
|
148
|
+
when 3600...86400 then '%Y%m%d-%H'
|
149
|
+
else '%Y%m%d'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def multi_workers_ready?
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
157
|
+
def initialize
|
158
|
+
super
|
159
|
+
@compressor = nil
|
160
|
+
@uuid_flush_enabled = false
|
161
|
+
end
|
162
|
+
|
163
|
+
def start
|
164
|
+
@oss_sdk_log_dir += '/' unless @oss_sdk_log_dir.end_with?('/')
|
165
|
+
Aliyun::Common::Logging.set_log_file(@oss_sdk_log_dir + Aliyun::Common::Logging::DEFAULT_LOG_FILE)
|
166
|
+
create_oss_client unless @oss
|
167
|
+
|
168
|
+
ensure_bucket if @check_bucket
|
169
|
+
super
|
170
|
+
end
|
171
|
+
|
172
|
+
def format(tag, time, record)
|
173
|
+
r = inject_values_to_record(tag, time, record)
|
174
|
+
@formatter.format(tag, time, r)
|
175
|
+
end
|
176
|
+
|
177
|
+
def write(chunk)
|
178
|
+
index = 0
|
179
|
+
metadata = chunk.metadata
|
180
|
+
time_slice = if metadata.timekey.nil?
|
181
|
+
''.freeze
|
182
|
+
else
|
183
|
+
@time_slice_with_tz.call(metadata.timekey)
|
184
|
+
end
|
185
|
+
|
186
|
+
@values_for_oss_object_chunk[chunk.unique_id] ||= {
|
187
|
+
'%{hex_random}' => hex_random(chunk)
|
188
|
+
}
|
189
|
+
|
190
|
+
if @check_object
|
191
|
+
exist_key = nil
|
192
|
+
begin
|
193
|
+
values_for_oss_key = {
|
194
|
+
'%{path}' => @path,
|
195
|
+
'%{thread_id}' => Thread.current.object_id.to_s,
|
196
|
+
'%{file_extension}' => @compressor.ext,
|
197
|
+
'%{time_slice}' => time_slice,
|
198
|
+
'%{index}' => sprintf(@index_format, index)
|
199
|
+
}.merge!(@values_for_oss_object_chunk[chunk.unique_id])
|
200
|
+
|
201
|
+
values_for_oss_key['%{uuid_flush}'.freeze] = uuid_random if @uuid_flush_enabled
|
202
|
+
|
203
|
+
key = @key_format.gsub(/%{[^}]+}/) do |matched_key|
|
204
|
+
values_for_oss_key.fetch(matched_key, matched_key)
|
205
|
+
end
|
206
|
+
key = extract_placeholders(key, chunk)
|
207
|
+
key = key.gsub(/%{[^}]+}/, values_for_oss_key)
|
208
|
+
|
209
|
+
if (index > 0) && (key == exist_key)
|
210
|
+
if @overwrite
|
211
|
+
log.warn "#{key} already exists, but will overwrite"
|
212
|
+
break
|
213
|
+
else
|
214
|
+
raise "duplicated path is generated. use %{index} in key_format: path = #{key}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
index += 1
|
219
|
+
exist_key = key
|
220
|
+
end while @bucket_handler.object_exists?(key)
|
221
|
+
else
|
222
|
+
hms_slice = Time.now.utc.strftime('%H%M%S')
|
223
|
+
hms_slice = Time.now.strftime('%H%M%S') if @local_time
|
224
|
+
|
225
|
+
values_for_oss_key = {
|
226
|
+
'%{path}' => @path,
|
227
|
+
'%{thread_id}' => Thread.current.object_id.to_s,
|
228
|
+
'%{file_extension}' => @compressor.ext,
|
229
|
+
'%{time_slice}' => time_slice,
|
230
|
+
'%{hms_slice}' => hms_slice
|
231
|
+
}.merge!(@values_for_oss_object_chunk[chunk.unique_id])
|
232
|
+
|
233
|
+
values_for_oss_key['%{uuid_flush}'.freeze] = uuid_random if @uuid_flush_enabled
|
234
|
+
|
235
|
+
key = @key_format.gsub(/%{[^}]+}/) do |matched_key|
|
236
|
+
values_for_oss_key.fetch(matched_key, matched_key)
|
237
|
+
end
|
238
|
+
key = extract_placeholders(key, chunk)
|
239
|
+
key = key.gsub(/%{[^}]+}/, values_for_oss_key)
|
240
|
+
end
|
241
|
+
|
242
|
+
out_file = Tempfile.new('oss-fluent-')
|
243
|
+
out_file.binmode
|
244
|
+
begin
|
245
|
+
@compressor.compress(chunk, out_file)
|
246
|
+
out_file.rewind
|
247
|
+
log.info "out_oss: write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to oss://#{@bucket}/#{key}, size #{out_file.size}"
|
248
|
+
|
249
|
+
start = Time.now.to_i
|
250
|
+
@bucket_handler.put_object(key, file: out_file, content_type: @compressor.content_type)
|
251
|
+
|
252
|
+
log.debug "out_oss: write oss://#{@bucket}/#{key} used #{Time.now.to_i - start} seconds, size #{out_file.length}"
|
253
|
+
@values_for_oss_object_chunk.delete(chunk.unique_id)
|
254
|
+
|
255
|
+
if @warn_for_delay
|
256
|
+
if Time.at(chunk.metadata.timekey) < Time.now - @warn_for_delay
|
257
|
+
log.warn "out_oss: delayed events were put to oss://#{@bucket}/#{key}"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
ensure
|
261
|
+
out_file.close(true) rescue nil
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def create_oss_client
|
266
|
+
@oss = Aliyun::OSS::Client.new(
|
267
|
+
endpoint: @endpoint,
|
268
|
+
access_key_id: @access_key_id,
|
269
|
+
access_key_secret: @access_key_secret,
|
270
|
+
download_crc_enable: @download_crc_enable,
|
271
|
+
upload_crc_enable: @upload_crc_enable,
|
272
|
+
open_timeout: @open_timeout,
|
273
|
+
read_timeout: @read_timeout
|
274
|
+
)
|
275
|
+
end
|
276
|
+
|
277
|
+
def process_key_format
|
278
|
+
if @key_format.include?('%{uuid_flush}')
|
279
|
+
# verify uuidtools
|
280
|
+
begin
|
281
|
+
require 'uuidtools'
|
282
|
+
rescue LoadError
|
283
|
+
raise Fluent::ConfigError, 'uuidtools gem not found.'\
|
284
|
+
' Install uuidtools gem first'
|
285
|
+
end
|
286
|
+
|
287
|
+
begin
|
288
|
+
uuid_random
|
289
|
+
rescue => e
|
290
|
+
raise Fluent::ConfigError, "generating uuid doesn't work. "\
|
291
|
+
"Can't use %{uuid_flush} on this environment. #{e}"
|
292
|
+
end
|
293
|
+
|
294
|
+
@uuid_flush_enabled = true
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def uuid_random
|
299
|
+
::UUIDTools::UUID.random_create.to_s
|
300
|
+
end
|
301
|
+
|
302
|
+
def hex_random(chunk)
|
303
|
+
unique_hex = Fluent::UniqueId.hex(chunk.unique_id)
|
304
|
+
# unique_hex is like (time_sec, time_usec, rand) => reversing gives more randomness
|
305
|
+
unique_hex.reverse!
|
306
|
+
unique_hex[0...@hex_random_length]
|
307
|
+
end
|
308
|
+
|
309
|
+
def ensure_bucket
|
310
|
+
unless @oss.bucket_exist?(@bucket)
|
311
|
+
if @auto_create_bucket
|
312
|
+
log.info "creating bucket #{@bucket} on #{@endpoint}"
|
313
|
+
@oss.create_bucket(@bucket)
|
314
|
+
else
|
315
|
+
raise "the specified bucket does not exist: bucket = #{@bucket}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
@bucket_handler = @oss.get_bucket(@bucket)
|
320
|
+
end
|
321
|
+
|
322
|
+
# Compression base class.
|
323
|
+
class Compressor
|
324
|
+
include Fluent::Configurable
|
325
|
+
|
326
|
+
attr_reader :log
|
327
|
+
|
328
|
+
def initialize(opts = {})
|
329
|
+
super()
|
330
|
+
@buffer_type = opts[:buffer_type]
|
331
|
+
@log = opts[:log]
|
332
|
+
end
|
333
|
+
|
334
|
+
def configure(conf)
|
335
|
+
super
|
336
|
+
end
|
337
|
+
|
338
|
+
def ext; end
|
339
|
+
|
340
|
+
def content_type; end
|
341
|
+
|
342
|
+
def compress(chunk, file); end
|
343
|
+
|
344
|
+
private
|
345
|
+
|
346
|
+
def check_command(command, encode = nil)
|
347
|
+
require 'open3'
|
348
|
+
|
349
|
+
encode = command if encode.nil?
|
350
|
+
begin
|
351
|
+
Open3.capture3("#{command} -V")
|
352
|
+
rescue Errno::ENOENT
|
353
|
+
raise Fluent::ConfigError,
|
354
|
+
"'#{command}' utility must be in PATH for #{encode} compression"
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Gzip compression.
|
360
|
+
class GzipCompressor < Compressor
|
361
|
+
def ext
|
362
|
+
'gz'.freeze
|
363
|
+
end
|
364
|
+
|
365
|
+
def content_type
|
366
|
+
'application/x-gzip'.freeze
|
367
|
+
end
|
368
|
+
|
369
|
+
def compress(chunk, file)
|
370
|
+
out = Zlib::GzipWriter.new(file)
|
371
|
+
chunk.write_to(out)
|
372
|
+
out.finish
|
373
|
+
ensure
|
374
|
+
begin
|
375
|
+
out.finish
|
376
|
+
rescue StandardError
|
377
|
+
nil
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Text output format.
|
383
|
+
class TextCompressor < Compressor
|
384
|
+
def ext
|
385
|
+
'txt'.freeze
|
386
|
+
end
|
387
|
+
|
388
|
+
def content_type
|
389
|
+
'text/plain'.freeze
|
390
|
+
end
|
391
|
+
|
392
|
+
def compress(chunk, file)
|
393
|
+
chunk.write_to(file)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Json compression.
|
398
|
+
class JsonCompressor < TextCompressor
|
399
|
+
def ext
|
400
|
+
'json'.freeze
|
401
|
+
end
|
402
|
+
|
403
|
+
def content_type
|
404
|
+
'application/json'.freeze
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
COMPRESSOR_REGISTRY = Fluent::Registry.new(:oss_compressor_type,
|
409
|
+
'fluent/plugin/oss_compressor_')
|
410
|
+
{
|
411
|
+
'gzip' => GzipCompressor,
|
412
|
+
'json' => JsonCompressor,
|
413
|
+
'text' => TextCompressor
|
414
|
+
}.each do |name, compressor|
|
415
|
+
COMPRESSOR_REGISTRY.register(name, compressor)
|
416
|
+
end
|
417
|
+
|
418
|
+
def self.register_compressor(name, compressor)
|
419
|
+
COMPRESSOR_REGISTRY.register(name, compressor)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|