fluent-plugin-azurestorage-gen2 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f15931cc13f12c4aeb6723ba2a44c604998e819
4
- data.tar.gz: 557964c5e84e0274a840af3e18500d9197ea8cd4
3
+ metadata.gz: 511b8f199ba0f1c6b7937711b0e3d146baf3a856
4
+ data.tar.gz: ff6cf330c0ecacd65a48c8f312a87bb8bdb402a5
5
5
  SHA512:
6
- metadata.gz: 7d0e97ee7d6b1429e98f91aa72135c4f84bf4cfd8b573cc8ec747cb36fe0e65fb98bce5d02d5416db83a4946c406c93f27ede48a1ba989d25bf675771311e6cd
7
- data.tar.gz: 6ed3f231452a94eb0e66244648be66500af2fd82577b3796df11d0cda363238b95b50cc478ae01b54d1e8deb4cdb12101151ccfecb8177f53923db97b395248b
6
+ metadata.gz: 368340200f930c8f4984ab5b630e779f375061d82faab35cf55f1ae83f7b92e58f63f7a4a152880a51d62840a1fe9e4bc56ffacdb3a6c1930c5a617d11378eae
7
+ data.tar.gz: e0a54bb0bfd963134cf7e1afeb80c8ea936e3421002d444d9cf6abbaaafc8d2e1bcedcbb118afbe0a2a44a76c9d9ebfe207495af11cb0c19a2f63621cc2c83ab
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Azure Datalake Storage Gen2 Fluentd Output Plugin (IN PROGRESS)
1
+ # Azure Datalake Storage Gen2 Fluentd Output Plugin
2
2
 
