logstash-logger 0.26.1 → 1.1.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 +4 -4
- data/.github/workflows/tests.yml +53 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +85 -105
- data/Appraisals +6 -20
- data/CHANGELOG.md +61 -0
- data/Gemfile +2 -2
- data/README.md +12 -833
- data/docs/buffering.md +86 -0
- data/docs/customization.md +86 -0
- data/docs/outputs.md +65 -0
- data/docs/rails.md +349 -0
- data/docs/ssl.md +102 -0
- data/docs/troubleshooting.md +106 -0
- data/docs/usage.md +148 -0
- data/gemfiles/rails_8.0.gemfile +10 -0
- data/gemfiles/rails_8.1.gemfile +9 -0
- data/lib/logstash-logger/buffer.rb +15 -5
- data/lib/logstash-logger/configuration.rb +1 -2
- data/lib/logstash-logger/device/aws_stream.rb +96 -17
- data/lib/logstash-logger/device/base.rb +2 -2
- data/lib/logstash-logger/device/connectable.rb +30 -12
- data/lib/logstash-logger/device/file.rb +21 -4
- data/lib/logstash-logger/device/firehose.rb +6 -0
- data/lib/logstash-logger/device/http.rb +104 -0
- data/lib/logstash-logger/device/kafka.rb +182 -38
- data/lib/logstash-logger/device/kinesis.rb +10 -1
- data/lib/logstash-logger/device/redis.rb +8 -1
- data/lib/logstash-logger/device/tcp.rb +54 -12
- data/lib/logstash-logger/device.rb +24 -2
- data/lib/logstash-logger/formatter/base.rb +97 -11
- data/lib/logstash-logger/formatter/cee_syslog.rb +8 -11
- data/lib/logstash-logger/formatter/json.rb +13 -0
- data/lib/logstash-logger/formatter/json_lines.rb +13 -0
- data/lib/logstash-logger/formatter.rb +14 -6
- data/lib/logstash-logger/logger.rb +6 -19
- data/lib/logstash-logger/multi_logger.rb +2 -1
- data/lib/logstash-logger/railtie.rb +1 -1
- data/lib/logstash-logger/tagged_logging.rb +3 -1
- data/lib/logstash-logger/version.rb +1 -1
- data/logstash-logger.gemspec +8 -4
- data/spec/device/aws_stream_limits_spec.rb +219 -0
- data/spec/device/connectable_spec.rb +51 -0
- data/spec/device/file_spec.rb +65 -0
- data/spec/device/firehose_spec.rb +4 -1
- data/spec/device/http_spec.rb +176 -0
- data/spec/device/kafka_spec.rb +429 -14
- data/spec/device/kinesis_spec.rb +4 -1
- data/spec/device/tcp_spec.rb +94 -5
- data/spec/device/tcp_tls_integration_spec.rb +91 -0
- data/spec/device_spec.rb +13 -0
- data/spec/formatter/base_spec.rb +82 -1
- data/spec/formatter/cee_syslog_spec.rb +71 -19
- data/spec/formatter/framing_spec.rb +58 -0
- data/spec/formatter/json_lines_spec.rb +41 -0
- data/spec/formatter/json_spec.rb +49 -0
- data/spec/formatter_spec.rb +19 -2
- data/spec/logger_spec.rb +5 -5
- data/spec/multi_logger_spec.rb +16 -0
- data/spec/spec_helper.rb +7 -5
- data/spec/support/tls_helper.rb +30 -0
- data/spec/support/unserializable_value.rb +10 -0
- data/spec/tagged_logging_spec.rb +15 -0
- metadata +101 -16
- data/.travis.yml +0 -26
- data/gemfiles/rails_3.2.gemfile +0 -9
- data/gemfiles/rails_4.0.gemfile +0 -9
- data/gemfiles/rails_4.1.gemfile +0 -9
- data/gemfiles/rails_4.2.gemfile +0 -9
- data/gemfiles/rails_5.0.gemfile +0 -9
- data/gemfiles/rails_5.1.gemfile +0 -9
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
module LogStashLogger
|
|
2
2
|
module Formatter
|
|
3
3
|
class CeeSyslog < Cee
|
|
4
|
-
def call(severity, time, progname, message)
|
|
5
|
-
@progname = progname
|
|
6
|
-
super
|
|
7
|
-
end
|
|
8
|
-
|
|
9
4
|
private
|
|
10
5
|
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
facility << " #{@progname}" if @progname
|
|
14
|
-
facility
|
|
6
|
+
def frame_event(event, progname)
|
|
7
|
+
"#{build_header(progname)}:#{format_event(event)}\n"
|
|
15
8
|
end
|
|
16
9
|
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
# The header names this process, never a value taken from the logged
|
|
11
|
+
# content, and always stays on one line.
|
|
12
|
+
def build_header(progname)
|
|
13
|
+
header = HOST['hostname'.freeze].to_s.dup
|
|
14
|
+
header << " #{progname}" if progname
|
|
15
|
+
header.gsub(/[[:cntrl:]]/, ' '.freeze)
|
|
19
16
|
end
|
|
20
17
|
end
|
|
21
18
|
end
|
|
@@ -4,7 +4,20 @@ module LogStashLogger
|
|
|
4
4
|
private
|
|
5
5
|
|
|
6
6
|
def format_event(event)
|
|
7
|
+
# Proactively check for encoding issues to handle cross-platform differences.
|
|
8
|
+
# Some Ruby implementations (e.g., JRuby) may not raise exceptions during
|
|
9
|
+
# JSON encoding but produce malformed output instead.
|
|
10
|
+
if message_has_encoding_issue?(event)
|
|
11
|
+
log_error(Encoding::InvalidByteSequenceError.new("Invalid encoding in message"))
|
|
12
|
+
return force_utf8_encoding(event).to_json
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
event.to_json
|
|
16
|
+
rescue Encoding::UndefinedConversionError,
|
|
17
|
+
Encoding::InvalidByteSequenceError,
|
|
18
|
+
JSON::GeneratorError => e
|
|
19
|
+
log_error(e)
|
|
20
|
+
force_utf8_encoding(event).to_json
|
|
8
21
|
end
|
|
9
22
|
end
|
|
10
23
|
end
|
|
@@ -4,7 +4,20 @@ module LogStashLogger
|
|
|
4
4
|
private
|
|
5
5
|
|
|
6
6
|
def format_event(event)
|
|
7
|
+
# Proactively check for encoding issues to handle cross-platform differences.
|
|
8
|
+
# Some Ruby implementations (e.g., JRuby) may not raise exceptions during
|
|
9
|
+
# JSON encoding but produce malformed output instead.
|
|
10
|
+
if message_has_encoding_issue?(event)
|
|
11
|
+
log_error(Encoding::InvalidByteSequenceError.new("Invalid encoding in message"))
|
|
12
|
+
return "#{force_utf8_encoding(event).to_json}\n"
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
"#{event.to_json}\n"
|
|
16
|
+
rescue Encoding::UndefinedConversionError,
|
|
17
|
+
Encoding::InvalidByteSequenceError,
|
|
18
|
+
JSON::GeneratorError => e
|
|
19
|
+
log_error(e)
|
|
20
|
+
"#{force_utf8_encoding(event).to_json}\n"
|
|
8
21
|
end
|
|
9
22
|
end
|
|
10
23
|
end
|
|
@@ -10,20 +10,20 @@ module LogStashLogger
|
|
|
10
10
|
autoload :Cee, 'logstash-logger/formatter/cee'
|
|
11
11
|
autoload :CeeSyslog, 'logstash-logger/formatter/cee_syslog'
|
|
12
12
|
|
|
13
|
-
def self.new(formatter_type, customize_event: nil)
|
|
14
|
-
build_formatter(formatter_type, customize_event)
|
|
13
|
+
def self.new(formatter_type, customize_event: nil, error_logger: LogStashLogger.configuration.default_error_logger)
|
|
14
|
+
build_formatter(formatter_type, customize_event, error_logger)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def self.build_formatter(formatter_type, customize_event)
|
|
17
|
+
def self.build_formatter(formatter_type, customize_event, error_logger)
|
|
18
18
|
formatter_type ||= DEFAULT_FORMATTER
|
|
19
19
|
|
|
20
20
|
formatter = if custom_formatter_instance?(formatter_type)
|
|
21
21
|
formatter_type
|
|
22
22
|
elsif custom_formatter_class?(formatter_type)
|
|
23
|
-
formatter_type
|
|
23
|
+
initialize_custom_formatter_klass(formatter_type, customize_event)
|
|
24
24
|
else
|
|
25
|
-
formatter_klass(formatter_type).new(customize_event: customize_event)
|
|
26
|
-
|
|
25
|
+
formatter_klass(formatter_type).new(customize_event: customize_event, error_logger: error_logger)
|
|
26
|
+
end
|
|
27
27
|
|
|
28
28
|
formatter.send(:extend, ::LogStashLogger::TaggedLogging::Formatter)
|
|
29
29
|
formatter
|
|
@@ -47,5 +47,13 @@ module LogStashLogger
|
|
|
47
47
|
def self.custom_formatter_class?(formatter_type)
|
|
48
48
|
formatter_type.is_a?(Class) && formatter_type.method_defined?(:call)
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
def self.initialize_custom_formatter_klass(formatter_klass, customize_event)
|
|
52
|
+
if formatter_klass.instance_method(:initialize).parameters.include?([:key, :customize_event])
|
|
53
|
+
formatter_klass.new(customize_event: customize_event)
|
|
54
|
+
else
|
|
55
|
+
formatter_klass.new
|
|
56
|
+
end
|
|
57
|
+
end
|
|
50
58
|
end
|
|
51
59
|
end
|
|
@@ -26,31 +26,19 @@ module LogStashLogger
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
protected
|
|
30
29
|
|
|
31
30
|
def self.extract_opts(*args)
|
|
32
|
-
args.
|
|
33
|
-
|
|
34
|
-
if args.length > 1
|
|
35
|
-
if args.all?{|arg| arg.is_a?(Hash)}
|
|
36
|
-
# Deprecated array of hashes
|
|
37
|
-
warn "[LogStashLogger] Passing an array of hashes to the constructor is deprecated. Please replace with an options hash: { type: :multi_delegator, outputs: [...] }"
|
|
38
|
-
{ type: :multi_delegator, outputs: args }
|
|
39
|
-
else
|
|
40
|
-
# Deprecated host/port/type constructor
|
|
41
|
-
warn "[LogStashLogger] The (host, port, type) constructor is deprecated. Please use an options hash instead."
|
|
42
|
-
host, port, type = *args
|
|
43
|
-
{ host: host, port: port, type: type }
|
|
44
|
-
end
|
|
45
|
-
elsif Hash === args[0]
|
|
31
|
+
if args.length == 1 && args[0].is_a?(Hash)
|
|
46
32
|
args[0]
|
|
47
33
|
else
|
|
48
|
-
fail ArgumentError, "Invalid LogStashLogger options"
|
|
34
|
+
fail ArgumentError, "Invalid LogStashLogger options. Expected a single options hash."
|
|
49
35
|
end
|
|
50
36
|
end
|
|
51
37
|
|
|
52
38
|
def self.build_logger(opts)
|
|
53
|
-
formatter = Formatter.new(opts.delete(:formatter),
|
|
39
|
+
formatter = Formatter.new(opts.delete(:formatter),
|
|
40
|
+
customize_event: opts.delete(:customize_event),
|
|
41
|
+
error_logger: opts.fetch(:error_logger, LogStashLogger.configuration.default_error_logger))
|
|
54
42
|
|
|
55
43
|
logger_type = opts[:type].to_s.to_sym
|
|
56
44
|
logger = case logger_type
|
|
@@ -60,13 +48,12 @@ module LogStashLogger
|
|
|
60
48
|
build_syslog_logger(opts)
|
|
61
49
|
else
|
|
62
50
|
build_default_logger(opts)
|
|
63
|
-
|
|
51
|
+
end
|
|
64
52
|
|
|
65
53
|
logger.formatter = formatter if formatter
|
|
66
54
|
logger
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
private
|
|
70
57
|
|
|
71
58
|
def self.build_default_logger(opts)
|
|
72
59
|
logger_class = opts.delete(:logger_class) || ::Logger
|
|
@@ -70,7 +70,7 @@ module LogStashLogger
|
|
|
70
70
|
def method_missing(name, *args, &block)
|
|
71
71
|
@loggers.each do |logger|
|
|
72
72
|
if logger.respond_to?(name)
|
|
73
|
-
logger.send(name, args, &block)
|
|
73
|
+
logger.send(name, *args, &block)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -101,6 +101,7 @@ module LogStashLogger
|
|
|
101
101
|
logger.add(severity, message, progname, &block)
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
|
+
alias log add
|
|
104
105
|
|
|
105
106
|
def <<(msg)
|
|
106
107
|
@loggers.each do |logger|
|
|
@@ -25,7 +25,7 @@ module LogStashLogger
|
|
|
25
25
|
# Convert logger options to OrderedOptions if regular Hash
|
|
26
26
|
logger_options = ActiveSupport::OrderedOptions.new.merge(logger_options)
|
|
27
27
|
|
|
28
|
-
if parsed_uri_options = LogStashLogger::Device.parse_uri_config(logger_options)
|
|
28
|
+
if (parsed_uri_options = LogStashLogger::Device.parse_uri_config(logger_options))
|
|
29
29
|
logger_options.delete(:uri)
|
|
30
30
|
logger_options.merge!(parsed_uri_options)
|
|
31
31
|
end
|
|
@@ -19,7 +19,9 @@ module LogStashLogger
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def push_tags(*tags)
|
|
22
|
-
tags.flatten.reject{ |t| t.
|
|
22
|
+
non_empty_tags = tags.flatten.compact.reject { |t| t.respond_to?(:empty?) && t.empty? }
|
|
23
|
+
|
|
24
|
+
non_empty_tags.tap do |new_tags|
|
|
23
25
|
current_tags.concat new_tags
|
|
24
26
|
end
|
|
25
27
|
end
|
data/logstash-logger.gemspec
CHANGED
|
@@ -22,13 +22,11 @@ Gem::Specification.new do |gem|
|
|
|
22
22
|
|
|
23
23
|
gem.add_development_dependency 'rails'
|
|
24
24
|
gem.add_development_dependency 'redis'
|
|
25
|
-
gem.add_development_dependency '
|
|
25
|
+
gem.add_development_dependency 'ruby-kafka'
|
|
26
26
|
gem.add_development_dependency 'aws-sdk-kinesis'
|
|
27
27
|
gem.add_development_dependency 'aws-sdk-firehose'
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
gem.add_development_dependency 'SyslogLogger'
|
|
31
|
-
end
|
|
29
|
+
gem.add_development_dependency 'syslog', '>= 0.4'
|
|
32
30
|
|
|
33
31
|
gem.add_development_dependency 'rspec', '>= 3'
|
|
34
32
|
gem.add_development_dependency 'rake'
|
|
@@ -36,4 +34,10 @@ Gem::Specification.new do |gem|
|
|
|
36
34
|
gem.add_development_dependency 'wwtd'
|
|
37
35
|
gem.add_development_dependency 'appraisal'
|
|
38
36
|
gem.add_development_dependency 'rubocop'
|
|
37
|
+
gem.add_development_dependency 'rubocop-performance'
|
|
38
|
+
gem.add_development_dependency 'rubocop-rails'
|
|
39
|
+
gem.add_development_dependency 'rubocop-rake'
|
|
40
|
+
gem.add_development_dependency 'rubocop-rspec'
|
|
41
|
+
|
|
42
|
+
gem.required_ruby_version = '>= 3.3'
|
|
39
43
|
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require 'logstash-logger'
|
|
2
|
+
require 'logger'
|
|
3
|
+
|
|
4
|
+
# Stub AWS clients that record every request and can be told to fail.
|
|
5
|
+
class FakeKinesisClient
|
|
6
|
+
attr_reader :requests
|
|
7
|
+
|
|
8
|
+
def initialize(fail_requests: [])
|
|
9
|
+
@requests = []
|
|
10
|
+
@fail_requests = fail_requests
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def put_records(params)
|
|
14
|
+
@requests << params[:records].map { |r| r[:data] }
|
|
15
|
+
raise Seahorse::Client::NetworkingError.new(StandardError.new('connection reset')) if @fail_requests.include?(@requests.size)
|
|
16
|
+
entries = params[:records].map { Aws::Kinesis::Types::PutRecordsResultEntry.new(sequence_number: '1', shard_id: 's') }
|
|
17
|
+
Aws::Kinesis::Types::PutRecordsOutput.new(failed_record_count: 0, records: entries)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class FakeFirehoseClient
|
|
22
|
+
attr_reader :requests
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@requests = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def put_record_batch(params)
|
|
29
|
+
@requests << params[:records].map { |r| r[:data] }
|
|
30
|
+
entries = params[:records].map { Aws::Firehose::Types::PutRecordBatchResponseEntry.new(record_id: '1') }
|
|
31
|
+
Aws::Firehose::Types::PutRecordBatchOutput.new(failed_put_count: 0, request_responses: entries)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe 'AWS outputs and service limits' do
|
|
36
|
+
let(:error_log) { StringIO.new }
|
|
37
|
+
let(:common) do
|
|
38
|
+
{ sync: false, buffer_max_interval: 3600, buffer_flush_at_exit: false,
|
|
39
|
+
aws_region: 'us-east-1', error_logger: ::Logger.new(error_log) }
|
|
40
|
+
end
|
|
41
|
+
let(:small) { (1..5).map { |i| %({"n":#{i}}) } }
|
|
42
|
+
|
|
43
|
+
def pending_count(device)
|
|
44
|
+
device.instance_variable_get(:@buffer_state)[:pending_count]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe LogStashLogger::Device::Kinesis do
|
|
48
|
+
let(:client) { FakeKinesisClient.new }
|
|
49
|
+
let(:device) { LogStashLogger::Device.new(common.merge(type: :kinesis)) }
|
|
50
|
+
|
|
51
|
+
before { allow(Aws::Kinesis::Client).to receive(:new) { client } }
|
|
52
|
+
|
|
53
|
+
it 'drops only a record over the per-record limit' do
|
|
54
|
+
device.write_batch(small + ['x' * 1_048_576])
|
|
55
|
+
expect(client.requests).to eq([small])
|
|
56
|
+
expect(error_log.string).to match(/dropping a record of 1048612 bytes: over the 1048576-byte limit/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'accepts larger records when aws_max_record_bytes is raised' do
|
|
60
|
+
big = 'x' * 2_000_000
|
|
61
|
+
device = LogStashLogger::Device.new(common.merge(type: :kinesis, aws_max_record_bytes: 10_485_760))
|
|
62
|
+
device.write_batch([big])
|
|
63
|
+
expect(client.requests).to eq([[big]])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'splits more than 500 records into several requests' do
|
|
67
|
+
messages = (1..600).map(&:to_s)
|
|
68
|
+
device.write_batch(messages)
|
|
69
|
+
expect(client.requests.map(&:size)).to eq([500, 100])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'splits requests that would go over the request-size limit' do
|
|
73
|
+
messages = Array.new(12) { 'x' * 1_000_000 }
|
|
74
|
+
device.write_batch(messages)
|
|
75
|
+
expect(client.requests.map(&:size)).to eq([10, 2])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'sends records that are not Strings as text' do
|
|
79
|
+
device.write_batch([123, :sym])
|
|
80
|
+
expect(client.requests).to eq([['123', 'sym']])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'when a request fails' do
|
|
84
|
+
let(:client) { FakeKinesisClient.new(fail_requests: [1]) }
|
|
85
|
+
|
|
86
|
+
it 'keeps the records for the next flush instead of treating them as delivered' do
|
|
87
|
+
small.each { |m| device.write(m) }
|
|
88
|
+
expect { device.buffer_flush(force: true) }.not_to raise_error
|
|
89
|
+
expect(pending_count(device)).to eq(small.size)
|
|
90
|
+
expect(error_log.string).to include('5 records will be retried on the next flush')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'drops them when drop_messages_on_flush_error is set' do
|
|
94
|
+
device = LogStashLogger::Device.new(common.merge(type: :kinesis, drop_messages_on_flush_error: true))
|
|
95
|
+
small.each { |m| device.write(m) }
|
|
96
|
+
device.buffer_flush(force: true)
|
|
97
|
+
expect(pending_count(device)).to eq(0)
|
|
98
|
+
expect(error_log.string).to include('dropping 5 records after a failed request')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context 'when a later request fails' do
|
|
103
|
+
let(:client) { FakeKinesisClient.new(fail_requests: [2]) }
|
|
104
|
+
|
|
105
|
+
it 'retries only the records that were not sent' do
|
|
106
|
+
device.write_batch((1..600).map(&:to_s))
|
|
107
|
+
expect(pending_count(device)).to eq(100)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'delivers the rest of a buffered batch around an oversized record' do
|
|
112
|
+
small.each { |m| device.write(m) }
|
|
113
|
+
device.write('x' * 2_000_000)
|
|
114
|
+
device.buffer_flush(force: true)
|
|
115
|
+
expect(client.requests).to eq([small])
|
|
116
|
+
expect(pending_count(device)).to eq(0)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe LogStashLogger::Device::Firehose do
|
|
121
|
+
let(:client) { FakeFirehoseClient.new }
|
|
122
|
+
let(:device) { LogStashLogger::Device.new(common.merge(type: :firehose)) }
|
|
123
|
+
|
|
124
|
+
before { allow(Aws::Firehose::Client).to receive(:new) { client } }
|
|
125
|
+
|
|
126
|
+
it 'drops only a record over the 1,000 KiB limit' do
|
|
127
|
+
device.write_batch(small + ['x' * 1_024_001])
|
|
128
|
+
expect(client.requests).to eq([small])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'splits requests at 4 MB' do
|
|
132
|
+
device.write_batch(Array.new(5) { 'x' * 1_000_000 })
|
|
133
|
+
expect(client.requests.map(&:size)).to eq([4, 1])
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Throttles every record of the first `throttled` requests, then accepts all.
|
|
139
|
+
class ThrottlingKinesisClient
|
|
140
|
+
attr_reader :requests
|
|
141
|
+
|
|
142
|
+
def initialize(throttled:, code: 'ProvisionedThroughputExceededException')
|
|
143
|
+
@requests = []
|
|
144
|
+
@throttled = throttled
|
|
145
|
+
@code = code
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def put_records(params)
|
|
149
|
+
@requests << params[:records].map { |r| r[:data] }
|
|
150
|
+
if @requests.size <= @throttled
|
|
151
|
+
entries = params[:records].map { Aws::Kinesis::Types::PutRecordsResultEntry.new(error_code: @code, error_message: 'slow down') }
|
|
152
|
+
Aws::Kinesis::Types::PutRecordsOutput.new(failed_record_count: entries.size, records: entries)
|
|
153
|
+
else
|
|
154
|
+
entries = params[:records].map { Aws::Kinesis::Types::PutRecordsResultEntry.new(sequence_number: '1', shard_id: 's') }
|
|
155
|
+
Aws::Kinesis::Types::PutRecordsOutput.new(failed_record_count: 0, records: entries)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delivered
|
|
160
|
+
@requests.drop(@throttled).flatten
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe LogStashLogger::Device::Kinesis, 'when records are throttled' do
|
|
165
|
+
let(:error_log) { StringIO.new }
|
|
166
|
+
let(:client) { ThrottlingKinesisClient.new(throttled: 1) }
|
|
167
|
+
let(:records) { (1..50).map(&:to_s) }
|
|
168
|
+
|
|
169
|
+
before { allow(Aws::Kinesis::Client).to receive(:new) { client } }
|
|
170
|
+
|
|
171
|
+
def device(**opts)
|
|
172
|
+
LogStashLogger::Device.new({ type: :kinesis, aws_region: 'us-east-1', sync: false, buffer_max_interval: 0.2,
|
|
173
|
+
buffer_flush_at_exit: false, error_logger: ::Logger.new(error_log) }.merge(opts))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# These flushes run on the buffer's background timer thread, so poll until
|
|
177
|
+
# the block returns true, giving up after `timeout` seconds. The example's
|
|
178
|
+
# own expectations then report what went wrong if it never does.
|
|
179
|
+
def wait_until(timeout: 5)
|
|
180
|
+
deadline = Time.now + timeout
|
|
181
|
+
sleep 0.05 until yield || Time.now > deadline
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'retries a full throttled batch on the timer thread without losing it or stopping the timer' do
|
|
185
|
+
dev = device
|
|
186
|
+
timer = dev.instance_variable_get(:@flush_timer_thread)
|
|
187
|
+
records.each { |r| dev.write(r) }
|
|
188
|
+
|
|
189
|
+
wait_until { client.delivered.size == records.size }
|
|
190
|
+
|
|
191
|
+
expect(client.delivered).to match_array(records)
|
|
192
|
+
expect(dev.instance_variable_get(:@buffer_state)[:pending_count]).to eq(0)
|
|
193
|
+
expect(dev.instance_variable_get(:@buffer_state)[:outgoing_count]).to eq(0)
|
|
194
|
+
expect(timer).to be_alive
|
|
195
|
+
expect(error_log.string).to include('50 records will be retried on the next flush (ProvisionedThroughputExceededException: 50)')
|
|
196
|
+
expect(error_log.string).not_to include('Buffer Full')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'does not block logging when drop_messages_on_full_buffer is false' do
|
|
200
|
+
dev = device(drop_messages_on_full_buffer: false)
|
|
201
|
+
records.each { |r| dev.write(r) }
|
|
202
|
+
wait_until { client.delivered.size == records.size }
|
|
203
|
+
|
|
204
|
+
writer = Thread.new { dev.write('after') }
|
|
205
|
+
expect(writer.join(2)).not_to be_nil
|
|
206
|
+
expect(client.delivered).to match_array(records)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
context 'with InternalFailure' do
|
|
210
|
+
let(:client) { ThrottlingKinesisClient.new(throttled: 1, code: 'InternalFailure') }
|
|
211
|
+
|
|
212
|
+
it 'retries the records' do
|
|
213
|
+
dev = device
|
|
214
|
+
records.first(3).each { |r| dev.write(r) }
|
|
215
|
+
wait_until { client.delivered.size == 3 }
|
|
216
|
+
expect(client.delivered).to match_array(records.first(3))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -72,3 +72,54 @@ describe LogStashLogger::Device::Connectable do
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
describe LogStashLogger::Device::Connectable, 'max_record_bytes' do
|
|
77
|
+
let(:error_log) { StringIO.new }
|
|
78
|
+
|
|
79
|
+
def device(**opts)
|
|
80
|
+
LogStashLogger::Device.new({ type: :udp, port: 5228, sync: false, buffer_max_interval: 3600,
|
|
81
|
+
buffer_flush_at_exit: false, error_logger: ::Logger.new(error_log) }.merge(opts))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def pending(dev)
|
|
85
|
+
dev.instance_variable_get(:@buffer_state)[:pending_items][nil]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'is off by default' do
|
|
89
|
+
dev = device
|
|
90
|
+
dev.write('x' * 2_000_000)
|
|
91
|
+
expect(pending(dev).size).to eq(1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'drops only records over the limit, logging the size but not the content' do
|
|
95
|
+
dev = device(max_record_bytes: 100)
|
|
96
|
+
dev.write('small')
|
|
97
|
+
dev.write('secret' * 50)
|
|
98
|
+
expect(pending(dev)).to eq(['small'])
|
|
99
|
+
expect(error_log.string).to include('dropping a record of 300 bytes: over max_record_bytes (100)')
|
|
100
|
+
expect(error_log.string).not_to include('secret')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'measures records that are not Strings' do
|
|
104
|
+
dev = device(max_record_bytes: 2)
|
|
105
|
+
dev.write(123)
|
|
106
|
+
expect(pending(dev)).to be_empty
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'accepts a String value, as URI query options arrive' do
|
|
110
|
+
dev = LogStashLogger::Device.new(uri: 'udp://localhost:5228?max_record_bytes=100', buffer_flush_at_exit: false,
|
|
111
|
+
error_logger: ::Logger.new(error_log))
|
|
112
|
+
dev.write('x' * 101)
|
|
113
|
+
expect(pending(dev)).to be_empty
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'rejects a value that is not a positive number' do
|
|
117
|
+
expect { device(max_record_bytes: 'lots') }.to raise_error(ArgumentError)
|
|
118
|
+
expect { device(max_record_bytes: 0) }.to raise_error(ArgumentError, /positive/)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'is not forwarded to the Redis client' do
|
|
122
|
+
expect(::Redis).to receive(:new).with(hash_excluding(:max_record_bytes)).and_return(double('redis'))
|
|
123
|
+
LogStashLogger::Device.new(type: :redis, max_record_bytes: 100, buffer_flush_at_exit: false).connect
|
|
124
|
+
end
|
|
125
|
+
end
|
data/spec/device/file_spec.rb
CHANGED
|
@@ -7,6 +7,71 @@ describe LogStashLogger::Device::File do
|
|
|
7
7
|
expect(file_device.to_io).to be_a ::File
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
context "with shift options" do
|
|
11
|
+
let(:shift_file) do
|
|
12
|
+
temp = Tempfile.new('logstash_shift')
|
|
13
|
+
temp.close
|
|
14
|
+
temp
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:shift_device) do
|
|
18
|
+
LogStashLogger::Device.new(
|
|
19
|
+
type: :file,
|
|
20
|
+
path: shift_file.path,
|
|
21
|
+
shift_age: 1,
|
|
22
|
+
shift_size: 1
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:period_file) do
|
|
27
|
+
temp = Tempfile.new('logstash_period_shift')
|
|
28
|
+
temp.close
|
|
29
|
+
temp
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:period_device) do
|
|
33
|
+
LogStashLogger::Device.new(
|
|
34
|
+
type: :file,
|
|
35
|
+
path: period_file.path,
|
|
36
|
+
shift_age: 'daily',
|
|
37
|
+
shift_period_suffix: '%Y-%m-%d'
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
after do
|
|
42
|
+
shift_device.close
|
|
43
|
+
::Dir.glob("#{shift_file.path}*").each do |path|
|
|
44
|
+
::File.delete(path) if ::File.exist?(path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
period_device.close
|
|
48
|
+
::Dir.glob("#{period_file.path}*").each do |path|
|
|
49
|
+
::File.delete(path) if ::File.exist?(path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "wraps a Logger::LogDevice for rotation" do
|
|
54
|
+
expect(shift_device.io).to be_a ::Logger::LogDevice
|
|
55
|
+
expect(shift_device.to_io).to be_a ::File
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "rotates when shift_size is exceeded" do
|
|
59
|
+
shift_device.write("a" * 10)
|
|
60
|
+
shift_device.write("b" * 10)
|
|
61
|
+
shift_device.close
|
|
62
|
+
|
|
63
|
+
expect(::File.exist?("#{shift_file.path}.0")).to be(true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "uses shift_period_suffix for time-based rotation" do
|
|
67
|
+
period_device.write("a")
|
|
68
|
+
period_device.io.send(:shift_log_period, Time.new(2026, 1, 22))
|
|
69
|
+
period_device.close
|
|
70
|
+
|
|
71
|
+
expect(::File.exist?("#{period_file.path}.2026-01-22")).to be(true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
10
75
|
context "when path is not specified" do
|
|
11
76
|
it "raises an exception" do
|
|
12
77
|
expect { described_class.new }.to raise_error(ArgumentError)
|
|
@@ -30,9 +30,12 @@ describe LogStashLogger::Device::Firehose do
|
|
|
30
30
|
response.request_responses = [failed_record]
|
|
31
31
|
|
|
32
32
|
expect(client).to receive(:put_record_batch) { response }
|
|
33
|
-
expect(firehose_device).
|
|
33
|
+
expect(firehose_device).not_to receive(:write)
|
|
34
34
|
|
|
35
35
|
firehose_device.write_one "foo"
|
|
36
|
+
state = firehose_device.instance_variable_get(:@buffer_state)
|
|
37
|
+
expect(state[:pending_items][nil]).to eq(["foo"])
|
|
38
|
+
expect(state[:pending_count]).to eq(1)
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
it "defaults the Firehose stream to logstash" do
|