fluent-plugin-aliyun-oss 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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