3
3
  [![Build Status](https://travis-ci.org/oleewere/fluent-plugin-azurestorage-gen2.svg?branch=master)](https://travis-ci.org/oleewere/fluent-plugin-azurestorage-gen2)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -9,11 +9,11 @@
9
9
 
10
10
  | fluent-plugin-azurestorage-gen2 | fluentd | ruby |
11
11
  |------------------------|---------|------|
12
- | >= 0.1.1 | >= v0.14.0 | >= 2.4 |
12
+ | >= 0.1.2 | >= v0.14.0 | >= 2.4 |
13
13
 
14
14
  ## Overview
15
15
 
16
- Fluent output plugin that can use ABFS api and append blobs with MSI support
16
+ Fluent output plugin that can use ABFS api and append blobs with MSI and OAuth support.
17
17
 
18
18
  ## Installation
19
19
 
@@ -24,6 +24,7 @@ $ gem install fluent-plugin-azurestorage-gen2
24
24
 
25
25
  ## Configuration
26
26
 
27
+ - Configuration on VMs by using MSI:
27
28
  ```
28
29
  <match **>
29
30
  @type azurestorage_gen2
@@ -47,6 +48,32 @@ $ gem install fluent-plugin-azurestorage-gen2
47
48
  </match>
48
49
  ```
49
50
 
51
+ - Configuration outside of VMs with OAuth credentials:
52
+ ```
53
+ <match **>
54
+ @type azurestorage_gen2
55
+ azure_storage_account mystorageabfs
56
+ azure_container mycontainer
57
+ azure_object_key_format %{path}-%{index}.%{file_extension}
58
+ azure_oauth_tenant_id <my tenant id>
59
+ azure_oauth_app_id <my app client id>
60
+ azure_oauth_secret <my client secret>
61
+ azure_oauth_refresh_interval 3600
62
+ time_slice_format %Y%m%d-%H
63
+ file_extension log
64
+ path "/cluster-logs/myfolder/${tag[1]}-#{Socket.gethostname}-%M"
65
+ auto_create_container true
66
+ <buffer tag,time>
67
+ @type file
68
+ path /var/log/fluent/azurestorage-buffer
69
+ timekey 5m
70
+ timekey_wait 0s
71
+ timekey_use_utc true
72
+ chunk_limit_size 64m
73
+ </buffer>
74
+ </match>
75
+ ```
76
+
50
77
  ### Configuration options
51
78
 
52
79
  ### azure_storage_account
@@ -62,9 +89,21 @@ Your Azure Storage Access Key(Primary or Secondary). This also can be got from A
62
89
 
63
90
  Your Azure Managed Service Identity ID. When storage key authentication is not used, the plugin uses OAuth2 to authenticate as given MSI. This authentication method only works on Azure VM. If the VM has only one MSI assigned, this parameter becomes optional and the only MSI will be used. Otherwise this parameter is required.
64
91
 
92
+ ### azure_oauth_tenant_id
93
+
94
+ Azure account tenant id from your Azure Directory. Required if OAuth based credential mechanism is used.
95
+
96
+ ### azure_oauth_app_id
97
+
98
+ OAuth client id that is used for OAuth based authentication. Required if OAuth based credential mechanism is used.
99
+
100
+ ### azure_oauth_secret
101
+
102
+ OAuth client secret that is used for OAuth based authentication. Required if OAuth based credential mechanism is used.
103
+
65
104
  ### azure_oauth_refresh_interval
66
105
 
67
- OAuth2 access token refreshment interval in second. Only applies when MSI authentication is used.
106
+ OAuth2 access token refreshment interval in second. Only applies when MSI / OAuth authentication is used.
68
107
 
69
108
  ### azure_container (Required)
70
109
 
@@ -205,7 +244,6 @@ Use UTC instead of local time.
205
244
  ## TODOs
206
245
 
207
246
  - add storage key support
208
- - add compression (if append is not used)
209
247
 
210
248
  ## Contributing
211
249
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -0,0 +1,51 @@
1
+ module Fluent
2
+ class AzureStorageGen2Output
3
+ class GzipCommandCompressor < Compressor
4
+ AzureStorageGen2Output.register_compressor('gzip_command', self)
5
+
6
+ config_param :command_parameter, :string, :default => ''
7
+
8
+ def configure(conf)
9
+ super
10
+ check_command('gzip')
11
+ end
12
+
13
+ def ext
14
+ 'gz'.freeze
15
+ end
16
+
17
+ def content_type
18
+ 'application/x-gzip'.freeze
19
+ end
20
+
21
+ def compress(chunk, tmp)
22
+ chunk_is_file = @buffer_type == 'file'
23
+ path = if chunk_is_file
24
+ chunk.path
25
+ else
26
+ w = Tempfile.new("chunk-gzip-tmp")
27
+ chunk.write_to(w)
28
+ w.close
29
+ w.path
30
+ end
31
+
32
+ res = system "gzip #{@command_parameter} -c #{path} > #{tmp.path}"
33
+ unless res
34
+ log.warn "failed to execute gzip command. Fallback to GzipWriter. status = #{$?}"
35
+ begin
36
+ tmp.truncate(0)
37
+ gw = Zlib::GzipWriter.new(tmp)
38
+ chunk.write_to(gw)
39
+ gw.close
40
+ ensure
41
+ gw.close rescue nil
42
+ end
43
+ end
44
+ ensure
45
+ unless chunk_is_file
46
+ w.close(true) rescue nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ module Fluent
2
+ class AzureStorageGen2Output
3
+ class LZMA2Compressor < Compressor
4
+ AzureStorageGen2Output.register_compressor('lzma2', self)
5
+
6
+ config_param :command_parameter, :string, :default => '-qf0'
7
+
8
+ def configure(conf)
9
+ super
10
+ check_command('xz', 'LZMA2')
11
+ end
12
+
13
+ def ext
14
+ 'xz'.freeze
15
+ end
16
+
17
+ def content_type
18
+ 'application/x-xz'.freeze
19
+ end
20
+
21
+ def compress(chunk, tmp)
22
+ w = Tempfile.new("chunk-xz-tmp")
23
+ chunk.write_to(w)
24
+ w.close
25
+
26
+ # We don't check the return code because we can't recover lzop failure.
27
+ system "xz #{@command_parameter} -c #{w.path} > #{tmp.path}"
28
+ ensure
29
+ w.close rescue nil
30
+ w.unlink rescue nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module Fluent
2
+ class AzureStorageGen2Output
3
+ class LZOCompressor < Compressor
4
+ AzureStorageGen2Output.register_compressor('lzo', self)
5
+
6
+ config_param :command_parameter, :string, :default => '-qf1'
7
+
8
+ def configure(conf)
9
+ super
10
+ check_command('lzop', 'LZO')
11
+ end
12
+
13
+ def ext
14
+ 'lzo'.freeze
15
+ end
16
+
17
+ def content_type
18
+ 'application/x-lzop'.freeze
19
+ end
20
+
21
+ def compress(chunk, tmp)
22
+ w = Tempfile.new("chunk-tmp")
23
+ chunk.write_to(w)
24
+ w.close
25
+
26
+ # We don't check the return code because we can't recover lzop failure.
27
+ system "lzop #{@command_parameter} -o #{tmp.path} #{w.path}"
28
+ ensure
29
+ w.close rescue nil
30
+ w.unlink rescue nil
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,6 @@
1
1
  require 'net/http'
2
+ require 'base64'
3
+ require 'openssl'
2
4
  require 'json'
3
5
  require 'tempfile'
4
6
  require 'time'
@@ -21,14 +23,19 @@ module Fluent::Plugin
21
23
  config_param :azure_storage_account, :string, :default => nil
22
24
  config_param :azure_storage_access_key, :string, :default => nil, :secret => true
23
25
  config_param :azure_instance_msi, :string, :default => nil
26
+ config_param :azure_oauth_app_id, :string, :default => nil, :secret => true
27
+ config_param :azure_oauth_secret, :string, :default => nil, :secret => true
28
+ config_param :azure_oauth_tenant_id, :string, :default => nil
24
29
  config_param :azure_oauth_refresh_interval, :integer, :default => 60 * 60 # one hour
25
30
  config_param :azure_container, :string, :default => nil
26
31
  config_param :azure_object_key_format, :string, :default => "%{path}%{time_slice}_%{index}.%{file_extension}"
27
32
  config_param :file_extension, :string, :default => "log"
28
- config_param :store_as, :string, :default => "gzip"
33
+ config_param :store_as, :string, :default => "none"
29
34
  config_param :auto_create_container, :bool, :default => false
30
35
  config_param :format, :string, :default => "out_file"
31
36
  config_param :time_slice_format, :string, :default => '%Y%m%d'
37
+ config_param :command_parameter, :string, :default => nil
38
+ config_param :message_field, :string, :default => nil
32
39
 
33
40
  DEFAULT_FORMAT_TYPE = "out_file"
34
41
  URL_DOMAIN_SUFFIX = '.dfs.core.windows.net'
@@ -49,6 +56,14 @@ module Fluent::Plugin
49
56
  compat_parameters_convert(conf, :buffer, :formatter, :inject)
50
57
  super
51
58
 
59
+ begin
60
+ @compressor = COMPRESSOR_REGISTRY.lookup(@store_as).new(:buffer_type => @buffer_type, :log => log)
61
+ rescue => e
62
+ log.warn "#{@store_as} not found. Use 'text' instead"
63
+ @compressor = TextCompressor.new
64
+ end
65
+ @compressor.configure(conf)
66
+
52
67
  @formatter = formatter_create
53
68
 
54
69
  if @localtime
@@ -68,6 +83,14 @@ module Fluent::Plugin
68
83
  @azure_storage_path = ''
69
84
  @last_azure_storage_path = ''
70
85
  @current_index = 0
86
+ if @store_as.nil? || @store_as == "none"
87
+ @blob_content_type = "text/plain"
88
+ @final_file_extension = @file_extension
89
+ else
90
+ @blob_content_type = @compressor.content_type
91
+ @final_file_extension = @compressor.ext
92
+ end
93
+
71
94
  end
72
95
 
73
96
  def multi_workers_ready?
@@ -82,39 +105,44 @@ module Fluent::Plugin
82
105
 
83
106
  def write(chunk)
84
107
  metadata = chunk.metadata
85
-
86
- #tmp = Tempfile.new("azure-")
87
- #begin
88
- # tmp.close
89
- # generate_log_name(metadata, @current_index)
90
- # if @last_azure_storage_path != @azure_storage_path
91
- # @current_index = 0
92
- # generate_log_name(metadata, @current_index)
93
- # end
94
- # content = File.open(tmp.path, 'rb') { |file| file.read }
95
- # raw_data = raw_data.chomp
96
- # log.debug "Content: #{content}"
97
- # upload_blob(content, metadata)
98
- # @last_azure_storage_path = @azure_storage_path
99
- #ensure
100
- # tmp.close(true) rescue nil
101
- # tmp.unlink
102
- #end
103
- raw_data=''
104
- generate_log_name(metadata, @current_index)
105
- if @last_azure_storage_path != @azure_storage_path
106
- @current_index = 0
108
+ if @store_as.nil? || @store_as == "none"
109
+ raw_data=''
107
110
  generate_log_name(metadata, @current_index)
111
+ if @last_azure_storage_path != @azure_storage_path
112
+ @current_index = 0
113
+ generate_log_name(metadata, @current_index)
114
+ end
115
+ chunk.each do |emit_time, record|
116
+ if @message_field.nil? || @message_field.empty?
117
+ raw_data << "#{Yajl.dump(record)}\n"
118
+ elsif record.key?(@message_field)
119
+ line = record[@message_field].chomp
120
+ raw_data << "#{line}\n"
121
+ end
122
+ end
123
+ raw_data = raw_data.chomp
124
+ unless raw_data.empty?
125
+ upload_blob(raw_data, metadata)
126
+ end
127
+ @last_azure_storage_path = @azure_storage_path
128
+ else
129
+ tmp = Tempfile.new("azure-")
130
+ begin
131
+ @compressor.compress(chunk, tmp)
132
+ tmp.close
133
+ generate_log_name(metadata, @current_index)
134
+ if @last_azure_storage_path != @azure_storage_path
135
+ @current_index = 0
136
+ generate_log_name(metadata, @current_index)
137
+ end
138
+ log.debug "Start uploading temp file: #{tmp.path}"
139
+ content = File.open(tmp.path, 'rb') { |file| file.read }
140
+ upload_blob(content, metadata)
141
+ @last_azure_storage_path = @azure_storage_path
142
+ ensure
143
+ tmp.unlink
144
+ end
108
145
  end
109
- chunk.each do |emit_time, record|
110
- line = record["message"].chomp
111
- raw_data << "#{line}\n"
112
- end
113
- raw_data = raw_data.chomp
114
- unless raw_data.empty?
115
- upload_blob(raw_data, metadata)
116
- end
117
- @last_azure_storage_path = @azure_storage_path
118
146
 
119
147
  end
120
148
 
@@ -145,7 +173,8 @@ module Fluent::Plugin
145
173
  "%{path}" => path,
146
174
  "%{time_slice}" => time_slice,
147
175
  "%{index}" => index,
148
- "%{file_extension}" => @file_extension
176
+ "%{uuid_flush}" => uuid_random,
177
+ "%{file_extension}" => @final_file_extension
149
178
  }
150
179
  storage_path = @azure_object_key_format.gsub(%r(%{[^}]+}), values_for_object_key)
