custom_fluent-plugin-azure-storage-append-blob 0.2.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b8125e41d1256f174b0b1935bf97095f5dadb263dfa54720ee215d892abf559
4
+ data.tar.gz: e2116c5d4a4268bee98930c667fb307bbc3185586e12cd0ddcf7595291ee8ad3
5
+ SHA512:
6
+ metadata.gz: 39f6149b9874d1d152837051e9bd5f97bf6fcd17112a178cbfb0387491512b194a150f9aa2d8249edc2356f08dc0ef23cf879f5da35d20712abe12e20a54d24c
7
+ data.tar.gz: 4c831fe935735326795ab15c9d3b45168af132746194271bb09b0e3b7ccefc93799b9dfc71100b3cd3454f7585b7accc092ba8deac1a38c706a38d823e099354
@@ -0,0 +1,31 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ otp:
7
+ description: 'One Time Password'
8
+ required: true
9
+
10
+ jobs:
11
+ build:
12
+ name: Build + Publish
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - name: Set up Ruby 2.6
18
+ uses: actions/setup-ruby@v1
19
+ with:
20
+ ruby-version: 2.6.x
21
+
22
+ - name: Publish to RubyGems
23
+ run: |
24
+ mkdir -p $HOME/.gem
25
+ touch $HOME/.gem/credentials
26
+ chmod 0600 $HOME/.gem/credentials
27
+ printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
28
+ gem build *.gemspec
29
+ gem push *.gem --otp ${{ github.event.inputs.otp }}
30
+ env:
31
+ RUBYGEMS_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
@@ -0,0 +1,23 @@
1
+ name: Unittests
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: 2.6
20
+ - name: Install dependencies
21
+ run: bundle install
22
+ - name: Run tests
23
+ run: bundle exec rake test
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
@@ -0,0 +1,2 @@
1
+ Style/FrozenStringLiteralComment:
2
+ Enabled: false
@@ -0,0 +1,36 @@
1
+ FROM ruby:latest
2
+
3
+ WORKDIR /plugin
4
+
5
+ ADD . /plugin
6
+
7
+ RUN gem install bundler && \
8
+ gem install fluentd --no-doc && \
9
+ fluent-gem build fluent-plugin-azure-storage-append-blob.gemspec && \
10
+ fluent-gem install fluent-plugin-azure-storage-append-blob-*.gem
11
+
12
+ RUN echo "<source>\n\
13
+ @type sample\n\
14
+ sample {\"hello\":\"world\"}\n\
15
+ tag pattern\n\
16
+ </source>\n\
17
+ <match pattern>\n\
18
+ @type azure-storage-append-blob\n\
19
+ azure_storage_account \"#{ENV['STORAGE_ACCOUNT']}\"\n\
20
+ azure_storage_access_key \"#{ENV['STORAGE_ACCESS_KEY']}\"\n\
21
+ azure_storage_sas_token \"#{ENV['STORAGE_SAS_TOKEN']}\"\n\
22
+ azure_container fluentd\n\
23
+ auto_create_container true\n\
24
+ path logs/\n\
25
+ azure_object_key_format %{path}%{time_slice}_%{index}.log\n\
26
+ time_slice_format %Y%m%d-%H\n\
27
+ <buffer tag,time>\n\
28
+ @type file\n\
29
+ path /var/log/fluent/azurestorageappendblob\n\
30
+ timekey 120 # 2 minutes\n\
31
+ timekey_wait 60\n\
32
+ timekey_use_utc true # use utc\n\
33
+ </buffer>\n\
34
+ </match>" > /plugin/fluent.conf
35
+
36
+ ENTRYPOINT ["fluentd", "-c", "fluent.conf"]
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation. All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,170 @@
1
+ # fluent-plugin-azure-storage-append-blob
2
+
3
+ [Fluentd](https://fluentd.org/) out plugin to do something.
4
+
5
+ Azure Storage Append Blob output plugin buffers logs in local file and uploads them to Azure Storage Append Blob periodically.
6
+
7
+ ## Installation
8
+
9
+ ### RubyGems
10
+
11
+ gem install fluent-plugin-azure-storage-append-blob
12
+
13
+ ### Bundler
14
+
15
+ Add following line to your Gemfile:
16
+
17
+ gem "fluent-plugin-azure-storage-append-blob"
18
+
19
+ And then execute:
20
+
21
+ bundle
22
+
23
+ ## Configuration
24
+
25
+ <match pattern>
26
+ type azure-storage-append-blob
27
+
28
+ azure_storage_account <your azure storage account>
29
+ azure_storage_access_key <your azure storage access key> # leave empty to use MSI
30
+ azure_storage_sas_token <your azure storage sas token> # leave empty to use MSI
31
+ azure_imds_api_version <Azure Instance Metadata Service API Version> # only used for MSI
32
+ azure_token_refresh_interval <refresh interval in min> # only used for MSI
33
+ azure_container <your azure storage container>
34
+ auto_create_container true
35
+ path logs/
36
+ azure_object_key_format %{path}%{time_slice}_%{index}.log
37
+ time_slice_format %Y%m%d-%H
38
+ # if you want to use %{tag} or %Y/%m/%d/ like syntax in path / azure_blob_name_format,
39
+ # need to specify tag for %{tag} and time for %Y/%m/%d in <buffer> argument.
40
+ <buffer tag,time>
41
+ @type file
42
+ path /var/log/fluent/azurestorageappendblob
43
+ timekey 120 # 2 minutes
44
+ timekey_wait 60
45
+ timekey_use_utc true # use utc
46
+ </buffer>
47
+ </match>
48
+
49
+ ### `azure_storage_account` (Required)
50
+
51
+ Your Azure Storage Account Name. This can be retrieved from Azure Management portal.
52
+
53
+ ### `azure_storage_access_key` or `azure_storage_sas_token` (Either required or both empty to use MSI)
54
+
55
+ Your Azure Storage Access Key (Primary or Secondary) or shared access signature (SAS) token.
56
+ This also can be retrieved from Azure Management portal.
57
+
58
+ If both are empty, the plugin will use the local Managed Identity endpoint to obtain a token for the target storage account.
59
+
60
+ ### `azure_imds_api_version` (Optional, only for MSI)
61
+
62
+ Default: 2019-08-15
63
+
64
+ The Instance Metadata Service is used during the OAuth flow to obtain an access token. This API is versioned and specifying the version is mandatory.
65
+
66
+ See [here](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/instance-metadata-service#versioning) for more details.
67
+
68
+ ### `azure_token_refresh_interval` (Optional, only for MSI)
69
+
70
+ Default: 60 (1 hour)
71
+
72
+ When using MSI, the initial access token needs to be refreshed periodically.
73
+
74
+ ### `azure_container` (Required)
75
+
76
+ Azure Storage Container name
77
+
78
+ ### `auto_create_container`
79
+
80
+ This plugin creates the Azure container if it does not already exist exist when you set 'auto_create_container' to true.
81
+ The default value is `true`
82
+
83
+ ### `azure_object_key_format`
84
+
85
+ The format of Azure Storage object keys. You can use several built-in variables:
86
+
87
+ - %{path}
88
+ - %{time_slice}
89
+ - %{index}
90
+
91
+ to decide keys dynamically.
92
+
93
+ %{path} is exactly the value of *path* configured in the configuration file. E.g., "logs/" in the example configuration above.
94
+ %{time_slice} is the time-slice in text that are formatted with *time_slice_format*.
95
+ %{index} is used only if your blob exceed Azure 50000 blocks limit per blob to prevent data loss. Its not required to use this parameter.
96
+
97
+ The default format is "%{path}%{time_slice}-%{index}.log".
98
+
99
+ For instance, using the example configuration above, actual object keys on Azure Storage will be something like:
100
+
101
+ "logs/20130111-22-0.log"
102
+ "logs/20130111-23-0.log"
103
+ "logs/20130112-00-0.log"
104
+
105
+ With the configuration:
106
+
107
+ azure_object_key_format %{path}/events/ts=%{time_slice}/events.log
108
+ path log
109
+ time_slice_format %Y%m%d-%H
110
+
111
+ You get:
112
+
113
+ "log/events/ts=20130111-22/events.log"
114
+ "log/events/ts=20130111-23/events.log"
115
+ "log/events/ts=20130112-00/events.log"
116
+
117
+ The [fluent-mixin-config-placeholders](https://github.com/tagomoris/fluent-mixin-config-placeholders) mixin is also incorporated, so additional variables such as %{hostname}, etc. can be used in the `azure_object_key_format`. This is useful in preventing filename conflicts when writing from multiple servers.
118
+
119
+ azure_object_key_format %{path}/events/ts=%{time_slice}/events-%{hostname}.log
120
+
121
+ ### `time_slice_format`
122
+
123
+ Format of the time used in the file name. Default is '%Y%m%d'. Use '%Y%m%d%H' to split files hourly.
124
+
125
+ ### Run tests
126
+
127
+ gem install bundler
128
+ bundle install
129
+ bundle exec rake test
130
+
131
+
132
+ ### Test Fluentd
133
+
134
+ 1. Create Storage Account and VM with enabled MSI
135
+ 2. Setup Docker ang Git
136
+ 3. SSH into VM
137
+ 4. Download this repo
138
+ ```
139
+ git clone https://github.com/microsoft/fluent-plugin-azure-storage-append-blob.git
140
+ cd fluent-plugin-azure-storage-append-blob
141
+ ```
142
+ 5. Build Docker image
143
+ `docker build -t fluent .`
144
+ 6. Run Docker image with different set of parameters:
145
+
146
+ 1. `STORAGE_ACCOUNT`: required, name of your storage account
147
+ 2. `STORAGE_ACCESS_KEY`: storage account access key
148
+ 3. `STORAGE_SAS_TOKEN`: storage sas token with enough permissions for the plugin
149
+
150
+ You need to specify `STORAGE_ACCOUNT` and one of auth ways. If you run it from VM with MSI,
151
+ just `STORAGE_ACCOUNT` is required. Keep in mind, there is no way to refresh MSI Token, so
152
+ ensure you setup proper permissions first.
153
+
154
+ ```bash
155
+ docker run -it -e STORAGE_ACCOUNT=<storage> -e STORAGE_ACCESS_KEY=<key> fluent
156
+ ```
157
+
158
+ ## Contributing
159
+
160
+ This project welcomes contributions and suggestions. Most contributions require you to agree to a
161
+ Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
162
+ the rights to use your contribution. For details, visit [https://cla.microsoft.com](https://cla.microsoft.com).
163
+
164
+ When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
165
+ a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
166
+ provided by the bot. You will only need to do this once across all repos using our CLA.
167
+
168
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
169
+ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
170
+ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs.push('lib', 'test')
8
+ t.test_files = FileList['test/**/test_*.rb']
9
+ t.verbose = true
10
+ t.warning = true
11
+ end
12
+
13
+ task default: [:test]
@@ -0,0 +1,20 @@
1
+ # Ruby
2
+ # Package your Ruby project.
3
+ # Add steps that install rails, analyze code, save build artifacts, deploy, and more:
4
+ # https://docs.microsoft.com/azure/devops/pipelines/languages/ruby
5
+
6
+ pool:
7
+ vmImage: 'Ubuntu 16.04'
8
+
9
+ steps:
10
+ - task: UseRubyVersion@0
11
+ inputs:
12
+ versionSpec: '>= 2.5'
13
+
14
+ - script: |
15
+ gem install bundler
16
+ bundle install --retry=3 --jobs=4
17
+ displayName: 'bundle install'
18
+
19
+ - script: bundle exec rake
20
+ displayName: 'bundle exec rake'
@@ -0,0 +1,28 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'custom_fluent-plugin-azure-storage-append-blob'
6
+ spec.version = '0.2.1'
7
+ spec.authors = ['Microsoft Corporation']
8
+ spec.email = ['']
9
+
10
+ spec.summary = 'Azure Storage Append Blob output plugin for Fluentd event collector'
11
+ spec.description = 'Fluentd plugin to upload logs to Azure Storage append blobs.'
12
+ spec.homepage = 'https://github.com/Microsoft/fluent-plugin-azure-storage-append-blob'
13
+ spec.license = 'MIT'
14
+
15
+ test_files, files = `git ls-files -z`.split("\x0").partition do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.files = files
19
+ spec.executables = files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = test_files
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 2.0'
24
+ spec.add_development_dependency 'rake', '~> 13.0'
25
+ spec.add_development_dependency 'test-unit', '~> 3.0'
26
+ spec.add_runtime_dependency 'azure-storage-blob', '~> 2.0'
27
+ spec.add_runtime_dependency 'fluentd', ['>= 0.14.10', '< 2']
28
+ end
@@ -0,0 +1,240 @@
1
+ #---------------------------------------------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for license information.
4
+ #--------------------------------------------------------------------------------------------*/
5
+
6
+ require 'azure/storage/common'
7
+ require 'azure/storage/blob'
8
+ require 'faraday'
9
+ require 'fluent/plugin/output'
10
+ require 'json'
11
+
12
+ module Fluent
13
+ module Plugin
14
+ class AzureStorageAppendBlobOut < Fluent::Plugin::Output
15
+ Fluent::Plugin.register_output('azure-storage-append-blob', self)
16
+
17
+ helpers :formatter, :inject
18
+
19
+ DEFAULT_FORMAT_TYPE = 'out_file'.freeze
20
+ AZURE_BLOCK_SIZE_LIMIT = 4 * 1024 * 1024 - 1
21
+
22
+ config_param :path, :string, default: ''
23
+ config_param :azure_storage_account, :string, default: nil
24
+ config_param :azure_storage_access_key, :string, default: nil, secret: true
25
+ config_param :azure_storage_sas_token, :string, default: nil, secret: true
26
+ config_param :azure_container, :string, default: nil
27
+ config_param :azure_imds_api_version, :string, default: '2019-08-15'
28
+ config_param :azure_token_refresh_interval, :integer, default: 60
29
+ config_param :use_msi, :bool, default: false
30
+ config_param :azure_object_key_format, :string, default: '%{path}%{time_slice}-%{index}.log'
31
+ config_param :auto_create_container, :bool, default: true
32
+ config_param :format, :string, default: DEFAULT_FORMAT_TYPE
33
+ config_param :time_slice_format, :string, default: '%Y%m%d'
34
+ config_param :localtime, :bool, default: false
35
+
36
+ config_section :format do
37
+ config_set_default :@type, DEFAULT_FORMAT_TYPE
38
+ end
39
+
40
+ config_section :buffer do
41
+ config_set_default :chunk_keys, ['time']
42
+ config_set_default :timekey, (60 * 60 * 24)
43
+ end
44
+
45
+ attr_reader :bs
46
+
47
+ def configure(conf)
48
+ super
49
+
50
+ @formatter = formatter_create
51
+
52
+ @path_slicer = if @localtime
53
+ proc do |path|
54
+ Time.now.strftime(path)
55
+ end
56
+ else
57
+ proc do |path|
58
+ Time.now.utc.strftime(path)
59
+ end
60
+ end
61
+
62
+ raise ConfigError, 'azure_storage_account needs to be specified' if @azure_storage_account.nil?
63
+
64
+ raise ConfigError, 'azure_container needs to be specified' if @azure_container.nil?
65
+
66
+ if (@azure_storage_access_key.nil? || @azure_storage_access_key.empty?) && (@azure_storage_sas_token.nil? || @azure_storage_sas_token.empty?)
67
+ log.info 'Using MSI since neither azure_storage_access_key nor azure_storage_sas_token was provided.'
68
+ @use_msi = true
69
+ end
70
+ end
71
+
72
+ def multi_workers_ready?
73
+ true
74
+ end
75
+
76
+ def get_access_token
77
+ access_key_request = Faraday.new('http://169.254.169.254/metadata/identity/oauth2/token?' \
78
+ "api-version=#{@azure_imds_api_version}" \
79
+ '&resource=https://storage.azure.com/',
80
+ headers: { 'Metadata' => 'true' })
81
+ .get
82
+ .body
83
+ JSON.parse(access_key_request)['access_token']
84
+ end
85
+
86
+ def start
87
+ super
88
+ if @use_msi
89
+ token_credential = Azure::Storage::Common::Core::TokenCredential.new get_access_token
90
+ token_signer = Azure::Storage::Common::Core::Auth::TokenSigner.new token_credential
91
+ @bs = Azure::Storage::Blob::BlobService.new(storage_account_name: @azure_storage_account, signer: token_signer)
92
+
93
+ refresh_interval = @azure_token_refresh_interval * 60
94
+ cancelled = false
95
+ renew_token = Thread.new do
96
+ Thread.stop
97
+ until cancelled
98
+ sleep(refresh_interval)
99
+
100
+ token_credential.renew_token get_access_token
101
+ end
102
+ end
103
+ sleep 0.1 while renew_token.status != 'sleep'
104
+ renew_token.run
105
+ else
106
+ @bs_params = { storage_account_name: @azure_storage_account }
107
+
108
+ if !@azure_storage_access_key.nil? && !@azure_storage_access_key.empty?
109
+ @bs_params.merge!({ storage_access_key: @azure_storage_access_key })
110
+ elsif !@azure_storage_sas_token.nil? && !@azure_storage_sas_token.empty?
111
+ @bs_params.merge!({ storage_sas_token: @azure_storage_sas_token })
112
+ end
113
+
114
+ @bs = Azure::Storage::Blob::BlobService.create(@bs_params)
115
+ end
116
+
117
+ ensure_container
118
+ @azure_storage_path = ''
119
+ @last_azure_storage_path = ''
120
+ @current_index = 0
121
+ end
122
+
123
+ def format(tag, time, record)
124
+ r = inject_values_to_record(tag, time, record)
125
+ @formatter.format(tag, time, r)
126
+ end
127
+
128
+ def write(chunk)
129
+ metadata = chunk.metadata
130
+ tmp = Tempfile.new('azure-')
131
+ begin
132
+ chunk.write_to(tmp)
133
+
134
+ generate_log_name(metadata, @current_index)
135
+ if @last_azure_storage_path != @azure_storage_path
136
+ @current_index = 0
137
+ generate_log_name(metadata, @current_index)
138
+ end
139
+
140
+ content = File.open(tmp.path, 'rb', &:read)
141
+
142
+ if content.length > 0
143
+ append_blob(content, metadata)
144
+ end
145
+
146
+ @last_azure_storage_path = @azure_storage_path
147
+ ensure
148
+ begin
149
+ tmp.close(true)
150
+ rescue StandardError
151
+ nil
152
+ end
153
+ end
154
+ end
155
+
156
+ def container_exists?(container)
157
+ begin
158
+ @bs.get_container_properties(container)
159
+ rescue Azure::Core::Http::HTTPError => ex
160
+ if ex.status_code == 404 # container does not exist
161
+ return false
162
+ else
163
+ raise
164
+ end
165
+ end
166
+ return true
167
+ end
168
+
169
+ private
170
+
171
+ def ensure_container
172
+ unless container_exists? @azure_container
173
+ if @auto_create_container
174
+ @bs.create_container(@azure_container)
175
+ else
176
+ raise "The specified container does not exist: container = #{@azure_container}"
177
+ end
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def generate_log_name(metadata, index)
184
+ time_slice = if metadata.timekey.nil?
185
+ ''.freeze
186
+ else
187
+ Time.at(metadata.timekey).utc.strftime(@time_slice_format)
188
+ end
189
+
190
+ path = @path_slicer.call(@path)
191
+ values_for_object_key = {
192
+ '%{path}' => path,
193
+ '%{time_slice}' => time_slice,
194
+ '%{index}' => index
195
+ }
196
+ storage_path = @azure_object_key_format.gsub(/%{[^}]+}/, values_for_object_key)
197
+ @azure_storage_path = extract_placeholders(storage_path, metadata)
198
+ end
199
+
200
+ private
201
+
202
+ def append_blob(content, metadata)
203
+ position = 0
204
+ log.debug "azure_storage_append_blob: append_blob.start: Content size: #{content.length}"
205
+ loop do
206
+ begin
207
+ size = [content.length - position, AZURE_BLOCK_SIZE_LIMIT].min
208
+ log.debug "azure_storage_append_blob: append_blob.chunk: content[#{position}..#{position + size}]"
209
+ @bs.append_blob_block(@azure_container, @azure_storage_path, content[position..position + size])
210
+ position += size
211
+ break if position >= content.length
212
+ rescue Azure::Core::Http::HTTPError => e
213
+ status_code = e.status_code
214
+
215
+ if status_code == 409 # exceeds azure block limit
216
+ @current_index += 1
217
+ old_azure_storage_path = @azure_storage_path
218
+ generate_log_name(metadata, @current_index)
219
+
220
+ # If index is not a part of format, rethrow exception.
221
+ if old_azure_storage_path == @azure_storage_path
222
+ log.warn 'azure_storage_append_blob: append_blob: blocks limit reached, you need to use %{index} for the format.'
223
+ raise
224
+ end
225
+
226
+ log.debug "azure_storage_append_blob: append_blob: blocks limit reached, creating new blob #{@azure_storage_path}."
227
+ @bs.create_append_blob(@azure_container, @azure_storage_path)
228
+ elsif status_code == 404 # blob not found
229
+ log.debug "azure_storage_append_blob: append_blob: #{@azure_storage_path} blob doesn't exist, creating new blob."
230
+ @bs.create_append_blob(@azure_container, @azure_storage_path)
231
+ else
232
+ raise
233
+ end
234
+ end
235
+ end
236
+ log.debug 'azure_storage_append_blob: append_blob.complete'
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path('..', __dir__))
2
+ require 'test-unit'
3
+ require 'fluent/test'
4
+ require 'fluent/test/driver/output'
5
+ require 'fluent/test/helpers'
6
+
7
+ Test::Unit::TestCase.include(Fluent::Test::Helpers)
8
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)
@@ -0,0 +1,137 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/out_azure-storage-append-blob.rb'
3
+ require 'azure/core/http/http_response'
4
+ require 'azure/core/http/http_error'
5
+
6
+ include Fluent::Test::Helpers
7
+
8
+ class AzureStorageAppendBlobOutTest < Test::Unit::TestCase
9
+ setup do
10
+ Fluent::Test.setup
11
+ end
12
+
13
+ CONFIG = %(
14
+ azure_storage_account test_storage_account
15
+ azure_storage_access_key MY_FAKE_SECRET
16
+ azure_container test_container
17
+ time_slice_format %Y%m%d-%H
18
+ path log
19
+ ).freeze
20
+
21
+ MSI_CONFIG = %(
22
+ azure_storage_account test_storage_account
23
+ azure_container test_container
24
+ azure_imds_api_version 1970-01-01
25
+ azure_token_refresh_interval 120
26
+ time_slice_format %Y%m%d-%H
27
+ path log
28
+ ).freeze
29
+
30
+ def create_driver(conf = CONFIG)
31
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::AzureStorageAppendBlobOut).configure(conf)
32
+ end
33
+
34
+ sub_test_case 'test config' do
35
+ test 'config should reject with no azure container' do
36
+ assert_raise Fluent::ConfigError do
37
+ create_driver(%(
38
+ azure_storage_account test_storage_account
39
+ azure_storage_access_key MY_FAKE_SECRET
40
+ time_slice_format %Y%m%d-%H
41
+ time_slice_wait 10m
42
+ path log
43
+ ))
44
+ end
45
+ end
46
+
47
+ test 'config with access key should set instance variables' do
48
+ d = create_driver
49
+ assert_equal 'test_storage_account', d.instance.azure_storage_account
50
+ assert_equal 'MY_FAKE_SECRET', d.instance.azure_storage_access_key
51
+ assert_equal 'test_container', d.instance.azure_container
52
+ assert_equal true, d.instance.auto_create_container
53
+ assert_equal '%{path}%{time_slice}-%{index}.log', d.instance.azure_object_key_format
54
+ end
55
+
56
+ test 'config with managed identity enabled should set instance variables' do
57
+ d = create_driver(MSI_CONFIG)
58
+ assert_equal 'test_storage_account', d.instance.azure_storage_account
59
+ assert_equal 'test_container', d.instance.azure_container
60
+ assert_equal true, d.instance.use_msi
61
+ assert_equal true, d.instance.auto_create_container
62
+ assert_equal '%{path}%{time_slice}-%{index}.log', d.instance.azure_object_key_format
63
+ assert_equal 120, d.instance.azure_token_refresh_interval
64
+ assert_equal '1970-01-01', d.instance.azure_imds_api_version
65
+ end
66
+ end
67
+
68
+ sub_test_case 'test path slicing' do
69
+ test 'test path_slicing' do
70
+ config = CONFIG.clone.gsub(/path\slog/, 'path log/%Y/%m/%d')
71
+ d = create_driver(config)
72
+ path_slicer = d.instance.instance_variable_get(:@path_slicer)
73
+ path = d.instance.instance_variable_get(:@path)
74
+ slice = path_slicer.call(path)
75
+ assert_equal slice, Time.now.utc.strftime('log/%Y/%m/%d')
76
+ end
77
+
78
+ test 'path slicing utc' do
79
+ config = CONFIG.clone.gsub(/path\slog/, 'path log/%Y/%m/%d')
80
+ config << "\nutc\n"
81
+ d = create_driver(config)
82
+ path_slicer = d.instance.instance_variable_get(:@path_slicer)
83
+ path = d.instance.instance_variable_get(:@path)
84
+ slice = path_slicer.call(path)
85
+ assert_equal slice, Time.now.utc.strftime('log/%Y/%m/%d')
86
+ end
87
+ end
88
+
89
+ # This class is used to create an Azure::Core::Http::HTTPError. HTTPError parses
90
+ # a response object when it is created.
91
+ class FakeResponse
92
+ def initialize(status=404)
93
+ @status = status
94
+ @body = "body"
95
+ @headers = {}
96
+ end
97
+
98
+ attr_reader :status
99
+ attr_reader :body
100
+ attr_reader :headers
101
+ end
102
+
103
+ # This class is used to test plugin functions which interact with the blob service
104
+ class FakeBlobService
105
+ def initialize(status)
106
+ @response = Azure::Core::Http::HttpResponse.new(FakeResponse.new(status))
107
+ end
108
+
109
+ def get_container_properties(container)
110
+ unless @response.status_code == 200
111
+ raise Azure::Core::Http::HTTPError.new(@response)
112
+ end
113
+ end
114
+ end
115
+
116
+ sub_test_case 'test container_exists' do
117
+ test 'container 404 returns false' do
118
+ d = create_driver
119
+ d.instance.instance_variable_set(:@bs, FakeBlobService.new(404))
120
+ assert_false d.instance.container_exists? "anything"
121
+ end
122
+
123
+ test 'existing container returns true' do
124
+ d = create_driver
125
+ d.instance.instance_variable_set(:@bs, FakeBlobService.new(200))
126
+ assert_true d.instance.container_exists? "anything"
127
+ end
128
+
129
+ test 'unexpected exception raises' do
130
+ d = create_driver
131
+ d.instance.instance_variable_set(:@bs, FakeBlobService.new(500))
132
+ assert_raise_kind_of Azure::Core::Http::HTTPError do
133
+ d.instance.container_exists? "anything"
134
+ end
135
+ end
136
+ end
137
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: custom_fluent-plugin-azure-storage-append-blob
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Microsoft Corporation
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: azure-storage-blob
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fluentd
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.14.10
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: '2'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 0.14.10
86
+ - - "<"
87
+ - !ruby/object:Gem::Version
88
+ version: '2'
89
+ description: Fluentd plugin to upload logs to Azure Storage append blobs.
90
+ email:
91
+ - ''
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".github/workflows/gem-push.yml"
97
+ - ".github/workflows/ruby.yml"
98
+ - ".gitignore"
99
+ - ".rubocop.yml"
100
+ - Dockerfile
101
+ - Gemfile
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - azure-pipelines.yml
106
+ - fluent-plugin-azure-storage-append-blob.gemspec
107
+ - lib/fluent/plugin/out_azure-storage-append-blob.rb
108
+ - test/helper.rb
109
+ - test/plugin/test_out_azure_storage_append_blob.rb
110
+ homepage: https://github.com/Microsoft/fluent-plugin-azure-storage-append-blob
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.7.6
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Azure Storage Append Blob output plugin for Fluentd event collector
134
+ test_files:
135
+ - test/helper.rb
136
+ - test/plugin/test_out_azure_storage_append_blob.rb