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.
@@ -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