151
180
  extracted_path = extract_placeholders(storage_path, metadata)
@@ -154,25 +183,40 @@ module Fluent::Plugin
154
183
  end
155
184
 
156
185
  def setup_access_token
157
- @get_token_lock = Concurrent::ReadWriteLock.new
158
- acquire_access_token
159
- if @azure_oauth_refresh_interval > 0
160
- log.info("azurestorage_gen2: Start getting access token every #{@azure_oauth_refresh_interval} seconds.")
161
- @get_token_task = Concurrent::TimerTask.new(
162
- execution_interval: @azure_oauth_refresh_interval) {
163
- begin
164
- acquire_access_token
165
- rescue Exception => e
166
- log.warn("#{e.message}, continue with previous credentials.")
167
- end
168
- }
169
- @get_token_task.execute
186
+ if @azure_storage_access_key.nil?
187
+ @get_token_lock = Concurrent::ReadWriteLock.new
188
+ acquire_access_token
189
+ if @azure_oauth_refresh_interval > 0
190
+ log.info("azurestorage_gen2: Start getting access token every #{@azure_oauth_refresh_interval} seconds.")
191
+ @get_token_task = Concurrent::TimerTask.new(
192
+ execution_interval: @azure_oauth_refresh_interval) {
193
+ begin
194
+ acquire_access_token
195
+ rescue Exception => e
196
+ log.warn("#{e.message}, continue with previous credentials.")
197
+ end
198
+ }
199
+ @get_token_task.execute
200
+ end
201
+ else
202
+ log.info "azurestorage_gen2: Access storage key is configured, MSI support is disabled."
203
+ end
204
+ end
205
+
206
+ def acquire_access_token
207
+ if !@azure_instance_msi.nil?
208
+ acquire_access_token_msi
209
+ elsif !@azure_oauth_app_id.nil? and !@azure_oauth_secret.nil? and !@azure_oauth_tenant_id.nil?
210
+ acquire_access_token_oauth_app
211
+ else
212
+ raise Fluent::UnrecoverableError, "Using MSI or simple OAuth 2.0 based authentication parameters (azure_oauth_tenant_id, azure_oauth_app_id, azure_oauth_secret) are required."
170
213
  end
