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.
- checksums.yaml +7 -0
- data/.github/workflows/gem-push.yml +31 -0
- data/.github/workflows/ruby.yml +23 -0
- data/.gitignore +50 -0
- data/.rubocop.yml +2 -0
- data/Dockerfile +36 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +170 -0
- data/Rakefile +13 -0
- data/azure-pipelines.yml +20 -0
- data/fluent-plugin-azure-storage-append-blob.gemspec +28 -0
- data/lib/fluent/plugin/out_azure-storage-append-blob.rb +240 -0
- data/test/helper.rb +8 -0
- data/test/plugin/test_out_azure_storage_append_blob.rb +137 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
data/.rubocop.yml
ADDED
data/Dockerfile
ADDED
@@ -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
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
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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]
|
data/azure-pipelines.yml
ADDED
@@ -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
|
data/test/helper.rb
ADDED
@@ -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
|