logstash-output-azureblob 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|