171
214
  end
172
215
 
173
216
  # Referenced from azure doc.
174
217
  # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/tutorial-linux-vm-access-storage#get-an-access-token-and-use-it-to-call-azure-storage
175
- def acquire_access_token
218
+ private
219
+ def acquire_access_token_msi
176
220
  params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "https://storage.azure.com/" }
177
221
  unless @azure_instance_msi.nil?
178
222
  params[:msi_res_id] = @azure_instance_msi
@@ -190,10 +234,30 @@ module Fluent::Plugin
190
234
  request.run
191
235
  end
192
236
 
237
+ private
238
+ def acquire_access_token_oauth_app
239
+ params = { :"api-version" => ACCESS_TOKEN_API_VERSION, :resource => "https://storage.azure.com/"}
240
+ content = "grant_type=client_credentials&client_id=#{@azure_oauth_app_id}&client_secret=#{@azure_oauth_secret}&resource=https://storage.azure.com/"
241
+ request = Typhoeus::Request.new("https://login.microsoftonline.com/#{@azure_oauth_tenant_id}/oauth2/token", params: params, :body => content)
242
+ request.on_complete do |response|
243
+ if response.success?
244
+ data = JSON.parse(response.body)
245
+ log.debug "azurestorage_gen2: Token response: #{data}"
246
+ @azure_access_token = data["access_token"]
247
+ else
248
+ raise Fluent::UnrecoverableError, "Failed to acquire access token. #{response.code}: #{response.body}"
249
+ end
250
+ end
251
+ request.run
252
+ end
253
+
193
254
  private
