logstash-output-azureblob 0.9.0
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/CHANGELOG.md +2 -0
- data/CONTRIBUTORS +16 -0
- data/DEVELOPER.md +2 -0
- data/Gemfile +3 -0
- data/LICENSE +11 -0
- data/README.md +102 -0
- data/lib/logstash/outputs/azure_blob.rb +235 -0
- data/lib/logstash/outputs/blob/file_repository.rb +138 -0
- data/lib/logstash/outputs/blob/path_validator.rb +20 -0
- data/lib/logstash/outputs/blob/size_and_time_rotation_policy.rb +28 -0
- data/lib/logstash/outputs/blob/size_rotation_policy.rb +29 -0
- data/lib/logstash/outputs/blob/temporary_file.rb +81 -0
- data/lib/logstash/outputs/blob/temporary_file_factory.rb +135 -0
- data/lib/logstash/outputs/blob/time_rotation_policy.rb +29 -0
- data/lib/logstash/outputs/blob/uploader.rb +75 -0
- data/lib/logstash/outputs/blob/writable_directory_validator.rb +19 -0
- data/lib/logstash/outputs/template.rb +16 -0
- data/logstash-output-azureblob.gemspec +25 -0
- data/spec/outputs/azureblob_spec.rb +49 -0
- data/spec/outputs/blob/file_repository_spec.rb +140 -0
- data/spec/outputs/blob/size_and_time_rotation_policy_spec.rb +76 -0
- data/spec/outputs/blob/size_rotation_policy_spec.rb +39 -0
- data/spec/outputs/blob/temporary_file_factory_spec.rb +88 -0
- data/spec/outputs/blob/temporary_file_spec.rb +46 -0
- data/spec/outputs/blob/time_rotation_policy_spec.rb +59 -0
- data/spec/outputs/blob/uploader_spec.rb +61 -0
- data/spec/outputs/blob/writable_directory_validator_spec.rb +39 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/supports/helpers.rb +27 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3b36f0e9cef45f58a3ce40f93e9d8808f4acb554cf3eeea2fc20a77b6a62e25a
|
4
|
+
data.tar.gz: 372d3ac19b637d675df14fc9733eb8da90117c41a6aab4b5fa745d31ea46c81c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0bf72cc5ba7923f1c9ba8714c33ca1b226ef40c04fe611ece979f254e65f44a9cee95c332734e05678ad3dc6c13c59be1278b987145cfd3d6bf633c2a2e58863
|
7
|
+
data.tar.gz: 9aad5f4b182892d3700a09b3e58a95846ca235f14075aef507898bff50cc36965d7e0a73c3b21c90641da8dab06051143cd949b130eeeb857d8de762c38d5e4f
|
data/CHANGELOG.md
ADDED
data/CONTRIBUTORS
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
The following is a list of people who have contributed ideas, code, bug
|
2
|
+
reports, or in general have helped logstash along its way.
|
3
|
+
|
4
|
+
Current Contributors:
|
5
|
+
* Sean Stark -sean.stark@microsoft.com
|
6
|
+
|
7
|
+
Original Contributors - no longer maintainers
|
8
|
+
* Tuffk - tuffkmulhall@gmail.com
|
9
|
+
* BrunoLerner - bru.lerner@gmail.com
|
10
|
+
* Alex-Tsyganok -
|
11
|
+
* Charlie Zha - zysimplelife@gmail.com
|
12
|
+
|
13
|
+
Note: If you've sent us patches, bug reports, or otherwise contributed to
|
14
|
+
Logstash, and you aren't on the list above and want to be, please let us know
|
15
|
+
and we'll make sure you're here. Contributions from folks like you are what make
|
16
|
+
open source awesome.
|
data/DEVELOPER.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
2
|
+
you may not use this file except in compliance with the License.
|
3
|
+
You may obtain a copy of the License at
|
4
|
+
|
5
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
See the License for the specific language governing permissions and
|
11
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
|
2
|
+
# Logstash Output Plugin for Azure Blob Storage
|
3
|
+
|
4
|
+
This is an output plugin for [Logstash](https://github.com/elastic/logstash). It is fully free and open source. The license is Apache 2.0. This plugin was forked from https://github.com/tuffk/Logstash-output-to-Azure-Blob and updated to use the latest Azure Storage Ruby SDK
|
5
|
+
|
6
|
+
- This plugin uses the https://github.com/Azure/azure-storage-ruby library
|
7
|
+
- The class documentation is here: https://www.rubydoc.info/gems/azure-storage-blob
|
8
|
+
|
9
|
+
## Disclaimers
|
10
|
+
|
11
|
+
I am not a Ruby developer and may not be able to respond efficently to issues or bugs. Please take this into consideration when using this plugin
|
12
|
+
|
13
|
+
- Azure Data Lake Storage Gen2 accounts are not currently supported.
|
14
|
+
- Managed Identities and Service Principles are currently not supported for auth.
|
15
|
+
|
16
|
+
## Requirements
|
17
|
+
- Logstash version 8.6+ [Installation instructions](https://www.elastic.co/guide/en/logstash/current/installing-logstash.html).
|
18
|
+
- Tested on 8.6.2
|
19
|
+
- Azure Storage Account
|
20
|
+
- Azure Storage Account Access Key(s)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
```sh
|
24
|
+
bin/logstash-plugin install logstash-output-azureblob
|
25
|
+
```
|
26
|
+
> On Ubuntu the default path is /usr/share/logstash/bin/
|
27
|
+
|
28
|
+
## Configuration
|
29
|
+
|
30
|
+
Information about configuring Logstash can be found in the [Logstash configuration guide](https://www.elastic.co/guide/en/logstash/current/configuration.html).
|
31
|
+
|
32
|
+
You will need to configure this plugin before sending events from Logstash to an Azure Storage Account. The following example shows the minimum you need to provide:
|
33
|
+
|
34
|
+
```yaml
|
35
|
+
output {
|
36
|
+
azure_blob {
|
37
|
+
storage_account_name => "my-azure-account" # required
|
38
|
+
storage_access_key => "my-super-secret-key" # required
|
39
|
+
container_name => "my-container" # required
|
40
|
+
size_file => 1024*1024*5 # optional
|
41
|
+
time_file => 10 # optional
|
42
|
+
restore => true # optional
|
43
|
+
temporary_directory => "path/to/directory" # optional
|
44
|
+
prefix => "a_prefix" # optional
|
45
|
+
upload_queue_size => 2 # optional
|
46
|
+
upload_workers_count => 1 # optional
|
47
|
+
rotation_strategy_val => "size_and_time" # optional
|
48
|
+
tags => [] # optional
|
49
|
+
encoding => "none" # optional
|
50
|
+
}
|
51
|
+
}
|
52
|
+
```
|
53
|
+
|
54
|
+
### Example with syslog
|
55
|
+
|
56
|
+
```yaml
|
57
|
+
input {
|
58
|
+
syslog {
|
59
|
+
port => "5514"
|
60
|
+
type => "syslog"
|
61
|
+
codec => cef
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
output {
|
66
|
+
azure_blob {
|
67
|
+
storage_account_name => "<account-name>"
|
68
|
+
storage_access_key => "<access-key>"
|
69
|
+
container_name => "<container-name>"
|
70
|
+
}
|
71
|
+
}
|
72
|
+
```
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
- Docker Image - [cameronkerrnz/logstash-plugin-dev:7.17](https://hub.docker.com/r/cameronkerrnz/logstash-plugin-dev)
|
77
|
+
- https://github.com/cameronkerrnz/logstash-plugin-dev
|
78
|
+
- jruby 9.2.20.1 (2.5.8)
|
79
|
+
- Logstash Version 8.6.2+
|
80
|
+
|
81
|
+
1. Install Dependencies
|
82
|
+
|
83
|
+
```shell
|
84
|
+
rake vendor
|
85
|
+
bundle install
|
86
|
+
```
|
87
|
+
2. Build the plugin
|
88
|
+
```shell
|
89
|
+
gem build logstash-output-azureblob.gemspec
|
90
|
+
```
|
91
|
+
3. Install Locally
|
92
|
+
```shell
|
93
|
+
/usr/share/logstash/bin/logstash-plugin install /usr/share/logstash/logstash-output-azureblob-0.9.0.gem
|
94
|
+
```
|
95
|
+
4. Test with configuration file
|
96
|
+
```shell
|
97
|
+
/usr/share/logstash/bin/logstash -f blob.conf
|
98
|
+
```
|
99
|
+
|
100
|
+
## Contributing
|
101
|
+
|
102
|
+
All contributions are welcome: ideas, patches, documentation, bug reports, and complaints. For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
|
@@ -0,0 +1,235 @@
|
|
1
|
+
require 'logstash/outputs/base'
|
2
|
+
require 'logstash/namespace'
|
3
|
+
require 'azure/storage/blob'
|
4
|
+
require 'azure/storage/common'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
class LogStash::Outputs::LogstashAzureBlobOutput < LogStash::Outputs::Base
|
8
|
+
# name for the namespace under output for logstash configuration
|
9
|
+
config_name 'azure_blob'
|
10
|
+
default :codec, "line"
|
11
|
+
|
12
|
+
require 'logstash/outputs/blob/writable_directory_validator'
|
13
|
+
require 'logstash/outputs/blob/path_validator'
|
14
|
+
require 'logstash/outputs/blob/size_rotation_policy'
|
15
|
+
require 'logstash/outputs/blob/time_rotation_policy'
|
16
|
+
require 'logstash/outputs/blob/size_and_time_rotation_policy'
|
17
|
+
require 'logstash/outputs/blob/temporary_file'
|
18
|
+
require 'logstash/outputs/blob/temporary_file_factory'
|
19
|
+
require 'logstash/outputs/blob/uploader'
|
20
|
+
require 'logstash/outputs/blob/file_repository'
|
21
|
+
|
22
|
+
PREFIX_KEY_NORMALIZE_CHARACTER = '_'.freeze
|
23
|
+
PERIODIC_CHECK_INTERVAL_IN_SECONDS = 15
|
24
|
+
CRASH_RECOVERY_THREADPOOL = Concurrent::ThreadPoolExecutor.new(min_threads: 1,
|
25
|
+
max_threads: 2,
|
26
|
+
fallback_policy: :caller_runs)
|
27
|
+
|
28
|
+
# azure container
|
29
|
+
config :storage_account_name, validate: :string, required: false
|
30
|
+
|
31
|
+
# azure key
|
32
|
+
config :storage_access_key, validate: :string, required: false
|
33
|
+
|
34
|
+
# conatainer name
|
35
|
+
config :container_name, validate: :string, required: false
|
36
|
+
|
37
|
+
# mamadas
|
38
|
+
config :size_file, validate: :number, default: 1024 * 1024 * 5
|
39
|
+
config :time_file, validate: :number, default: 15
|
40
|
+
config :restore, validate: :boolean, default: true
|
41
|
+
config :temporary_directory, validate: :string, default: File.join(Dir.tmpdir, 'logstash')
|
42
|
+
config :prefix, validate: :string, default: ''
|
43
|
+
config :upload_queue_size, validate: :number, default: 2 * (Concurrent.processor_count * 0.25).ceil
|
44
|
+
config :upload_workers_count, validate: :number, default: (Concurrent.processor_count * 0.5).ceil
|
45
|
+
config :rotation_strategy_val, validate: %w[size_and_time size time], default: 'size_and_time'
|
46
|
+
config :tags, validate: :array, default: []
|
47
|
+
config :encoding, validate: %w[none gzip], default: 'none'
|
48
|
+
|
49
|
+
attr_accessor :storage_account_name, :storage_access_key, :container_name,
|
50
|
+
:size_file, :time_file, :restore, :temporary_directory, :prefix, :upload_queue_size,
|
51
|
+
:upload_workers_count, :rotation_strategy_val, :tags, :encoding
|
52
|
+
|
53
|
+
# initializes the +LogstashAzureBlobOutput+ instances
|
54
|
+
# validates all config parameters
|
55
|
+
# initializes the uploader
|
56
|
+
def register
|
57
|
+
unless @prefix.empty?
|
58
|
+
unless PathValidator.valid?(prefix)
|
59
|
+
raise LogStash::ConfigurationError.new("Prefix must not contains: #{PathValidator::INVALID_CHARACTERS}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
unless WritableDirectoryValidator.valid?(@temporary_directory)
|
64
|
+
raise LogStash::ConfigurationError.new("Logstash must have the permissions to write to the temporary directory: #{@temporary_directory}")
|
65
|
+
end
|
66
|
+
|
67
|
+
if @time_file.nil? && @size_file.nil? || @size_file.zero? && @time_file.zero?
|
68
|
+
raise LogStash::ConfigurationError.new('at least one of time_file or size_file set to a value greater than 0')
|
69
|
+
end
|
70
|
+
|
71
|
+
@file_repository = FileRepository.new(@tags, @encoding, @temporary_directory)
|
72
|
+
|
73
|
+
@rotation = rotation_strategy
|
74
|
+
|
75
|
+
executor = Concurrent::ThreadPoolExecutor.new(min_threads: 1,
|
76
|
+
max_threads: @upload_workers_count,
|
77
|
+
max_queue: @upload_queue_size,
|
78
|
+
fallback_policy: :caller_runs)
|
79
|
+
|
80
|
+
@uploader = Uploader.new(blob_container_resource, container_name, @logger, executor)
|
81
|
+
|
82
|
+
restore_from_crash if @restore
|
83
|
+
start_periodic_check if @rotation.needs_periodic?
|
84
|
+
end
|
85
|
+
|
86
|
+
# Receives multiple events and check if there is space in temporary directory
|
87
|
+
# @param events_and_encoded [Object]
|
88
|
+
def multi_receive_encoded(events_and_encoded)
|
89
|
+
prefix_written_to = Set.new
|
90
|
+
|
91
|
+
events_and_encoded.each do |event, encoded|
|
92
|
+
prefix_key = normalize_key(event.sprintf(@prefix))
|
93
|
+
prefix_written_to << prefix_key
|
94
|
+
|
95
|
+
begin
|
96
|
+
@file_repository.get_file(prefix_key) { |file| file.write(encoded) }
|
97
|
+
# The output should stop accepting new events coming in, since it cannot do anything with them anymore.
|
98
|
+
# Log the error and rethrow it.
|
99
|
+
rescue Errno::ENOSPC => e
|
100
|
+
@logger.error('Azure: No space left in temporary directory', temporary_directory: @temporary_directory)
|
101
|
+
raise e
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Groups IO calls to optimize fstat checks
|
106
|
+
rotate_if_needed(prefix_written_to)
|
107
|
+
end
|
108
|
+
|
109
|
+
# close the temporary file and uploads the content to Azure
|
110
|
+
def close
|
111
|
+
stop_periodic_check if @rotation.needs_periodic?
|
112
|
+
|
113
|
+
@logger.debug('Uploading current workspace')
|
114
|
+
|
115
|
+
# The plugin has stopped receiving new events, but we still have
|
116
|
+
# data on disk, lets make sure it get to Azure blob.
|
117
|
+
# If Logstash get interrupted, the `restore_from_crash` (when set to true) method will pickup
|
118
|
+
# the content in the temporary directly and upload it.
|
119
|
+
# This will block the shutdown until all upload are done or the use force quit.
|
120
|
+
@file_repository.each_files do |file|
|
121
|
+
upload_file(file)
|
122
|
+
end
|
123
|
+
|
124
|
+
@file_repository.shutdown
|
125
|
+
|
126
|
+
@uploader.stop # wait until all the current upload are complete
|
127
|
+
@crash_uploader.stop if @restore # we might have still work to do for recovery so wait until we are done
|
128
|
+
end
|
129
|
+
|
130
|
+
# Validates and normalize prefix key
|
131
|
+
# @param prefix_key [String]
|
132
|
+
def normalize_key(prefix_key)
|
133
|
+
prefix_key.gsub(PathValidator.matches_re, PREFIX_KEY_NORMALIZE_CHARACTER)
|
134
|
+
end
|
135
|
+
|
136
|
+
# checks periodically the tmeporary file if it needs to be rotated
|
137
|
+
def start_periodic_check
|
138
|
+
@logger.debug('Start periodic rotation check')
|
139
|
+
|
140
|
+
@periodic_check = Concurrent::TimerTask.new(execution_interval: PERIODIC_CHECK_INTERVAL_IN_SECONDS) do
|
141
|
+
@logger.debug('Periodic check for stale files')
|
142
|
+
|
143
|
+
rotate_if_needed(@file_repository.keys)
|
144
|
+
end
|
145
|
+
|
146
|
+
@periodic_check.execute
|
147
|
+
end
|
148
|
+
|
149
|
+
def stop_periodic_check
|
150
|
+
@periodic_check.shutdown
|
151
|
+
end
|
152
|
+
|
153
|
+
# login to azure cloud using azure storage blob client and create the container if it doesn't exist
|
154
|
+
# @return [Object] the azure_blob_service object, which is the endpoint to azure gem
|
155
|
+
def blob_container_resource
|
156
|
+
blob_client = Azure::Storage::Blob::BlobService.create(
|
157
|
+
storage_account_name: storage_account_name,
|
158
|
+
storage_access_key: storage_access_key
|
159
|
+
)
|
160
|
+
list = blob_client.list_containers()
|
161
|
+
list.each do |item|
|
162
|
+
@container = item if item.name == container_name
|
163
|
+
end
|
164
|
+
|
165
|
+
blob_client.create_container(container_name) unless @container
|
166
|
+
blob_client
|
167
|
+
end
|
168
|
+
|
169
|
+
# check if it needs to rotate according to rotation policy and rotates it if it needs
|
170
|
+
# @param prefixes [String]
|
171
|
+
def rotate_if_needed(prefixes)
|
172
|
+
prefixes.each do |prefix|
|
173
|
+
# Each file access is thread safe,
|
174
|
+
# until the rotation is done then only
|
175
|
+
# one thread has access to the resource.
|
176
|
+
@file_repository.get_factory(prefix) do |factory|
|
177
|
+
temp_file = factory.current
|
178
|
+
|
179
|
+
if @rotation.rotate?(temp_file)
|
180
|
+
@logger.debug('Rotate file',
|
181
|
+
strategy: @rotation.class.name,
|
182
|
+
key: temp_file.key,
|
183
|
+
path: temp_file.path)
|
184
|
+
|
185
|
+
upload_file(temp_file)
|
186
|
+
factory.rotate!
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# uploads the file using the +Uploader+
|
193
|
+
def upload_file(temp_file)
|
194
|
+
@logger.debug('Queue for upload', path: temp_file.path)
|
195
|
+
|
196
|
+
# if the queue is full the calling thread will be used to upload
|
197
|
+
temp_file.close # make sure the content is on disk
|
198
|
+
unless temp_file.empty? # rubocop:disable GuardClause
|
199
|
+
@uploader.upload_async(temp_file,
|
200
|
+
on_complete: method(:clean_temporary_file))
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# creates an instance for the rotation strategy
|
205
|
+
def rotation_strategy
|
206
|
+
case @rotation_strategy_val
|
207
|
+
when 'size'
|
208
|
+
SizeRotationPolicy.new(size_file)
|
209
|
+
when 'time'
|
210
|
+
TimeRotationPolicy.new(time_file)
|
211
|
+
when 'size_and_time'
|
212
|
+
SizeAndTimeRotationPolicy.new(size_file, time_file)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Cleans the temporary files after it is uploaded to azure blob
|
217
|
+
def clean_temporary_file(file)
|
218
|
+
@logger.debug('Removing temporary file', file: file.path)
|
219
|
+
file.delete!
|
220
|
+
end
|
221
|
+
|
222
|
+
# uploads files if there was a crash before
|
223
|
+
def restore_from_crash
|
224
|
+
@crash_uploader = Uploader.new(blob_container_resource, container_name, @logger, CRASH_RECOVERY_THREADPOOL)
|
225
|
+
|
226
|
+
temp_folder_path = Pathname.new(@temporary_directory)
|
227
|
+
Dir.glob(::File.join(@temporary_directory, '**/*'))
|
228
|
+
.select { |file| ::File.file?(file) }
|
229
|
+
.each do |file|
|
230
|
+
temp_file = TemporaryFile.create_from_existing_file(file, temp_folder_path)
|
231
|
+
@logger.debug('Recovering from crash and uploading', file: temp_file.path)
|
232
|
+
@crash_uploader.upload_async(temp_file, on_complete: method(:clean_temporary_file))
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
|
2
|
+
require 'java'
|
3
|
+
require 'concurrent'
|
4
|
+
require 'concurrent/timer_task'
|
5
|
+
require 'logstash/util'
|
6
|
+
|
7
|
+
ConcurrentHashMap = java.util.concurrent.ConcurrentHashMap
|
8
|
+
|
9
|
+
module LogStash
|
10
|
+
module Outputs
|
11
|
+
class LogstashAzureBlobOutput
|
12
|
+
# sub class for +LogstashAzureBlobOutput+
|
13
|
+
# this class manages the temporary directory for the temporary files
|
14
|
+
class FileRepository
|
15
|
+
DEFAULT_STATE_SWEEPER_INTERVAL_SECS = 60
|
16
|
+
DEFAULT_STALE_TIME_SECS = 15 * 60
|
17
|
+
# Ensure that all access or work done
|
18
|
+
# on a factory is threadsafe
|
19
|
+
class PrefixedValue
|
20
|
+
# initialize the factory
|
21
|
+
def initialize(file_factory, stale_time)
|
22
|
+
@file_factory = file_factory
|
23
|
+
@lock = Mutex.new
|
24
|
+
@stale_time = stale_time
|
25
|
+
end
|
26
|
+
|
27
|
+
# activate the lock
|
28
|
+
def with_lock
|
29
|
+
@lock.synchronize do
|
30
|
+
yield @file_factory
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# boolean method
|
35
|
+
def stale?
|
36
|
+
with_lock { |factory| factory.current.size.zero? && (Time.now - factory.current.ctime > @stale_time) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# return this class
|
40
|
+
def apply(_prefix)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# delete the current factory
|
45
|
+
def delete!
|
46
|
+
with_lock { |factory| factory.current.delete! }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# class for initializing the repo manager
|
51
|
+
class FactoryInitializer
|
52
|
+
# initializes the class
|
53
|
+
def initialize(tags, encoding, temporary_directory, stale_time)
|
54
|
+
@tags = tags
|
55
|
+
@encoding = encoding
|
56
|
+
@temporary_directory = temporary_directory
|
57
|
+
@stale_time = stale_time
|
58
|
+
end
|
59
|
+
|
60
|
+
# applies the prefix key
|
61
|
+
def apply(prefix_key)
|
62
|
+
PrefixedValue.new(TemporaryFileFactory.new(prefix_key, @tags, @encoding, @temporary_directory), @stale_time)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# initializes the class with more variables
|
66
|
+
def initialize(tags, encoding, temporary_directory,
|
67
|
+
stale_time = DEFAULT_STALE_TIME_SECS,
|
68
|
+
sweeper_interval = DEFAULT_STATE_SWEEPER_INTERVAL_SECS)
|
69
|
+
# The path need to contains the prefix so when we start
|
70
|
+
# logtash after a crash we keep the remote structure
|
71
|
+
@prefixed_factories = ConcurrentHashMap.new
|
72
|
+
|
73
|
+
@sweeper_interval = sweeper_interval
|
74
|
+
|
75
|
+
@factory_initializer = FactoryInitializer.new(tags, encoding, temporary_directory, stale_time)
|
76
|
+
|
77
|
+
start_stale_sweeper
|
78
|
+
end
|
79
|
+
|
80
|
+
# gets the key set
|
81
|
+
def keys
|
82
|
+
@prefixed_factories.keySet
|
83
|
+
end
|
84
|
+
|
85
|
+
# with lock for each file
|
86
|
+
def each_files
|
87
|
+
@prefixed_factories.elements.each do |prefixed_file|
|
88
|
+
prefixed_file.with_lock { |factory| yield factory.current }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the file factory
|
93
|
+
def get_factory(prefix_key)
|
94
|
+
@prefixed_factories.computeIfAbsent(prefix_key, @factory_initializer).with_lock { |factory| yield factory }
|
95
|
+
end
|
96
|
+
|
97
|
+
# gets file from prefix_key
|
98
|
+
def get_file(prefix_key)
|
99
|
+
get_factory(prefix_key) { |factory| yield factory.current }
|
100
|
+
end
|
101
|
+
|
102
|
+
# stops. shutdown
|
103
|
+
def shutdown
|
104
|
+
stop_stale_sweeper
|
105
|
+
end
|
106
|
+
|
107
|
+
# gets factory's size
|
108
|
+
def size
|
109
|
+
@prefixed_factories.size
|
110
|
+
end
|
111
|
+
|
112
|
+
# remove the stale given key and value
|
113
|
+
def remove_stale(k, v)
|
114
|
+
if v.stale? # rubocop:disable Style/GuardClause
|
115
|
+
@prefixed_factories.remove(k, v)
|
116
|
+
v.delete!
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# starts the stale sweeper
|
121
|
+
def start_stale_sweeper
|
122
|
+
@stale_sweeper = Concurrent::TimerTask.new(execution_interval: @sweeper_interval) do
|
123
|
+
LogStash::Util.set_thread_name('LogstashAzureBlobOutput, Stale factory sweeper')
|
124
|
+
|
125
|
+
@prefixed_factories.forEach { |k, v| remove_stale(k, v) }
|
126
|
+
end
|
127
|
+
|
128
|
+
@stale_sweeper.execute
|
129
|
+
end
|
130
|
+
|
131
|
+
# stops the stale sweeper
|
132
|
+
def stop_stale_sweeper
|
133
|
+
@stale_sweeper.shutdown
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module LogStash
|
2
|
+
module Outputs
|
3
|
+
class LogstashAzureBlobOutput
|
4
|
+
# a sub class of +LogstashAzureBlobOutput+
|
5
|
+
# valdiates the path for the temporary directory
|
6
|
+
class PathValidator
|
7
|
+
INVALID_CHARACTERS = "\^`><".freeze
|
8
|
+
# boolean method to check if a name is valid
|
9
|
+
def self.valid?(name)
|
10
|
+
name.match(matches_re).nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
# define the invalid characters that shouldn't be in the path name
|
14
|
+
def self.matches_re
|
15
|
+
/[#{Regexp.escape(INVALID_CHARACTERS)}]/
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logstash/outputs/blob/size_rotation_policy'
|
2
|
+
require 'logstash/outputs/blob/time_rotation_policy'
|
3
|
+
|
4
|
+
module LogStash
|
5
|
+
module Outputs
|
6
|
+
class LogstashAzureBlobOutput
|
7
|
+
# a sub class of +LogstashAzureBlobOutput+
|
8
|
+
# sets the rotation policy
|
9
|
+
class SizeAndTimeRotationPolicy
|
10
|
+
# initialize the class
|
11
|
+
def initialize(file_size, time_file)
|
12
|
+
@size_strategy = SizeRotationPolicy.new(file_size)
|
13
|
+
@time_strategy = TimeRotationPolicy.new(time_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
# check if it is time to rotate
|
17
|
+
def rotate?(file)
|
18
|
+
@size_strategy.rotate?(file) || @time_strategy.rotate?(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
# boolean method
|
22
|
+
def needs_periodic?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module LogStash
|
2
|
+
module Outputs
|
3
|
+
class LogstashAzureBlobOutput
|
4
|
+
# a sub class of +LogstashAzureBlobOutput+
|
5
|
+
# sets the rotation policy by size
|
6
|
+
class SizeRotationPolicy
|
7
|
+
attr_reader :size_file
|
8
|
+
# initialize the class
|
9
|
+
def initialize(size_file)
|
10
|
+
if size_file <= 0
|
11
|
+
raise LogStash::ConfigurationError.new('`size_file` need to be greather than 0')
|
12
|
+
end
|
13
|
+
|
14
|
+
@size_file = size_file
|
15
|
+
end
|
16
|
+
|
17
|
+
# boolean method to check if it is time to rotate
|
18
|
+
def rotate?(file)
|
19
|
+
file.size >= size_file
|
20
|
+
end
|
21
|
+
|
22
|
+
# boolean method
|
23
|
+
def needs_periodic?
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module LogStash
|
6
|
+
module Outputs
|
7
|
+
class LogstashAzureBlobOutput
|
8
|
+
# a sub class of +LogstashAzureBlobOutput+
|
9
|
+
# Wrap the actual file descriptor into an utility classe
|
10
|
+
# It make it more OOP and easier to reason with the paths.
|
11
|
+
class TemporaryFile
|
12
|
+
extend Forwardable
|
13
|
+
|
14
|
+
def_delegators :@fd, :path, :write, :close, :fsync
|
15
|
+
|
16
|
+
attr_reader :fd
|
17
|
+
|
18
|
+
# initialize the class
|
19
|
+
def initialize(key, fd, temp_path)
|
20
|
+
@fd = fd
|
21
|
+
@key = key
|
22
|
+
@temp_path = temp_path
|
23
|
+
@created_at = Time.now
|
24
|
+
end
|
25
|
+
|
26
|
+
# gets the created at time
|
27
|
+
def ctime
|
28
|
+
@created_at
|
29
|
+
end
|
30
|
+
|
31
|
+
# gets path to temporary directory
|
32
|
+
attr_reader :temp_path
|
33
|
+
|
34
|
+
# gets the size of file
|
35
|
+
def size
|
36
|
+
# Use the fd size to get the accurate result,
|
37
|
+
# so we dont have to deal with fsync
|
38
|
+
# if the file is close we will use the File::size
|
39
|
+
|
40
|
+
@fd.size
|
41
|
+
rescue IOError
|
42
|
+
::File.size(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
# gets the key
|
46
|
+
def key
|
47
|
+
@key.gsub(/^\//, '')
|
48
|
+
end
|
49
|
+
|
50
|
+
# Each temporary file is made inside a directory named with an UUID,
|
51
|
+
# instead of deleting the file directly and having the risk of deleting other files
|
52
|
+
# we delete the root of the UUID, using a UUID also remove the risk of deleting unwanted file, it acts as
|
53
|
+
# a sandbox.
|
54
|
+
def delete!
|
55
|
+
begin
|
56
|
+
@fd.close
|
57
|
+
rescue
|
58
|
+
IOError
|
59
|
+
end
|
60
|
+
FileUtils.rm_r(@temp_path, secure: true)
|
61
|
+
end
|
62
|
+
|
63
|
+
# boolean method to determine if the file is empty
|
64
|
+
def empty?
|
65
|
+
size.zero?
|
66
|
+
end
|
67
|
+
|
68
|
+
# creates the temporary file in an existing temporary directory from existing file
|
69
|
+
# @param file_path [String] path to the file
|
70
|
+
# @param temporary_folder [String] path to the temporary folder
|
71
|
+
def self.create_from_existing_file(file_path, temporary_folder)
|
72
|
+
key_parts = Pathname.new(file_path).relative_path_from(temporary_folder).to_s.split(::File::SEPARATOR)
|
73
|
+
|
74
|
+
TemporaryFile.new(key_parts.slice(1, key_parts.size).join('/'),
|
75
|
+
::File.open(file_path, 'r'),
|
76
|
+
::File.join(temporary_folder, key_parts.slice(0, 1)))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|