fluent-plugin-aliyun-oss 0.0.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 +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
|