194
255
  def ensure_container
195
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"Authorization" => "Bearer #{@azure_access_token}",:"Content-Length" => "0"}
256
+ datestamp = create_request_date
257
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp,:"Content-Length" => "0"}
196
258
  params = {:resource => "filesystem" }
259
+ auth_header = create_auth_header("head", datestamp, "#{@azure_container}", headers, params)
260
+ headers[:Authorization] = auth_header
197
261
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}", :method => :head, :params => params, :headers=> headers)
198
262
  request.on_complete do |response|
199
263
  if response.success?
@@ -208,7 +272,7 @@ module Fluent::Plugin
208
272
  raise Fluent::ConfigError, "The specified container does not exist: container = #{@azure_container}"
209
273
  end
210
274
  else
211
- raise Fluent::UnrecoverableError, "Get container request failed - code: #{response.code}, body: #{response.body}"
275
+ raise Fluent::UnrecoverableError, "Get container request failed - code: #{response.code}, headers: #{response.headers}"
212
276
  end
213
277
  end
214
278
  request.run
@@ -216,8 +280,11 @@ module Fluent::Plugin
216
280
 
217
281
  private
218
282
  def create_container
219
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"Authorization" => "Bearer #{@azure_access_token}",:"Content-Length" => "0"}
283
+ datestamp = create_request_date
284
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp, :"Content-Length" => "0"}
220
285
  params = {:resource => "filesystem" }
286
+ auth_header = create_auth_header("put", datestamp, "#{@azure_container}", headers, params)
287
+ headers[:Authorization] = auth_header
221
288
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}", :method => :put, :params => params, :headers=> headers)
222
289
  request.on_complete do |response|
223
290
  if response.success?
@@ -225,7 +292,7 @@ module Fluent::Plugin
225
292
  elsif response.timed_out?
226
293
  raise Fluent::UnrecoverableError, "Creating container '#{@azure_container}' request timed out."
227
294
  else
228
- raise Fluent::UnrecoverableError, "Creating container request failed - code: #{response.code}, body: #{response.body}"
295
+ raise Fluent::UnrecoverableError, "Creating container request failed - code: #{response.code}, body: #{response.body}, headers: #{response.headers}"
229
296
  end
