logstash-input-lumberjack 0.1.9 → 0.1.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da715b62794c6355a47c844a678f452afd568478
4
- data.tar.gz: 866e0a9fcfcdecd61a018fdc33c991393aa65216
3
+ metadata.gz: a24179f3e3ca8a3092c1d692875ea2a88edfa557
4
+ data.tar.gz: 46e2bde92697647cb0b26e66484124e7e4ae6c11
5
5
  SHA512:
6
- metadata.gz: c9a7534c7192428a7169eada97f06a6ec40f10bdee769764ab7d059af2eff2f8b12a68c1fe6da3d5148bda4b679715d13422573cf291c19f4b8907145d4289ea
7
- data.tar.gz: c6357995a2a4bc630e49ecab1b9d23703264579915b7ffbdc70d9dc858f7e645c7cd0630a5a2e5a196e95f2faf3c1e2845748877fc7667081b5f239c8f739b8e
6
+ metadata.gz: c21484a7108a55aabe51d75d8877ddc749c1abfe30a033096ec662fa020f46335ffb8a66cba3de79a20b85d24f31f4b7dc51435544263d0f7bdd815ee96387c2
7
+ data.tar.gz: cc0df5cd9059650ebf13c4d18956ff2953535e8683ea2ecb167d2592a7bdd43eab06353ac4d77e03133ff3106b2ba2f5996f53d7825e0c78756c5c371e90d88f
@@ -0,0 +1,4 @@
1
+ # 0.1.10
2
+ - Deprecating the `max_clients` option
3
+ - Use a circuit breaker to start refusing new connection when the queue is blocked for too long.
4
+ - Add an internal `SizeQueue` with a timeout to drop blocked connections. (https://github.com/logstash-plugins/logstash-input-lumberjack/pull/12)
@@ -0,0 +1,5 @@
1
+ Elasticsearch
2
+ Copyright 2012-2015 Elasticsearch
3
+
4
+ This product includes software developed by The Apache Software
5
+ Foundation (http://www.apache.org/).
@@ -0,0 +1,96 @@
1
+ require "thread"
2
+ require "cabin"
3
+
4
+ module LogStash
5
+ # Largely inspired by Martin's fowler circuit breaker
6
+ class CircuitBreaker
7
+ class OpenBreaker < StandardError; end
8
+
9
+ # Error threshold before opening the breaker,
10
+ # if the breaker is open it wont execute the code.
11
+ DEFAULT_ERROR_THRESHOLD = 5
12
+
13
+ # Recover time after the breaker is open to start
14
+ # executing the method again.
15
+ DEFAULT_TIME_BEFORE_RETRY = 30
16
+
17
+ # Exceptions catched by the circuit breaker,
18
+ # too much errors and the breaker will trip.
19
+ DEFAULT_EXCEPTION_RESCUED = [StandardError]
20
+
21
+ def initialize(name, options = {}, &block)
22
+ @exceptions = Array(options.fetch(:exceptions, [StandardError]))
23
+ @error_threshold = options.fetch(:error_threshold, DEFAULT_ERROR_THRESHOLD)
24
+ @time_before_retry = options.fetch(:time_before_retry, DEFAULT_TIME_BEFORE_RETRY)
25
+ @block = block
26
+ @name = name
27
+ @mutex = Mutex.new
28
+ reset
29
+ end
30
+
31
+ def execute(args = nil)
32
+ case state
33
+ when :open
34
+ logger.warn("CircuitBreaker::Open", :name => @name)
35
+ raise OpenBreaker, "for #{@name}"
36
+ when :close, :half_open
37
+ if block_given?
38
+ yield args
39
+ else
40
+ @block.call(args)
41
+ end
42
+
43
+ if state == :half_open
44
+ logger.warn("CircuitBreaker::Close", :name => @name)
45
+ reset
46
+ end
47
+ end
48
+ rescue *@exceptions => e
49
+ logger.warn("CircuitBreaker::rescuing exceptions", :name => @name, :exception => e.class)
50
+ increment_errors(e)
51
+ end
52
+
53
+ def closed?
54
+ state == :close || state == :half_open
55
+ end
56
+
57
+ private
58
+ def logger
59
+ @logger ||= Cabin::Channel.get(LogStash)
60
+ end
61
+
62
+ def reset
63
+ @mutex.synchronize do
64
+ @errors_count = 0
65
+ @last_failure_time = nil
66
+ end
67
+ end
68
+
69
+ def increment_errors(exception)
70
+ @mutex.synchronize do
71
+ @errors_count += 1
72
+ @last_failure_time = Time.now
73
+
74
+ logger.debug("CircuitBreaker increment errors",
75
+ :errors_count => @errors_count,
76
+ :error_threshold => @error_threshold,
77
+ :exception => exception.class,
78
+ :message => exception.message) if logger.debug?
79
+ end
80
+ end
81
+
82
+ def state
83
+ @mutex.synchronize do
84
+ if @errors_count >= @error_threshold
85
+ if Time.now - @last_failure_time > @time_before_retry
86
+ :half_open
87
+ else
88
+ :open
89
+ end
90
+ else
91
+ :close
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -29,18 +29,23 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
29
29
  # SSL key passphrase to use.
30
30
  config :ssl_key_passphrase, :validate => :password
31
31
 
32
- # Number of maximum clients that the lumberjack input will accept, this allow you
33
- # to control the back pressure up to the client and stop logstash to go OOM with
34
- # connection. This settings is a temporary solution and will be deprecated really soon.
35
- config :max_clients, :validate => :number, :default => 1000, :deprecated => true
32
+ # The lumberjack input using a fixed thread pool to do the actual work and
33
+ # will accept a number of client in a queue, before starting to refuse new
34
+ # connection. This solve an issue when logstash-forwarder clients are
35
+ # trying to connect to logstash which have a blocked pipeline and will
36
+ # make logstash crash with an out of memory exception.
37
+ config :max_clients, :validate => :number, :default => 1000
36
38
 
37
39
  # TODO(sissel): Add CA to authenticate clients with.
38
40
 
39
- public
41
+ BUFFERED_QUEUE_SIZE = 20
42
+ RECONNECT_BACKOFF_SLEEP = 0.5
43
+
40
44
  def register
41
45
  require "lumberjack/server"
42
- require "concurrent"
43
46
  require "concurrent/executors"
47
+ require "logstash/circuit_breaker"
48
+ require "logstash/sized_queue_timeout"
44
49
 
45
50
  @logger.info("Starting lumberjack input listener", :address => "#{@host}:#{@port}")
46
51
  @lumberjack = Lumberjack::Server.new(:address => @host, :port => @port,
@@ -48,6 +53,7 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
48
53
  :ssl_key_passphrase => @ssl_key_passphrase)
49
54
 
50
55
  # Limit the number of thread that can be created by the
56
+ # Limit the number of thread that can be created by the
51
57
  # lumberjack output, if the queue is full the input will
52
58
  # start rejecting new connection and raise an exception
53
59
  @threadpool = Concurrent::ThreadPoolExecutor.new(
@@ -56,37 +62,59 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
56
62
  :max_queue => 1, # in concurrent-ruby, bounded queue need to be at least 1.
57
63
  fallback_policy: :abort
58
64
  )
65
+ @threadpool = Concurrent::CachedThreadPool.new(:idletime => 15)
66
+
67
+ # in 1.5 the main SizeQueue doesnt have the concept of timeout
68
+ # We are using a small plugin buffer to move events to the internal queue
69
+ @buffered_queue = LogStash::SizedQueueTimeout.new(BUFFERED_QUEUE_SIZE)
70
+
71
+ @circuit_breaker = LogStash::CircuitBreaker.new("Lumberjack input",
72
+ :exceptions => [LogStash::SizedQueueTimeout::TimeoutError])
73
+
59
74
  end # def register
60
75
 
61
76
  def run(output_queue)
77
+ start_buffer_broker(output_queue)
78
+
62
79
  while true do
63
- accept do |connection, codec|
64
- invoke(connection, codec) do |_codec, line, fields|
65
- _codec.decode(line) do |event|
66
- decorate(event)
67
- fields.each { |k,v| event[k] = v; v.force_encoding(Encoding::UTF_8) }
68
- output_queue << event
80
+ begin
81
+ # Wrapping the accept call into a CircuitBreaker
82
+ if @circuit_breaker.closed?
83
+ connection = @lumberjack.accept # Blocking call that creates a new connection
84
+
85
+ invoke(connection, codec.clone) do |_codec, line, fields|
86
+ _codec.decode(line) do |event|
87
+ decorate(event)
88
+ fields.each { |k,v| event[k] = v; v.force_encoding(Encoding::UTF_8) }
89
+
90
+ @circuit_breaker.execute { @buffered_queue << event }
91
+ end
69
92
  end
93
+ else
94
+ @logger.warn("Lumberjack input: the pipeline is blocked, temporary refusing new connection.")
95
+ sleep(RECONNECT_BACKOFF_SLEEP)
70
96
  end
97
+ # When too many errors happen inside the circuit breaker it will throw
98
+ # this exception and start refusing connection, we need to catch it but
99
+ # it's safe to ignore.
100
+ rescue LogStash::CircuitBreaker::OpenBreaker => e
71
101
  end
72
102
  end
103
+ rescue LogStash::ShutdownSignal
104
+ @logger.info("Lumberjack input: received ShutdownSignal")
73
105
  rescue => e
74
- @logger.error("Exception in lumberjack input", :exception => e)
106
+ @logger.error("Lumberjack input: unhandled exception", :exception => e, :backtrace => e.backtrace)
107
+ ensure
75
108
  shutdown(output_queue)
76
109
  end # def run
77
110
 
78
111
  private
79
112
  def accept(&block)
80
113
  connection = @lumberjack.accept # Blocking call that creates a new connection
81
-
82
- if @threadpool.length < @threadpool.max_length
83
- block.call(connection, @codec.clone)
84
- else
85
- @logger.warn("Lumberjack input, maximum connection exceeded, new connection are rejected.", :max_clients => @max_clients)
86
- connection.close
87
- end
114
+ block.call(connection, @codec.clone)
88
115
  end
89
116
 
117
+ private
90
118
  def invoke(connection, codec, &block)
91
119
  @threadpool.post do
92
120
  begin
@@ -98,4 +126,12 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
98
126
  end
99
127
  end
100
128
  end
129
+
130
+ def start_buffer_broker(output_queue)
131
+ @threadpool.post do
132
+ while true
133
+ output_queue << @buffered_queue.pop_no_timeout
134
+ end
135
+ end
136
+ end
101
137
  end # class LogStash::Inputs::Lumberjack
@@ -0,0 +1,58 @@
1
+ require "concurrent/atomic/condition"
2
+ require "thread"
3
+
4
+ module LogStash
5
+ # Minimal subset implement of a SizedQueue supporting
6
+ # a timeout option on the lock.
7
+ #
8
+ # This will be part of the main Logstash's sized queue
9
+ class SizedQueueTimeout
10
+ class TimeoutError < StandardError; end
11
+
12
+ DEFAULT_TIMEOUT = 2 # in seconds
13
+
14
+ def initialize(max_size, options = {})
15
+ @condition_in = Concurrent::Condition.new
16
+ @condition_out = Concurrent::Condition.new
17
+
18
+ @max_size = max_size
19
+ @queue = []
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ def push(obj, timeout = DEFAULT_TIMEOUT)
24
+ @mutex.synchronize do
25
+ while full? # wake up check
26
+ result = @condition_out.wait(@mutex, timeout)
27
+ raise TimeoutError if result.timed_out?
28
+ end
29
+
30
+ @queue << obj
31
+ @condition_in.signal
32
+
33
+ return obj
34
+ end
35
+ end
36
+ alias_method :<<, :push
37
+
38
+ def size
39
+ @mutex.synchronize { @queue.size }
40
+ end
41
+
42
+ def pop_no_timeout
43
+ @mutex.synchronize do
44
+ @condition_in.wait(@mutex) while @queue.empty? # Wake up check
45
+
46
+ obj = @queue.shift
47
+ @condition_out.signal
48
+
49
+ return obj
50
+ end
51
+ end
52
+
53
+ private
54
+ def full?
55
+ @queue.size == @max_size
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-lumberjack'
4
- s.version = '0.1.9'
4
+ s.version = '0.1.10'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Receive events using the lumberjack protocol."
7
7
  s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
@@ -29,5 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency 'logstash-devutils'
30
30
  s.add_development_dependency 'stud'
31
31
  s.add_development_dependency 'logstash-codec-multiline'
32
+ s.add_development_dependency "flores"
33
+ s.add_development_dependency "stud"
32
34
  end
33
35
 
@@ -6,7 +6,6 @@ require "logstash/codecs/plain"
6
6
  require "logstash/codecs/multiline"
7
7
  require "logstash/event"
8
8
  require "lumberjack/client"
9
- require_relative "../support/logstash_test"
10
9
 
11
10
  describe LogStash::Inputs::Lumberjack do
12
11
  let(:connection) { double("connection") }
@@ -47,58 +46,4 @@ describe LogStash::Inputs::Lumberjack do
47
46
  end
48
47
  end
49
48
  end
50
-
51
- context "when we have the maximum clients connected" do
52
- let(:max_clients) { 1 }
53
- let(:window_size) { 1 }
54
- let(:config) do
55
- {
56
- "port" => port,
57
- "ssl_certificate" => certificate.ssl_cert,
58
- "ssl_key" => certificate.ssl_key,
59
- "type" => "testing",
60
- "max_clients" => max_clients
61
- }
62
- end
63
-
64
- let(:client_options) do
65
- {
66
- :port => port,
67
- :address => "127.0.0.1",
68
- :ssl_certificate => certificate.ssl_cert,
69
- :window_size => window_size
70
- }
71
- end
72
-
73
- before do
74
- lumberjack.register
75
-
76
- @server = Thread.new do
77
- lumberjack.run(queue)
78
- end
79
-
80
- sleep(0.1) # wait for the server to correctly accept messages
81
- end
82
-
83
- after do
84
- @server.raise(LogStash::ShutdownSignal)
85
- @server.join
86
- end
87
-
88
- it "stops accepting new connection" do
89
- client1 = Lumberjack::Socket.new(client_options)
90
-
91
- # Since the connection is stopped on the other side and OS X and
92
- # linux doesn't behave the same. The client could raise a IOError
93
- # or an SSLError. On OSX I had to try to send some data to trip
94
- # the error.
95
- expect {
96
- client2 = Lumberjack::Socket.new(client_options)
97
-
98
- (window_size + 1).times do
99
- client2.write_hash({"line" => "message"})
100
- end
101
- }.to raise_error
102
- end
103
- end
104
49
  end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+ require "logstash/circuit_breaker"
3
+
4
+ class DummyErrorTest < StandardError; end
5
+
6
+ describe LogStash::CircuitBreaker do
7
+ let(:error_threshold) { 1 }
8
+ let(:options) do
9
+ {
10
+ :exceptions => [DummyErrorTest],
11
+ :error_threshold => error_threshold
12
+ }
13
+ end
14
+
15
+ subject { LogStash::CircuitBreaker.new("testing", options) }
16
+
17
+
18
+ it "closed by default" do
19
+ expect(subject.closed?).to eq(true)
20
+ end
21
+
22
+ context "when having too many errors" do
23
+ let(:future_time) { Time.now + 3600 }
24
+ before do
25
+ subject.execute do
26
+ raise DummyErrorTest
27
+ end
28
+ end
29
+
30
+ it "raised an exception if we have too many errors" do
31
+ expect {
32
+ subject.execute do
33
+ raise DummyErrorTest
34
+ end
35
+ }.to raise_error(LogStash::CircuitBreaker::OpenBreaker)
36
+ end
37
+
38
+ it "sets the breaker to open" do
39
+ expect(subject.closed?).to eq(false)
40
+ end
41
+
42
+ it "resets the breaker after the time before retry" do
43
+ expect(Time).to receive(:now).at_least(1).and_return(future_time)
44
+ expect(subject.closed?).to eq(true)
45
+ end
46
+
47
+ it "doesnt run the command" do
48
+ runned = false
49
+
50
+ begin
51
+ subject.execute do
52
+ runned = true
53
+ end
54
+ rescue LogStash::CircuitBreaker::OpenBreaker
55
+ end
56
+
57
+ expect(runned).to eq(false)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,100 @@
1
+ require "spec_helper"
2
+ require "logstash/sized_queue_timeout"
3
+ require "flores/random"
4
+ require "stud/try"
5
+
6
+ describe "LogStash::SizedQueueTimeout" do
7
+ let(:max_size) { Flores::Random.integer(2..100) }
8
+ let(:element) { Flores::Random.text(0..100) }
9
+
10
+ subject { LogStash::SizedQueueTimeout.new(max_size) }
11
+
12
+ it "adds element to the queue" do
13
+ subject << element
14
+ expect(subject.size).to eq(1)
15
+ end
16
+
17
+ it "allow to pop element from the queue" do
18
+ subject << element
19
+ subject << "awesome"
20
+
21
+ expect(subject.pop_no_timeout).to eq(element)
22
+ end
23
+
24
+ context "when the queue is full" do
25
+ before do
26
+ max_size.times { subject << element }
27
+ end
28
+
29
+ it "block with a timeout" do
30
+ expect {
31
+ subject << element
32
+ }.to raise_error(LogStash::SizedQueueTimeout::TimeoutError)
33
+ end
34
+
35
+ it "unblock when we pop" do
36
+ blocked = Thread.new do
37
+ subject << element
38
+ end
39
+ sleep(0.1) until blocked.stop?
40
+
41
+ expect(blocked.status).to eq("sleep")
42
+
43
+ th = Thread.new do
44
+ subject.pop_no_timeout
45
+ end
46
+ sleep(0.1) until th.stop?
47
+
48
+ expect(blocked.status).to eq(false)
49
+
50
+ blocked.join
51
+ th.join
52
+ end
53
+ end
54
+
55
+ context "when the queue is empty" do
56
+ it "block on pop" do
57
+ blocked = Thread.new do
58
+ subject.pop_no_timeout
59
+ end
60
+ sleep(0.1) until blocked.stop?
61
+
62
+ expect(blocked.status).to eq("sleep")
63
+
64
+ th = Thread.new do
65
+ subject << element
66
+ end
67
+ sleep(0.1) until th.stop?
68
+
69
+ expect(blocked.status).to eq(false)
70
+ th.join
71
+ blocked.join
72
+ end
73
+ end
74
+
75
+ context "when the queue is occupied but not full" do
76
+ before :each do
77
+ Flores::Random.iterations(0..max_size) { subject << "hurray" }
78
+ end
79
+
80
+ it "doesnt block on pop" do
81
+ th = Thread.new do
82
+ subject.pop_no_timeout
83
+ end
84
+ sleep(0.1) until th.stop?
85
+
86
+ expect(th.status).to eq(false)
87
+ th.join
88
+ end
89
+
90
+ it "doesnt block on push" do
91
+ th = Thread.new do
92
+ subject << element
93
+ end
94
+ sleep(0.1) until th.stop?
95
+
96
+ expect(th.status).to eq(false)
97
+ th.join
98
+ end
99
+ end
100
+ end
@@ -1,2 +1,3 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
2
  require "logstash/codecs/plain"
3
+ require_relative "support/logstash_test"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-lumberjack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-12 00:00:00.000000000 Z
11
+ date: 2015-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-core
@@ -114,6 +114,34 @@ dependencies:
114
114
  version: '0'
115
115
  prerelease: false
116
116
  type: :development
117
+ - !ruby/object:Gem::Dependency
118
+ name: flores
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ prerelease: false
130
+ type: :development
131
+ - !ruby/object:Gem::Dependency
132
+ name: stud
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirement: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ prerelease: false
144
+ type: :development
117
145
  description: This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program
118
146
  email: info@elastic.co
119
147
  executables: []
@@ -125,11 +153,16 @@ files:
125
153
  - CONTRIBUTORS
126
154
  - Gemfile
127
155
  - LICENSE
156
+ - NOTICE.TXT
128
157
  - README.md
129
158
  - Rakefile
159
+ - lib/logstash/circuit_breaker.rb
130
160
  - lib/logstash/inputs/lumberjack.rb
161
+ - lib/logstash/sized_queue_timeout.rb
131
162
  - logstash-input-lumberjack.gemspec
132
163
  - spec/inputs/lumberjack_spec.rb
164
+ - spec/logstash/circuit_breaker_spec.rb
165
+ - spec/logstash/size_queue_timeout_spec.rb
133
166
  - spec/spec_helper.rb
134
167
  - spec/support/logstash_test.rb
135
168
  homepage: http://www.elastic.co/guide/en/logstash/current/index.html
@@ -160,5 +193,7 @@ specification_version: 4
160
193
  summary: Receive events using the lumberjack protocol.
161
194
  test_files:
162
195
  - spec/inputs/lumberjack_spec.rb
196
+ - spec/logstash/circuit_breaker_spec.rb
197
+ - spec/logstash/size_queue_timeout_spec.rb
163
198
  - spec/spec_helper.rb
164
199
  - spec/support/logstash_test.rb