logstash-output-azure 0.3.0 → 1.0.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 +5 -5
- data/CHANGELOG.md +4 -1
- data/CONTRIBUTORS +1 -0
- data/lib/logstash/outputs/azure.rb +59 -81
- data/lib/logstash/outputs/blob/file_repository.rb +33 -18
- data/lib/logstash/outputs/blob/path_validator.rb +3 -3
- data/lib/logstash/outputs/blob/size_and_time_rotation_policy.rb +6 -4
- data/lib/logstash/outputs/blob/size_rotation_policy.rb +5 -4
- data/lib/logstash/outputs/blob/temporary_file.rb +28 -19
- data/lib/logstash/outputs/blob/temporary_file_factory.rb +28 -16
- data/lib/logstash/outputs/blob/time_rotation_policy.rb +5 -4
- data/lib/logstash/outputs/blob/uploader.rb +29 -22
- data/lib/logstash/outputs/blob/writable_directory_validator.rb +6 -7
- data/logstash-output-azure.gemspec +10 -10
- data/spec/outputs/azure_spec.rb +16 -18
- data/spec/outputs/blob/file_repository_spec.rb +35 -38
- data/spec/outputs/blob/size_and_time_rotation_policy_spec.rb +20 -21
- data/spec/outputs/blob/size_rotation_policy_spec.rb +13 -15
- data/spec/outputs/blob/temporary_file_factory_spec.rb +27 -28
- data/spec/outputs/blob/temporary_file_spec.rb +14 -15
- data/spec/outputs/blob/time_rotation_policy_spec.rb +17 -18
- data/spec/outputs/blob/uploader_spec.rb +28 -32
- data/spec/outputs/blob/writable_directory_validator_spec.rb +8 -9
- data/spec/spec_helper.rb +4 -5
- data/spec/supports/helpers.rb +12 -15
- metadata +9 -9
@@ -1,16 +1,16 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
module LogStash
|
3
2
|
module Outputs
|
4
3
|
class LogstashAzureBlobOutput
|
5
4
|
# a sub class of +LogstashAzureBlobOutput+
|
6
5
|
# valdiates the path for the temporary directory
|
7
6
|
class PathValidator
|
8
|
-
INVALID_CHARACTERS = "\^`><"
|
9
|
-
|
7
|
+
INVALID_CHARACTERS = "\^`><".freeze
|
8
|
+
# boolean method to check if a name is valid
|
10
9
|
def self.valid?(name)
|
11
10
|
name.match(matches_re).nil?
|
12
11
|
end
|
13
12
|
|
13
|
+
# define the invalid characters that shouldn't be in the path name
|
14
14
|
def self.matches_re
|
15
15
|
/[#{Regexp.escape(INVALID_CHARACTERS)}]/
|
16
16
|
end
|
@@ -1,22 +1,24 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require "logstash/outputs/blob/time_rotation_policy"
|
1
|
+
require 'logstash/outputs/blob/size_rotation_policy'
|
2
|
+
require 'logstash/outputs/blob/time_rotation_policy'
|
4
3
|
|
5
4
|
module LogStash
|
6
5
|
module Outputs
|
7
6
|
class LogstashAzureBlobOutput
|
8
7
|
# a sub class of +LogstashAzureBlobOutput+
|
9
|
-
# sets the rotation policy
|
8
|
+
# sets the rotation policy
|
10
9
|
class SizeAndTimeRotationPolicy
|
10
|
+
# initialize the class
|
11
11
|
def initialize(file_size, time_file)
|
12
12
|
@size_strategy = SizeRotationPolicy.new(file_size)
|
13
13
|
@time_strategy = TimeRotationPolicy.new(time_file)
|
14
14
|
end
|
15
15
|
|
16
|
+
# check if it is time to rotate
|
16
17
|
def rotate?(file)
|
17
18
|
@size_strategy.rotate?(file) || @time_strategy.rotate?(file)
|
18
19
|
end
|
19
20
|
|
21
|
+
# boolean method
|
20
22
|
def needs_periodic?
|
21
23
|
true
|
22
24
|
end
|
@@ -1,24 +1,25 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
module LogStash
|
3
2
|
module Outputs
|
4
3
|
class LogstashAzureBlobOutput
|
5
4
|
# a sub class of +LogstashAzureBlobOutput+
|
6
|
-
# sets the rotation policy by size
|
5
|
+
# sets the rotation policy by size
|
7
6
|
class SizeRotationPolicy
|
8
7
|
attr_reader :size_file
|
9
|
-
|
8
|
+
# initialize the class
|
10
9
|
def initialize(size_file)
|
11
10
|
if size_file <= 0
|
12
|
-
raise LogStash::ConfigurationError
|
11
|
+
raise LogStash::ConfigurationError.new('`size_file` need to be greather than 0')
|
13
12
|
end
|
14
13
|
|
15
14
|
@size_file = size_file
|
16
15
|
end
|
17
16
|
|
17
|
+
# boolean method to check if it is time to rotate
|
18
18
|
def rotate?(file)
|
19
19
|
file.size >= size_file
|
20
20
|
end
|
21
21
|
|
22
|
+
# boolean method
|
22
23
|
def needs_periodic?
|
23
24
|
false
|
24
25
|
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require "fileutils"
|
1
|
+
require 'thread'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'fileutils'
|
5
4
|
|
6
5
|
module LogStash
|
7
6
|
module Outputs
|
@@ -16,6 +15,7 @@ module LogStash
|
|
16
15
|
|
17
16
|
attr_reader :fd
|
18
17
|
|
18
|
+
# initialize the class
|
19
19
|
def initialize(key, fd, temp_path)
|
20
20
|
@fd = fd
|
21
21
|
@key = key
|
@@ -23,27 +23,28 @@ module LogStash
|
|
23
23
|
@created_at = Time.now
|
24
24
|
end
|
25
25
|
|
26
|
+
# gets the created at time
|
26
27
|
def ctime
|
27
28
|
@created_at
|
28
29
|
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
end
|
31
|
+
# gets path to temporary directory
|
32
|
+
attr_reader :temp_path
|
33
33
|
|
34
|
+
# gets the size of file
|
34
35
|
def size
|
35
36
|
# Use the fd size to get the accurate result,
|
36
37
|
# so we dont have to deal with fsync
|
37
38
|
# if the file is close we will use the File::size
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
39
|
+
|
40
|
+
@fd.size
|
41
|
+
rescue IOError
|
42
|
+
::File.size(path)
|
43
43
|
end
|
44
44
|
|
45
|
+
# gets the key
|
45
46
|
def key
|
46
|
-
@key.gsub(/^\//,
|
47
|
+
@key.gsub(/^\//, '')
|
47
48
|
end
|
48
49
|
|
49
50
|
# Each temporary file is made inside a directory named with an UUID,
|
@@ -51,20 +52,28 @@ module LogStash
|
|
51
52
|
# we delete the root of the UUID, using a UUID also remove the risk of deleting unwanted file, it acts as
|
52
53
|
# a sandbox.
|
53
54
|
def delete!
|
54
|
-
|
55
|
-
|
55
|
+
begin
|
56
|
+
@fd.close
|
57
|
+
rescue
|
58
|
+
IOError
|
59
|
+
end
|
60
|
+
FileUtils.rm_r(@temp_path, secure: true)
|
56
61
|
end
|
57
62
|
|
63
|
+
# boolean method to determine if the file is empty
|
58
64
|
def empty?
|
59
|
-
size
|
65
|
+
size.zero?
|
60
66
|
end
|
61
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
|
62
71
|
def self.create_from_existing_file(file_path, temporary_folder)
|
63
72
|
key_parts = Pathname.new(file_path).relative_path_from(temporary_folder).to_s.split(::File::SEPARATOR)
|
64
73
|
|
65
|
-
TemporaryFile.new(key_parts.slice(1, key_parts.size).join(
|
66
|
-
|
67
|
-
|
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)))
|
68
77
|
end
|
69
78
|
end
|
70
79
|
end
|
@@ -1,9 +1,8 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require "forwardable"
|
1
|
+
require 'socket'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'zlib'
|
5
|
+
require 'forwardable'
|
7
6
|
|
8
7
|
module LogStash
|
9
8
|
module Outputs
|
@@ -11,14 +10,15 @@ module LogStash
|
|
11
10
|
# a sub class of +LogstashAzureBlobOutput+
|
12
11
|
# creates the temporary files to write and later upload
|
13
12
|
class TemporaryFileFactory
|
14
|
-
FILE_MODE =
|
15
|
-
GZIP_ENCODING =
|
16
|
-
GZIP_EXTENSION =
|
17
|
-
TXT_EXTENSION =
|
18
|
-
STRFTIME =
|
13
|
+
FILE_MODE = 'a'.freeze
|
14
|
+
GZIP_ENCODING = 'gzip'.freeze
|
15
|
+
GZIP_EXTENSION = 'txt.gz'.freeze
|
16
|
+
TXT_EXTENSION = 'txt'.freeze
|
17
|
+
STRFTIME = '%Y-%m-%dT%H.%M'.freeze
|
19
18
|
|
20
19
|
attr_accessor :counter, :tags, :prefix, :encoding, :temporary_directory, :current
|
21
20
|
|
21
|
+
# initialize the class
|
22
22
|
def initialize(prefix, tags, encoding, temporary_directory)
|
23
23
|
@counter = 0
|
24
24
|
@prefix = prefix
|
@@ -31,41 +31,49 @@ module LogStash
|
|
31
31
|
rotate!
|
32
32
|
end
|
33
33
|
|
34
|
+
# do the rotation
|
34
35
|
def rotate!
|
35
|
-
@lock.synchronize
|
36
|
+
@lock.synchronize do
|
36
37
|
@current = new_file
|
37
38
|
increment_counter
|
38
39
|
@current
|
39
|
-
|
40
|
+
end
|
40
41
|
end
|
41
42
|
|
42
43
|
private
|
44
|
+
|
45
|
+
# if it is not gzip ecoding, then it is txt extension
|
43
46
|
def extension
|
44
47
|
gzip? ? GZIP_EXTENSION : TXT_EXTENSION
|
45
48
|
end
|
46
49
|
|
50
|
+
# boolean method to check if its gzip encoding
|
47
51
|
def gzip?
|
48
52
|
encoding == GZIP_ENCODING
|
49
53
|
end
|
50
54
|
|
55
|
+
# increment the counter in 1 unit
|
51
56
|
def increment_counter
|
52
57
|
@counter += 1
|
53
58
|
end
|
54
59
|
|
60
|
+
# gets the current time
|
55
61
|
def current_time
|
56
62
|
Time.now.strftime(STRFTIME)
|
57
63
|
end
|
58
64
|
|
65
|
+
# method that generate the name of the file to be saved in blob storage
|
59
66
|
def generate_name
|
60
67
|
filename = "#{current_time}.#{SecureRandom.uuid}"
|
61
68
|
|
62
|
-
if tags.
|
69
|
+
if !tags.empty?
|
63
70
|
"#{filename}.tag_#{tags.join('.')}.part#{counter}.#{extension}"
|
64
71
|
else
|
65
72
|
"#{filename}.part#{counter}.#{extension}"
|
66
73
|
end
|
67
74
|
end
|
68
75
|
|
76
|
+
# create the file to be saved in blob storage
|
69
77
|
def new_file
|
70
78
|
uuid = SecureRandom.uuid
|
71
79
|
name = generate_name
|
@@ -85,25 +93,28 @@ module LogStash
|
|
85
93
|
TemporaryFile.new(key, io, path)
|
86
94
|
end
|
87
95
|
|
88
|
-
# clas for the
|
96
|
+
# clas for the encoding
|
89
97
|
class IOWrappedGzip
|
90
98
|
extend Forwardable
|
91
99
|
|
92
100
|
def_delegators :@gzip_writer, :write, :close
|
93
101
|
attr_reader :file_io, :gzip_writer
|
94
102
|
|
103
|
+
# initialize the class for encoding
|
95
104
|
def initialize(file_io)
|
96
105
|
@file_io = file_io
|
97
106
|
@gzip_writer = Zlib::GzipWriter.open(file_io)
|
98
107
|
end
|
99
108
|
|
109
|
+
# gets the path
|
100
110
|
def path
|
101
111
|
@gzip_writer.to_io.path
|
102
112
|
end
|
103
113
|
|
114
|
+
# gets the file size
|
104
115
|
def size
|
105
116
|
# to get the current file size
|
106
|
-
if @gzip_writer.pos
|
117
|
+
if @gzip_writer.pos.zero?
|
107
118
|
# Ensure a zero file size is returned when nothing has
|
108
119
|
# yet been written to the gzip file.
|
109
120
|
0
|
@@ -113,6 +124,7 @@ module LogStash
|
|
113
124
|
end
|
114
125
|
end
|
115
126
|
|
127
|
+
# gets the fsync
|
116
128
|
def fsync
|
117
129
|
@gzip_writer.to_io.fsync
|
118
130
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
module LogStash
|
3
2
|
module Outputs
|
4
3
|
class LogstashAzureBlobOutput
|
@@ -6,19 +5,21 @@ module LogStash
|
|
6
5
|
# sets the policy for time rotation
|
7
6
|
class TimeRotationPolicy
|
8
7
|
attr_reader :time_file
|
9
|
-
|
8
|
+
# initialize the class and validate the time file
|
10
9
|
def initialize(time_file)
|
11
10
|
if time_file <= 0
|
12
|
-
raise LogStash::ConfigurationError
|
11
|
+
raise LogStash::ConfigurationError.new('`time_file` need to be greather than 0')
|
13
12
|
end
|
14
13
|
|
15
14
|
@time_file = time_file * 60
|
16
15
|
end
|
17
16
|
|
17
|
+
# rotates based on time policy
|
18
18
|
def rotate?(file)
|
19
|
-
file.
|
19
|
+
!file.empty? && (Time.now - file.ctime) >= time_file
|
20
20
|
end
|
21
21
|
|
22
|
+
# boolean method
|
22
23
|
def needs_periodic?
|
23
24
|
true
|
24
25
|
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require "azure"
|
1
|
+
require 'logstash/util'
|
2
|
+
require 'azure'
|
4
3
|
|
5
4
|
module LogStash
|
6
5
|
module Outputs
|
@@ -9,22 +8,24 @@ module LogStash
|
|
9
8
|
# this class uploads the files to Azure cloud
|
10
9
|
class Uploader
|
11
10
|
TIME_BEFORE_RETRYING_SECONDS = 1
|
12
|
-
DEFAULT_THREADPOOL = Concurrent::ThreadPoolExecutor.new(
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
11
|
+
DEFAULT_THREADPOOL = Concurrent::ThreadPoolExecutor.new(min_threads: 1,
|
12
|
+
max_threads: 8,
|
13
|
+
max_queue: 1,
|
14
|
+
fallback_policy: :caller_runs)
|
15
|
+
|
16
|
+
attr_accessor :upload_options, :logger, :container_name, :blob_account
|
17
|
+
|
18
|
+
# Initializes the class
|
19
|
+
# @param blob_account [Object] endpoint to azure gem
|
20
|
+
# @param container_name [String] name of the container in azure blob, at this point, if it doesn't exist, it was already created
|
21
|
+
def initialize(blob_account, container_name, logger, threadpool = DEFAULT_THREADPOOL)
|
22
22
|
@blob_account = blob_account
|
23
23
|
@workers_pool = threadpool
|
24
24
|
@logger = logger
|
25
|
-
@container_name = container_name
|
26
|
-
|
25
|
+
@container_name = container_name
|
26
|
+
end
|
27
27
|
|
28
|
+
# Create threads to upload the file to the container
|
28
29
|
def upload_async(file, options = {})
|
29
30
|
@workers_pool.post do
|
30
31
|
LogStash::Util.set_thread_name("LogstashAzureBlobOutput output uploader, file: #{file.path}")
|
@@ -32,32 +33,38 @@ module LogStash
|
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
36
|
+
# Uploads the file to the container
|
35
37
|
def upload(file, options = {})
|
36
38
|
upload_options = options.fetch(:upload_options, {})
|
37
39
|
|
38
40
|
begin
|
39
|
-
content = Object::File.open(file.path,
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
content = Object::File.open(file.path, 'rb').read
|
42
|
+
filename = Object::File.basename file.path
|
43
|
+
puts filename
|
44
|
+
blob = blob_account.create_block_blob(container_name, filename, content)
|
45
|
+
puts blob.name
|
44
46
|
rescue => e
|
45
47
|
# When we get here it usually mean that LogstashAzureBlobOutput tried to do some retry by himself (default is 3)
|
46
48
|
# When the retry limit is reached or another error happen we will wait and retry.
|
47
49
|
#
|
48
50
|
# Thread might be stuck here, but I think its better than losing anything
|
49
51
|
# its either a transient errors or something bad really happened.
|
50
|
-
logger.error(
|
52
|
+
logger.error('Uploading failed, retrying', exception: e.class, message: e.message, path: file.path, backtrace: e.backtrace)
|
51
53
|
retry
|
52
54
|
end
|
53
55
|
|
54
56
|
options[:on_complete].call(file) unless options[:on_complete].nil?
|
55
57
|
blob
|
56
58
|
rescue => e
|
57
|
-
logger.error(
|
59
|
+
logger.error('An error occured in the `on_complete` uploader',
|
60
|
+
exception: e.class,
|
61
|
+
message: e.message,
|
62
|
+
path: file.path,
|
63
|
+
backtrace: e.backtrace)
|
58
64
|
raise e # reraise it since we don't deal with it now
|
59
65
|
end
|
60
66
|
|
67
|
+
# stop threads
|
61
68
|
def stop
|
62
69
|
@workers_pool.shutdown
|
63
70
|
@workers_pool.wait_for_termination(nil) # block until its done
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
module LogStash
|
3
2
|
module Outputs
|
4
3
|
class LogstashAzureBlobOutput
|
@@ -6,13 +5,13 @@ module LogStash
|
|
6
5
|
# validates that the specified tmeporary directory can be accesed with
|
7
6
|
# write permission
|
8
7
|
class WritableDirectoryValidator
|
8
|
+
# Checks if a path is valid
|
9
|
+
# @param path [String] String that represents the path
|
9
10
|
def self.valid?(path)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
false
|
15
|
-
end
|
11
|
+
FileUtils.mkdir_p(path) unless Dir.exist?(path)
|
12
|
+
::File.writable?(path)
|
13
|
+
rescue
|
14
|
+
false
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|
@@ -1,25 +1,25 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-output-azure'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '1.0.0'
|
4
4
|
s.licenses = ['Apache-2.0']
|
5
5
|
s.summary = 'Plugin for logstash to send output to Microsoft Azure Blob'
|
6
|
-
#s.description = 'TODO: Write a longer description or delete this line.'
|
7
|
-
#s.homepage = 'TODO: Put your plugin''s website or public repo URL here.'
|
6
|
+
# s.description = 'TODO: Write a longer description or delete this line.'
|
7
|
+
# s.homepage = 'TODO: Put your plugin''s website or public repo URL here.'
|
8
8
|
s.authors = ['Tuffk']
|
9
9
|
s.email = 'tuffkmulhall@gmail.com'
|
10
10
|
s.require_paths = ['lib']
|
11
11
|
|
12
12
|
# Files
|
13
|
-
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
|
14
|
-
|
13
|
+
s.files = Dir['lib/**/*', 'spec/**/*', 'vendor/**/*', '*.gemspec', '*.md', 'CONTRIBUTORS', 'Gemfile', 'LICENSE', 'NOTICE.TXT']
|
14
|
+
# Tests
|
15
15
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
16
|
|
17
17
|
# Special flag to let us know this is actually a logstash plugin
|
18
|
-
s.metadata = {
|
18
|
+
s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'output' }
|
19
19
|
|
20
20
|
# Gem dependencies
|
21
|
-
s.add_runtime_dependency
|
22
|
-
s.add_runtime_dependency
|
23
|
-
s.add_runtime_dependency
|
24
|
-
s.add_development_dependency
|
21
|
+
s.add_runtime_dependency 'azure', '~> 0.7'
|
22
|
+
s.add_runtime_dependency 'logstash-codec-plain'
|
23
|
+
s.add_runtime_dependency 'logstash-core-plugin-api', '~> 2.0'
|
24
|
+
s.add_development_dependency 'logstash-devutils'
|
25
25
|
end
|