230
297
  end
231
298
  request.run
@@ -233,8 +300,11 @@ module Fluent::Plugin
233
300
 
234
301
  private
235
302
  def create_blob(blob_path)
236
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"Authorization" => "Bearer #{@azure_access_token}",:"Content-Length" => "0", :"Content-Type" => "application/json"}
303
+ datestamp = create_request_date
304
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp,:"Content-Length" => "0", :"Content-Type" => "application/json"}
237
305
  params = {:resource => "file", :recursive => "false"}
306
+ auth_header = create_auth_header("put", datestamp, "#{@azure_container}#{blob_path}", headers, params)
307
+ headers[:Authorization] = auth_header
238
308
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}#{blob_path}", :method => :put, :params => params, :headers=> headers)
239
309
  request.on_complete do |response|
240
310
  if response.success?
@@ -244,7 +314,7 @@ module Fluent::Plugin
244
314
  elsif response.code == 409
245
315
  log.debug "azurestorage_gen2: Blob already exists: #{blob_path}"
246
316
  else
247
- raise Fluent::UnrecoverableError, "Creating blob '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}"
317
+ raise Fluent::UnrecoverableError, "Creating blob '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}, headers: #{response.headers}"
248
318
  end
249
319
  end
250
320
  request.run
@@ -253,8 +323,11 @@ module Fluent::Plugin
253
323
  private
254
324
  def append_blob_block(blob_path, content, position)
255
325
  log.debug "azurestorage_gen2: append_blob_block.start: Append blob ('#{blob_path}') called with position #{position}"
256
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-content-type" => "text/plain", :"Authorization" => "Bearer #{@azure_access_token}"}
326
+ datestamp = create_request_date
327
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp, :"x-ms-content-type" => "#{@blob_content_type}"}
257
328
  params = {:action => "append", :position => "#{position}"}
329
+ auth_header = create_auth_header("patch", datestamp, "#{@azure_container}#{blob_path}", headers, params)
330
+ headers[:Authorization] = auth_header
258
331
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}#{blob_path}", :method => :patch, :body => content, :params => params, :headers=> headers)
259
332
  request.on_complete do |response|
260
333
  if response.success?
@@ -266,7 +339,7 @@ module Fluent::Plugin
266
339
  elsif response.code == 409
267
340
  raise AppendBlobResponseError.new("Blob '#{blob_path}' has conflict. Error code: #{response.code}", 409)
268
341
  else
269
- raise Fluent::UnrecoverableError, "Appending blob '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}"
342
+ raise Fluent::UnrecoverableError, "Appending blob '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}, headers: #{response.headers}"
270
343
  end
271
344
  end
272
345
  request.run
@@ -275,8 +348,11 @@ module Fluent::Plugin
275
348
  private
276
349
  def flush(blob_path, position)
277
350
  log.debug "azurestorage_gen2: flush_blob.start: Flush blob ('#{blob_path}') called with position #{position}"
278
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"Authorization" => "Bearer #{@azure_access_token}",:"Content-Length" => "0"}
351
+ datestamp = create_request_date
352
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp,:"Content-Length" => "0"}
279
353
  params = {:action => "flush", :position => "#{position}"}
354
+ auth_header = create_auth_header("patch", datestamp, "#{@azure_container}#{blob_path}",headers, params)
355
+ headers[:Authorization] = auth_header
280
356
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}#{blob_path}", :method => :patch, :params => params, :headers=> headers)
281
357
  request.on_complete do |response|
282
358
  if response.success?
@@ -284,7 +360,7 @@ module Fluent::Plugin
284
360
  elsif response.timed_out?
285
361
  raise Fluent::UnrecoverableError, "Bloub '#{blob_path}' flush request timed out."
286
362
  else
287
- raise Fluent::UnrecoverableError, "Blob flush request failed - code: #{response.code}, body: #{response.body}"
363
+ raise Fluent::UnrecoverableError, "Blob flush request failed - code: #{response.code}, body: #{response.body}, headers: #{response.headers}"
288
364
  end
289
365
  end
290
366
  request.run
@@ -292,9 +368,12 @@ module Fluent::Plugin
292
368
 
293
369
  private
