logstash-input-qingstor 0.1.3 → 0.1.5
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 +4 -4
- data/Gemfile +0 -1
- data/README.md +5 -121
- data/README_zh_CN.md +116 -0
- data/lib/logstash/inputs/qingstor.rb +103 -177
- data/lib/logstash/inputs/qingstor/log_reader.rb +53 -0
- data/lib/logstash/inputs/qingstor/qingstor_validator.rb +32 -27
- data/lib/logstash/inputs/qingstor/sincedb.rb +36 -0
- data/lib/logstash/inputs/qingstor/uploader.rb +56 -0
- data/logstash-input-qingstor.gemspec +11 -9
- data/spec/logstash/inputs/qingstor/log_reader_spec.rb +48 -0
- data/spec/logstash/inputs/qingstor/sincedb_spec.rb +27 -0
- data/spec/logstash/inputs/qingstor/uploader_spec.rb +42 -0
- data/spec/logstash/inputs/qingstor_spec.rb +98 -0
- data/spec/{inputs → logstash/inputs}/qs_access_helper.rb +12 -14
- metadata +18 -10
- data/spec/inputs/qingstor_spec.rb +0 -66
- data/spec/inputs/qingstor_spec_validator_spec.rb +0 -37
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'logstash/inputs/qingstor'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module LogStash
|
5
|
+
module Inputs
|
6
|
+
class Qingstor
|
7
|
+
# define class LogReader to read log files
|
8
|
+
class LogReader
|
9
|
+
attr_accessor :filepath
|
10
|
+
|
11
|
+
def initialize(filepath)
|
12
|
+
@filepath = filepath
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_file(&block)
|
16
|
+
if gzip?(@filepath)
|
17
|
+
read_gzip_file(block)
|
18
|
+
else
|
19
|
+
read_plain_file(block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_gzip_file(block)
|
24
|
+
Zlib::GzipReader.open(@filepath) do |decoder|
|
25
|
+
decoder.each_line { |line| block.call(line) }
|
26
|
+
end
|
27
|
+
rescue Zlib::Error, Zlib::GzipFile::Error => e
|
28
|
+
@logger.error('Gzip codec: Cannot uncompress the file',
|
29
|
+
:filepath => @filepath)
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_plain_file(block)
|
34
|
+
::File.open(@filepath, 'rb') do |file|
|
35
|
+
file.each(&block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_format?(filepath)
|
40
|
+
logger?(filepath) || gzip?(filepath)
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger?(filepath)
|
44
|
+
filepath.end_with?('.log', '.txt')
|
45
|
+
end
|
46
|
+
|
47
|
+
def gzip?(filepath)
|
48
|
+
filepath.end_with?('.gz')
|
49
|
+
end
|
50
|
+
end # class LogReader
|
51
|
+
end # class QingStor
|
52
|
+
end # module Inputs
|
53
|
+
end # module LogStash
|
@@ -1,30 +1,35 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require "fileutils"
|
1
|
+
require 'logstash/inputs/qingstor'
|
2
|
+
require 'qingstor/sdk'
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# Validator for check the avaliablity of setting in QingStor
|
5
|
+
module QingstorValidator
|
6
|
+
def bucket_valid?(bucket)
|
7
|
+
res = bucket.head
|
8
|
+
case res[:status_code]
|
9
|
+
when 401
|
10
|
+
raise LogStash::ConfigurationError,
|
11
|
+
'Incorrect key id or access key.'
|
12
|
+
when 404
|
13
|
+
raise LogStash::ConfigurationError,
|
14
|
+
'Incorrect bucket/region name.'
|
15
|
+
end
|
16
|
+
true
|
17
|
+
end
|
9
18
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
true
|
19
|
-
end
|
19
|
+
def prefix_valid?(prefix)
|
20
|
+
if prefix.start_with?('/') || prefix.length >= 1024
|
21
|
+
raise LogStash::ConfigurationError, 'Prefix must not start with '\
|
22
|
+
+ "'/' with length less than 1024"
|
23
|
+
end
|
24
|
+
true
|
25
|
+
end
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
27
|
+
def create_if_not_exist(bucket)
|
28
|
+
return if bucket.head[:status_code] == 200
|
29
|
+
res = bucket.put
|
30
|
+
if res[:status_code] != 201
|
31
|
+
@logger.error('ERROR : cannot create the bucket ', res[:message])
|
32
|
+
raise LogStash::ConfigurationError, 'cannot create the bucket'
|
33
|
+
end
|
34
|
+
end # def create_if_not_exist
|
35
|
+
end # module QingstorValidator
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'logstash/inputs/qingstor'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# module used for record the download history
|
5
|
+
module LogStash
|
6
|
+
module Inputs
|
7
|
+
class Qingstor
|
8
|
+
# define the class SinceDB::File
|
9
|
+
class SinceDB
|
10
|
+
def initialize(file)
|
11
|
+
@sincedb_path = file
|
12
|
+
end
|
13
|
+
|
14
|
+
def newer?(date)
|
15
|
+
Time.at(date) > read
|
16
|
+
end
|
17
|
+
|
18
|
+
def read
|
19
|
+
if ::File.exist?(@sincedb_path)
|
20
|
+
content = ::File.read(@sincedb_path).chomp.strip
|
21
|
+
content.empty? ? Time.new(0) : Time.parse(content)
|
22
|
+
else
|
23
|
+
Time.new(0)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(since = nil)
|
28
|
+
since = Time.now if since.nil?
|
29
|
+
dir = ::File.dirname(@sincedb_path)
|
30
|
+
FileUtils.mkdir_p(dir) unless ::File.directory?(dir)
|
31
|
+
::File.open(@sincedb_path, 'w') { |file| file.write(since.to_s) }
|
32
|
+
end
|
33
|
+
end # class FILE
|
34
|
+
end # class QingStor
|
35
|
+
end # module Inputs
|
36
|
+
end # module LogStash
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'logstash/inputs/qingstor'
|
2
|
+
require 'qingstor/sdk'
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
module LogStash
|
6
|
+
module Inputs
|
7
|
+
class Qingstor
|
8
|
+
# define class Uploader to process upload jobs
|
9
|
+
class Uploader
|
10
|
+
require 'logstash/inputs/qingstor/qingstor_validator'
|
11
|
+
include QingstorValidator
|
12
|
+
|
13
|
+
TIME_BEFORE_RETRYING_SECONDS = 1
|
14
|
+
DEFAULT_THREADPOOL = Concurrent::ThreadPoolExecutor.new(
|
15
|
+
:min_thread => 1,
|
16
|
+
:max_thread => 8,
|
17
|
+
:max_queue => 2,
|
18
|
+
:fallback_policy => :caller_runs
|
19
|
+
)
|
20
|
+
|
21
|
+
attr_reader :bucket, :prefix, :logger
|
22
|
+
|
23
|
+
def initialize(bucket, prefix, logger)
|
24
|
+
@bucket = bucket
|
25
|
+
@prefix = prefix
|
26
|
+
@logger = logger
|
27
|
+
@workers_pool = DEFAULT_THREADPOOL
|
28
|
+
end
|
29
|
+
|
30
|
+
def upload_async(filename, filepath)
|
31
|
+
@workers_pool.post do
|
32
|
+
upload(filename, filepath)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def upload(filename, filepath)
|
37
|
+
create_if_not_exist(@bucket)
|
38
|
+
file_md5 = Digest::MD5.file(filepath).to_s
|
39
|
+
key = if @prefix.end_with?('/') || @prefix.empty?
|
40
|
+
@prefix + filename
|
41
|
+
else
|
42
|
+
@prefix + '/' + filename
|
43
|
+
end
|
44
|
+
@logger.debug('uploading backup file', :file => filename)
|
45
|
+
@bucket.put_object(key, 'content_md5' => file_md5,
|
46
|
+
'body' => ::File.open(filepath))
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop
|
50
|
+
@workers_pool.shutdown
|
51
|
+
@workers_pool.wait_for_termination(nil)
|
52
|
+
end
|
53
|
+
end # class Uploader
|
54
|
+
end # class QingStor
|
55
|
+
end # module Inputs
|
56
|
+
end # module LogStash
|
@@ -1,27 +1,29 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-input-qingstor'
|
3
|
-
s.version = '0.1.
|
3
|
+
s.version = '0.1.5'
|
4
4
|
s.licenses = ['Apache License (2.0)']
|
5
5
|
s.summary = 'logstash input plugin for QingStor'
|
6
|
-
s.description = '
|
7
|
-
s.homepage = 'https://github.com/
|
6
|
+
s.description = 'Fetch file from Qingstor as the input of logstash'
|
7
|
+
s.homepage = 'https://github.com/yunify/logstash-input-qingstor'
|
8
8
|
s.authors = ['Evan Zhao']
|
9
9
|
s.email = 'tacingiht@gmail.com'
|
10
10
|
s.require_paths = ['lib']
|
11
11
|
|
12
12
|
# Files
|
13
|
-
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md',
|
14
|
-
|
13
|
+
s.files = Dir['lib/**/*', 'spec/**/*', 'vendor/**/*', '*.gemspec', '*.md',
|
14
|
+
'CONTRIBUTORS', 'Gemfile', 'LICENSE', 'NOTICE.TXT']
|
15
|
+
|
16
|
+
# Tests
|
15
17
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
16
18
|
|
17
19
|
# Special flag to let us know this is actually a logstash plugin
|
18
|
-
s.metadata = {
|
20
|
+
s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'input' }
|
19
21
|
|
20
22
|
# Gem dependencies
|
21
|
-
s.add_runtime_dependency
|
23
|
+
s.add_runtime_dependency 'logstash-core-plugin-api', '>=1.6', '<=2.99'
|
22
24
|
s.add_runtime_dependency 'logstash-codec-plain'
|
23
25
|
s.add_runtime_dependency 'stud', '>= 0.0.22'
|
24
|
-
s.add_runtime_dependency
|
26
|
+
s.add_runtime_dependency 'qingstor-sdk', '>=1.9.2'
|
25
27
|
|
26
28
|
s.add_development_dependency 'logstash-devutils'
|
27
|
-
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'logstash/devutils/rspec/spec_helper'
|
2
|
+
require 'logstash/inputs/qingstor/log_reader'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
describe LogStash::Inputs::Qingstor::LogReader do
|
6
|
+
subject(:log_reader) { described_class.new('/a/example/path') }
|
7
|
+
|
8
|
+
let(:content) { 'may the code be with you!' }
|
9
|
+
let(:plain_file_path) { File.join(Dir.tmpdir, 'plain.log') }
|
10
|
+
let(:gzip_file_path) { File.join(Dir.tmpdir, 'gzip.gz') }
|
11
|
+
let(:invalid_file_path) { File.join(Dir.tmpdir, 'invalid.ivd') }
|
12
|
+
|
13
|
+
context 'when read plain file' do
|
14
|
+
before do
|
15
|
+
File.open(plain_file_path, 'w') do |f|
|
16
|
+
f.write(content)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it do
|
21
|
+
log_reader.filepath = plain_file_path
|
22
|
+
log_reader.read_file do |f|
|
23
|
+
expect(f).to eq(content)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when read gzip file' do
|
29
|
+
before do
|
30
|
+
Zlib::GzipWriter.open(gzip_file_path) do |gz|
|
31
|
+
gz.write(content)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it do
|
36
|
+
log_reader.filepath = gzip_file_path
|
37
|
+
log_reader.read_file do |f|
|
38
|
+
expect(f).to eq(content)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when valid format' do
|
44
|
+
it { expect(log_reader.valid_format?(plain_file_path)).to be_truthy }
|
45
|
+
it { expect(log_reader.valid_format?(gzip_file_path)).to be_truthy }
|
46
|
+
it { expect(log_reader.valid_format?(invalid_file_path)).to be_falsey }
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logstash/devutils/rspec/spec_helper'
|
2
|
+
require 'logstash/inputs/qingstor/sincedb'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
describe LogStash::Inputs::Qingstor::SinceDB do
|
6
|
+
subject(:sincedb) { described_class.new(sincedb_path) }
|
7
|
+
|
8
|
+
let(:sincedb_path) { File.join(Dir.tmpdir, 'log_tmp_dir/log_tmp.log') }
|
9
|
+
|
10
|
+
context 'when run at first time' do
|
11
|
+
before do
|
12
|
+
File.delete(sincedb_path) if File.exist?(sincedb_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
it { expect(sincedb.read).to eq(Time.new(0)) }
|
16
|
+
it { expect(sincedb.newer?(Time.now)).to be_truthy }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when write the record' do
|
20
|
+
it do
|
21
|
+
time = Time.now
|
22
|
+
sincedb.write(time)
|
23
|
+
content = File.read(sincedb_path).chomp.strip
|
24
|
+
expect(content).to eq(time.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'logstash/devutils/rspec/spec_helper'
|
2
|
+
require 'logstash/inputs/qingstor/uploader'
|
3
|
+
require 'qingstor/sdk'
|
4
|
+
require 'stud/temporary'
|
5
|
+
require_relative '../qs_access_helper'
|
6
|
+
|
7
|
+
describe LogStash::Inputs::Qingstor::Uploader do
|
8
|
+
let(:bucket) { qs_init_bucket }
|
9
|
+
let(:new_bucket) { qs_init_bucket }
|
10
|
+
let(:key) { 'foobar' }
|
11
|
+
let(:file) { Stud::Temporary.file }
|
12
|
+
let(:filepath) { file.path }
|
13
|
+
let(:logger) { spy(:logger) }
|
14
|
+
|
15
|
+
context 'when upload file' do
|
16
|
+
let(:prefix) { '' }
|
17
|
+
|
18
|
+
after do
|
19
|
+
delete_remote_file(prefix + key)
|
20
|
+
end
|
21
|
+
|
22
|
+
it do
|
23
|
+
uploader = described_class.new(bucket, prefix, logger)
|
24
|
+
uploader.upload(key, filepath)
|
25
|
+
expect(list_remote_file.size).to eq(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when upload file with a prefix' do
|
30
|
+
let(:prefix) { 'a/prefix/' }
|
31
|
+
|
32
|
+
after do
|
33
|
+
delete_remote_file(prefix + key)
|
34
|
+
end
|
35
|
+
|
36
|
+
it do
|
37
|
+
uploader = described_class.new(bucket, prefix, logger)
|
38
|
+
uploader.upload(key, filepath)
|
39
|
+
expect(list_remote_file.size).to eq(1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'logstash/devutils/rspec/spec_helper'
|
2
|
+
require 'logstash/inputs/qingstor'
|
3
|
+
require_relative './qs_access_helper'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
describe LogStash::Inputs::Qingstor do
|
7
|
+
before do
|
8
|
+
Thread.abort_on_exception = true
|
9
|
+
|
10
|
+
upload_file('../../fixtures/logstash.log', 'log3.log')
|
11
|
+
upload_file('../../fixtures/logstash.log.gz', 'log3.log.gz')
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
delete_remote_file 'log3.log'
|
16
|
+
delete_remote_file 'log3.log.gz'
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:config) do
|
20
|
+
{ 'access_key_id' => ENV['access_key_id'],
|
21
|
+
'secret_access_key' => ENV['secret_access_key'],
|
22
|
+
'bucket' => ENV['bucket'],
|
23
|
+
'region' => ENV['region'] }
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:key1) { 'log3.log' }
|
27
|
+
let(:key2) { 'log3.log.gz' }
|
28
|
+
let(:backup) { 'evamax' }
|
29
|
+
let(:local_backup_dir) { File.join(Dir.tmpdir, backup) }
|
30
|
+
|
31
|
+
context 'when at the local' do
|
32
|
+
it 'backup to local dir' do
|
33
|
+
fetch_events(config.merge('backup_local_dir' => local_backup_dir))
|
34
|
+
expect(File.exist?(File.join(local_backup_dir, key1))).to be_truthy
|
35
|
+
expect(File.exist?(File.join(local_backup_dir, key2))).to be_truthy
|
36
|
+
end
|
37
|
+
|
38
|
+
after do
|
39
|
+
FileUtils.rm_r(File.join(local_backup_dir, key1))
|
40
|
+
FileUtils.rm_r(File.join(local_backup_dir, key2))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'when backup to the remote end' do
|
45
|
+
it do
|
46
|
+
fetch_events(config.merge('backup_bucket' => backup))
|
47
|
+
expect(list_remote_file(backup).size).to eq(2)
|
48
|
+
end
|
49
|
+
|
50
|
+
after do
|
51
|
+
clean_and_delete_bucket(backup)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when test host redirection' do
|
56
|
+
it 'redirect without a port number' do
|
57
|
+
expect { fetch_events(config.merge('host' => 'qingstor.dev')) }
|
58
|
+
.to raise_error(Net::HTTP::Persistent::Error)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'redirect with a port number' do
|
62
|
+
new_config = config.merge('host' => 'qingstor.dev', 'port' => 444)
|
63
|
+
expect { fetch_events(new_config) }
|
64
|
+
.to raise_error(Net::HTTP::Persistent::Error)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when test with various config values' do
|
69
|
+
it do
|
70
|
+
config['access_key_id'] = 'wrongid'
|
71
|
+
expect { described_class.new(config).register }
|
72
|
+
.to raise_error(LogStash::ConfigurationError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it do
|
76
|
+
config['secret_access_key'] = 'wrongaccesskey'
|
77
|
+
expect { described_class.new(config).register }
|
78
|
+
.to raise_error(LogStash::ConfigurationError)
|
79
|
+
end
|
80
|
+
|
81
|
+
it do
|
82
|
+
config['bucket'] = 'wrongbucket'
|
83
|
+
expect { described_class.new(config).register }
|
84
|
+
.to raise_error(LogStash::ConfigurationError)
|
85
|
+
end
|
86
|
+
|
87
|
+
it do
|
88
|
+
config['region'] = 'wrongregion'
|
89
|
+
expect { described_class.new(config).register }
|
90
|
+
.to raise_error(LogStash::ConfigurationError)
|
91
|
+
end
|
92
|
+
|
93
|
+
it do
|
94
|
+
config.delete('region')
|
95
|
+
expect(described_class.new(config).register).to be_truthy
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|