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 +4 -4
- data/README.md +43 -5
- data/VERSION +1 -1
- data/lib/fluent/plugin/azurestorage_gen2_compressor_gzip_command.rb +51 -0
- data/lib/fluent/plugin/azurestorage_gen2_compressor_lzma2.rb +34 -0
- data/lib/fluent/plugin/azurestorage_gen2_compressor_lzo.rb +34 -0
- data/lib/fluent/plugin/out_azurestorage_gen2.rb +310 -59
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 511b8f199ba0f1c6b7937711b0e3d146baf3a856
|
4
|
+
data.tar.gz: ff6cf330c0ecacd65a48c8f312a87bb8bdb402a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
+
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 => "
|
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
|
-
|
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
|
-
"%{
|
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
|
-
@
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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
|
-
|
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},
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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
|