294
370
  def get_blob_properties(blob_path)
295
- headers = {:"x-ms-version" => ABFS_API_VERSION, :"Authorization" => "Bearer #{@azure_access_token}",:"Content-Length" => "0"}
371
+ datestamp = create_request_date
372
+ headers = {:"x-ms-version" => ABFS_API_VERSION, :"x-ms-date" => datestamp, :"Content-Length" => "0"}
296
373
  params = {}
297
374
  content_length = -1
375
+ auth_header = create_auth_header("head", datestamp, "#{@azure_container}#{blob_path}", headers, params)
376
+ headers[:Authorization] = auth_header
298
377
  request = Typhoeus::Request.new("https://#{azure_storage_account}#{URL_DOMAIN_SUFFIX}/#{@azure_container}#{blob_path}", :method => :head, :params => params, :headers=> headers)
299
378
  request.on_complete do |response|
300
379
  if response.success?
@@ -306,7 +385,7 @@ module Fluent::Plugin
306
385
  log.debug "azurestorage_gen2: Blob '#{blob_path}' does not exist. Creating it if needed..."
307
386
  content_length = 0
308
387
  else
309
- raise Fluent::UnrecoverableError, "Get blob properties '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}"
388
+ raise Fluent::UnrecoverableError, "Get blob properties '#{blob_path}' request failed - code: #{response.code}, body: #{response.body}, headers: #{response.headers}"
310
389
  end
311
390
  end
312
391
  request.run
@@ -354,6 +433,178 @@ module Fluent::Plugin
354
433
  flush(@azure_storage_path, existing_content_length)
355
434
  log.debug "azurestorage_gen2: append_blob.complete"
356
435
  end
436
+
437
+ private
438
+ def create_auth_header(method, datestamp, resource, headers, params)
439
+ if @azure_storage_access_key.nil?
440
+ "Bearer #{@azure_access_token}"
441
+ else
442
+ "SharedKey #{@azure_storage_account}:#{signed(method, datestamp, resource, headers, params)}"
443
+ end
444
+ end
445
+
446
+ private
447
+ def signed(method, datestamp, resource, headers, params)
448
+ decoded_access_key=Base64.strict_decode64(@azure_storage_access_key).unpack("H*").first
449
+ sign_request(decoded_access_key, signable_string(method, resource, params, headers, datestamp))
450
+ end
451
+
452
+ private
453
+ def sign_request(key, signable_string)
454
+ signed = OpenSSL::HMAC.digest('sha256', key, signable_string)
455
+ Base64.strict_encode64(signed)
456
+ end
457
+
458
+ private
459
+ def signable_string(method, resource, params, headers, datestamp)
460
+ [
461
+ method.to_s.upcase,
462
+ headers.fetch("Content-Encoding", ""),
463
+ headers.fetch("Content-Language", ""),
464
+ headers.fetch("Content-Length", "").sub(/^0+/, ""),
465
+ headers.fetch("Content-MD5", ""),
466
+ headers.fetch("Content-Type", ""),
467
+ headers.fetch("Date", ""),
468
+ headers.fetch("If-Modified-Since", ""),
469
+ headers.fetch("If-Match", ""),
470
+ headers.fetch("If-None-Match", ""),
471
+ headers.fetch("If-Unmodified-Since", ""),
472
+ headers.fetch("Range", ""),
473
+ "x-ms-date:#{datestamp}\nx-ms-version:#{ABFS_API_VERSION}",
474
+ get_canonicalized_resource(resource, params)
475
+ ].join("\n")
476
+ end
477
+
478
+ private
479
+ def get_canonicalized_resource(resource, params)
480
+ if params.empty?
481
+ canonicalized_resource="/#{@azure_storage_account}"
482
+ else
483
+ canonicalized_params = params
484
+ .map{|paramKey, paramValue| "#{paramKey.to_s.downcase}:#{paramValue}"}
485
+ .join("\n")
486
+ canonicalized_resource="/#{@azure_storage_account}/#{resource}\n#{canonicalized_params}"
487
+ end
488
+ end
489
+
490
+ private
491
+ def hex_to_bin(hex)
492
+ hex = '0' << hex unless (hex.length % 2) == 0
493
+ hex.scan(/[A-Fa-f0-9]{2}/).inject('') { |encoded, byte| encoded << [byte].pack('H*') }
494
+ end
495
+
496
+ private
497
+ def create_request_date
498
+ Time.now.strftime('%a, %e %b %y %H:%M:%S %Z')
499
+ end
500
+
501
+ def uuid_random
502
+ require 'uuidtools'
503
+ ::UUIDTools::UUID.random_create.to_s
504
+ end
505
+
506
+ def timekey_to_timeformat(timekey)
507
+ case timekey
508
+ when nil then ''
509
+ when 0...60 then '%Y%m%d%H%M%S' # 60 exclusive
510
+ when 60...3600 then '%Y%m%d%H%M'
511
+ when 3600...86400 then '%Y%m%d%H'
512
+ else '%Y%m%d'
513
+ end
514
+ end
515
+
516
+ class Compressor
517
+ include Fluent::Configurable
518
+
519
+ def initialize(opts = {})
520
+ super()
521
+ @buffer_type = opts[:buffer_type]
522
+ @log = opts[:log]
523
+ end
524
+
525
+ attr_reader :buffer_type, :log
526
+
527
+ def configure(conf)
528
+ super
529
+ end
530
+
531
+ def ext
532
+ end
533
+
534
+ def content_type
535
+ end
536
+
537
+ def compress(chunk, tmp)
538
+ end
539
+
540
+ private
541
+ def check_command(command, algo = nil)
542
+ require 'open3'
543
+
544
+ algo = command if algo.nil?
545
+ begin
546
+ Open3.capture3("#{command} -V")
547
+ rescue Errno::ENOENT
548
+ raise Fluent::ConfigError, "'#{command}' utility must be in PATH for #{algo} compression"
549
+ end
550
+ end
551
+ end
552
+
553
+ class GzipCompressor < Compressor
554
+ def ext
555
+ 'gz'.freeze
556
+ end
557
+
558
+ def content_type
559
+ 'application/x-gzip'.freeze
560
+ end
561
+
562
+ def compress(chunk, tmp)
563
+ w = Zlib::GzipWriter.new(tmp)
564
+ chunk.write_to(w)
565
+ w.finish
566
+ ensure
567
+ w.finish rescue nil
568
+ end
569
+ end
570
+
571
+ class TextCompressor < Compressor
572
+ def ext
573
+ 'txt'.freeze
574
+ end
575
+
576
+ def content_type
577
+ 'text/plain'.freeze
578
+ end
579
+
580
+ def compress(chunk, tmp)
581
+ chunk.write_to(tmp)
582
+ end
583
+ end
584
+
585
+ class JsonCompressor < TextCompressor
586
+ def ext
587
+ 'json'.freeze
588
+ end
589
+
590
+ def content_type
591
+ 'application/json'.freeze
592
+ end
593
+ end
594
+
595
+ COMPRESSOR_REGISTRY = Fluent::Registry.new(:azurestorage_compressor_type, 'fluent/plugin/azurestorage_gen2_compressor_')
596
+ {
597
+ 'gzip' => GzipCompressor,
598
+ 'json' => JsonCompressor,
599
+ 'text' => TextCompressor
600
+ }.each { |name, compressor|
601
+ COMPRESSOR_REGISTRY.register(name, compressor)
602
+ }
603
+
604
+ def self.register_compressor(name, compressor)
605
+ COMPRESSOR_REGISTRY.register(name, compressor)
606
+ end
607
+
357
608
  end
358
609
 
359
610
  class AppendBlobResponseError < StandardError
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-azurestorage-gen2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Szabo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-12-16 00:00:00.000000000 Z
11
+ date: 2019-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd
@@ -180,6 +180,9 @@ files:
180
180
  - Rakefile
181
181
  - VERSION
182
182
  - fluent-plugin-azurestorage-gen2.gemspec
183
+ - lib/fluent/plugin/azurestorage_gen2_compressor_gzip_command.rb
184
+ - lib/fluent/plugin/azurestorage_gen2_compressor_lzma2.rb
185
+ - lib/fluent/plugin/azurestorage_gen2_compressor_lzo.rb
183
186
  - lib/fluent/plugin/out_azurestorage_gen2.rb
184
187
  - test/helper.rb
185
188
  - test/plugin/test_out_azurestorage_gen2.rb