logstash-output-elasticsearch 2.0.0.pre.beta-java → 2.1.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/logstash/outputs/elasticsearch.rb +113 -140
- data/lib/logstash/outputs/elasticsearch/http_client.rb +74 -36
- data/logstash-output-elasticsearch.gemspec +6 -5
- data/spec/es_spec_helper.rb +1 -0
- data/spec/integration/outputs/index_spec.rb +18 -36
- data/spec/integration/outputs/retry_spec.rb +16 -6
- data/spec/integration/outputs/routing_spec.rb +20 -39
- data/spec/unit/outputs/elasticsearch/protocol_spec.rb +31 -16
- data/spec/unit/outputs/elasticsearch_proxy_spec.rb +1 -1
- data/spec/unit/outputs/elasticsearch_spec.rb +50 -2
- metadata +61 -49
- data/.gitignore +0 -5
- data/Rakefile +0 -1
@@ -6,15 +6,19 @@ require "elasticsearch/transport/transport/http/manticore"
|
|
6
6
|
|
7
7
|
module LogStash::Outputs::Elasticsearch
|
8
8
|
class HttpClient
|
9
|
-
attr_reader :client, :options, :client_options
|
10
|
-
DEFAULT_OPTIONS
|
11
|
-
|
12
|
-
|
9
|
+
attr_reader :client, :options, :client_options, :sniffer_thread
|
10
|
+
# This is here in case we use DEFAULT_OPTIONS in the future
|
11
|
+
# DEFAULT_OPTIONS = {
|
12
|
+
# :setting => value
|
13
|
+
# }
|
13
14
|
|
14
15
|
def initialize(options={})
|
15
|
-
@logger =
|
16
|
-
|
16
|
+
@logger = options[:logger]
|
17
|
+
# Again, in case we use DEFAULT_OPTIONS in the future, uncomment this.
|
18
|
+
# @options = DEFAULT_OPTIONS.merge(options)
|
19
|
+
@options = options
|
17
20
|
@client = build_client(@options)
|
21
|
+
start_sniffing!
|
18
22
|
end
|
19
23
|
|
20
24
|
def template_install(name, template, force=false)
|
@@ -49,26 +53,78 @@ module LogStash::Outputs::Elasticsearch
|
|
49
53
|
end
|
50
54
|
end.flatten
|
51
55
|
|
52
|
-
|
56
|
+
@client.bulk(:body => bulk_body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def start_sniffing!
|
60
|
+
if options[:sniffing]
|
61
|
+
@sniffer_thread = Thread.new do
|
62
|
+
loop do
|
63
|
+
sniff!
|
64
|
+
sleep (options[:sniffing_delay].to_f || 30)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
53
69
|
|
54
|
-
|
70
|
+
def stop_sniffing!
|
71
|
+
@sniffer_thread.kill() if @sniffer_thread
|
72
|
+
end
|
73
|
+
|
74
|
+
def sniff!
|
75
|
+
client.transport.reload_connections! if options[:sniffing]
|
76
|
+
hosts_by_name = client.transport.hosts.map {|h| h["name"]}.sort
|
77
|
+
@logger.debug({"count" => hosts_by_name.count, "hosts" => hosts_by_name})
|
78
|
+
rescue StandardError => e
|
79
|
+
@logger.error("Error while sniffing connection",
|
80
|
+
:message => e.message,
|
81
|
+
:class => e.class.name,
|
82
|
+
:backtrace => e.backtrace)
|
55
83
|
end
|
56
84
|
|
57
85
|
private
|
58
86
|
|
87
|
+
# Builds a client and returns an Elasticsearch::Client
|
88
|
+
#
|
89
|
+
# The `options` is a hash where the following symbol keys have meaning:
|
90
|
+
#
|
91
|
+
# * `:hosts` - array of String. Set a list of hosts to use for communication.
|
92
|
+
# * `:port` - number. set the port to use to communicate with Elasticsearch
|
93
|
+
# * `:user` - String. The user to use for authentication.
|
94
|
+
# * `:password` - String. The password to use for authentication.
|
95
|
+
# * `:timeout` - Float. A duration value, in seconds, after which a socket
|
96
|
+
# operation or request will be aborted if not yet successfull
|
97
|
+
# * `:client_settings` - a hash; see below for keys.
|
98
|
+
#
|
99
|
+
# The `client_settings` key is a has that can contain other settings:
|
100
|
+
#
|
101
|
+
# * `:ssl` - Boolean. Enable or disable SSL/TLS.
|
102
|
+
# * `:proxy` - String. Choose a HTTP HTTProxy to use.
|
103
|
+
# * `:path` - String. The leading path for prefixing Elasticsearch
|
104
|
+
# requests. This is sometimes used if you are proxying Elasticsearch access
|
105
|
+
# through a special http path, such as using mod_rewrite.
|
59
106
|
def build_client(options)
|
60
|
-
|
61
|
-
|
107
|
+
hosts = options[:hosts] || ["127.0.0.1"]
|
108
|
+
client_settings = options[:client_settings] || {}
|
109
|
+
timeout = options[:timeout] || 0
|
110
|
+
|
111
|
+
uris = hosts.map do |host|
|
112
|
+
proto = client_settings[:ssl] ? "https" : "http"
|
113
|
+
if host =~ /:\d+\z/
|
114
|
+
"#{proto}://#{host}#{client_settings[:path]}"
|
115
|
+
else
|
116
|
+
# Use default port of 9200 if none provided with host.
|
117
|
+
"#{proto}://#{host}:9200#{client_settings[:path]}"
|
118
|
+
end
|
62
119
|
end
|
63
120
|
|
64
121
|
@client_options = {
|
65
122
|
:hosts => uris,
|
66
|
-
:ssl =>
|
67
|
-
:
|
68
|
-
|
69
|
-
:
|
70
|
-
:
|
71
|
-
:proxy => options[:client_settings][:proxy]
|
123
|
+
:ssl => client_settings[:ssl],
|
124
|
+
:transport_options => {
|
125
|
+
:socket_timeout => timeout,
|
126
|
+
:request_timeout => timeout,
|
127
|
+
:proxy => client_settings[:proxy]
|
72
128
|
},
|
73
129
|
:transport_class => ::Elasticsearch::Transport::Transport::HTTP::Manticore
|
74
130
|
}
|
@@ -78,27 +134,9 @@ module LogStash::Outputs::Elasticsearch
|
|
78
134
|
@client_options[:headers] = { "Authorization" => "Basic #{token}" }
|
79
135
|
end
|
80
136
|
|
81
|
-
|
82
|
-
c.transport.reload_connections! if options[:client_settings][:reload_connections]
|
83
|
-
c
|
84
|
-
end
|
137
|
+
@logger.debug? && @logger.debug("Elasticsearch HTTP client options", client_options)
|
85
138
|
|
86
|
-
|
87
|
-
if bulk_response["errors"]
|
88
|
-
# The structure of the response from the REST Bulk API is follows:
|
89
|
-
# {"took"=>74, "errors"=>true, "items"=>[{"create"=>{"_index"=>"logstash-2014.11.17",
|
90
|
-
# "_type"=>"logs",
|
91
|
-
# "_id"=>"AUxTS2C55Jrgi-hC6rQF",
|
92
|
-
# "_version"=>1,
|
93
|
-
# "status"=>400,
|
94
|
-
# "error"=>"MapperParsingException[failed to parse]..."}}]}
|
95
|
-
# where each `item` is a hash of {OPTYPE => Hash[]}. calling first, will retrieve
|
96
|
-
# this hash as a single array with two elements, where the value is the second element (i.first[1])
|
97
|
-
# then the status of that item is retrieved.
|
98
|
-
{"errors" => true, "statuses" => bulk_response["items"].map { |i| i.first[1]['status'] }}
|
99
|
-
else
|
100
|
-
{"errors" => false}
|
101
|
-
end
|
139
|
+
Elasticsearch::Client.new(client_options)
|
102
140
|
end
|
103
141
|
|
104
142
|
def template_exists?(name)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-output-elasticsearch'
|
4
|
-
s.version = '2.
|
4
|
+
s.version = '2.1.0'
|
5
5
|
s.licenses = ['apache-2.0']
|
6
6
|
s.summary = "Logstash Output to Elasticsearch"
|
7
7
|
s.description = "Output events to elasticsearch"
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.require_paths = ["lib"]
|
12
12
|
|
13
13
|
# Files
|
14
|
-
s.files =
|
14
|
+
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
|
15
15
|
|
16
16
|
# Tests
|
17
17
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
@@ -21,13 +21,13 @@ Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
# Gem dependencies
|
23
23
|
s.add_runtime_dependency 'concurrent-ruby'
|
24
|
-
s.add_runtime_dependency 'elasticsearch', ['>= 1.0.
|
24
|
+
s.add_runtime_dependency 'elasticsearch', ['>= 1.0.13', '~> 1.0']
|
25
25
|
s.add_runtime_dependency 'stud', ['>= 0.0.17', '~> 0.0']
|
26
26
|
s.add_runtime_dependency 'cabin', ['~> 0.6']
|
27
|
-
s.add_runtime_dependency "logstash-core",
|
27
|
+
s.add_runtime_dependency "logstash-core", ">= 2.0.0.snapshot", "< 3.0.0"
|
28
28
|
|
29
29
|
s.add_development_dependency 'ftw', '~> 0.0.42'
|
30
|
-
s.add_development_dependency 'logstash-
|
30
|
+
s.add_development_dependency 'logstash-codec-plain'
|
31
31
|
|
32
32
|
if RUBY_PLATFORM == 'java'
|
33
33
|
s.platform = RUBY_PLATFORM
|
@@ -36,4 +36,5 @@ Gem::Specification.new do |s|
|
|
36
36
|
|
37
37
|
s.add_development_dependency 'logstash-devutils'
|
38
38
|
s.add_development_dependency 'longshoreman'
|
39
|
+
s.add_development_dependency 'flores'
|
39
40
|
end
|
data/spec/es_spec_helper.rb
CHANGED
@@ -6,13 +6,16 @@ shared_examples "an indexer" do
|
|
6
6
|
let(:event_count) { 10000 + rand(500) }
|
7
7
|
let(:flush_size) { rand(200) + 1 }
|
8
8
|
let(:config) { "not implemented" }
|
9
|
+
subject { LogStash::Outputs::ElasticSearch.new(config) }
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
before do
|
12
|
+
subject.register
|
13
|
+
event_count.times do
|
14
|
+
subject.receive(LogStash::Event.new("message" => "Hello World!", "type" => type))
|
15
|
+
end
|
16
|
+
end
|
15
17
|
|
18
|
+
it "ships events" do
|
16
19
|
index_url = "http://#{get_host}:#{get_port}/#{index}"
|
17
20
|
|
18
21
|
ftw = FTW::Agent.new
|
@@ -42,23 +45,12 @@ end
|
|
42
45
|
describe "an indexer with custom index_type", :integration => true do
|
43
46
|
it_behaves_like "an indexer" do
|
44
47
|
let(:config) {
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
type => "#{type}"
|
51
|
-
}
|
52
|
-
}
|
53
|
-
output {
|
54
|
-
elasticsearch {
|
55
|
-
hosts => "#{get_host()}"
|
56
|
-
port => "#{get_port}"
|
57
|
-
index => "#{index}"
|
58
|
-
flush_size => #{flush_size}
|
59
|
-
}
|
48
|
+
{
|
49
|
+
"hosts" => get_host,
|
50
|
+
"port" => get_port,
|
51
|
+
"index" => index,
|
52
|
+
"flush_size" => flush_size
|
60
53
|
}
|
61
|
-
CONFIG
|
62
54
|
}
|
63
55
|
end
|
64
56
|
end
|
@@ -67,22 +59,12 @@ describe "an indexer with no type value set (default to logs)", :integration =>
|
|
67
59
|
it_behaves_like "an indexer" do
|
68
60
|
let(:type) { "logs" }
|
69
61
|
let(:config) {
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
}
|
76
|
-
}
|
77
|
-
output {
|
78
|
-
elasticsearch {
|
79
|
-
hosts => "#{get_host()}"
|
80
|
-
port => "#{get_port}"
|
81
|
-
index => "#{index}"
|
82
|
-
flush_size => #{flush_size}
|
83
|
-
}
|
62
|
+
{
|
63
|
+
"hosts" => get_host,
|
64
|
+
"port" => get_port,
|
65
|
+
"index" => index,
|
66
|
+
"flush_size" => flush_size
|
84
67
|
}
|
85
|
-
CONFIG
|
86
68
|
}
|
87
69
|
end
|
88
70
|
end
|
@@ -11,8 +11,18 @@ describe "failures in bulk class expected behavior", :integration => true do
|
|
11
11
|
let(:max_retries) { 3 }
|
12
12
|
|
13
13
|
def mock_actions_with_response(*resp)
|
14
|
-
|
15
|
-
.
|
14
|
+
expanded_responses = resp.map do |resp|
|
15
|
+
items = resp["statuses"] && resp["statuses"].map do |status|
|
16
|
+
{"create" => {"status" => status, "error" => "Error for #{status}"}}
|
17
|
+
end
|
18
|
+
|
19
|
+
{
|
20
|
+
"errors" => resp["errors"],
|
21
|
+
"items" => items
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
allow_any_instance_of(LogStash::Outputs::Elasticsearch::HttpClient).to receive(:bulk).and_return(*expanded_responses)
|
16
26
|
end
|
17
27
|
|
18
28
|
subject! do
|
@@ -61,7 +71,7 @@ describe "failures in bulk class expected behavior", :integration => true do
|
|
61
71
|
end
|
62
72
|
subject.register
|
63
73
|
subject.receive(event1)
|
64
|
-
subject.
|
74
|
+
subject.close
|
65
75
|
end
|
66
76
|
|
67
77
|
it "should retry actions with response status of 503" do
|
@@ -109,7 +119,7 @@ describe "failures in bulk class expected behavior", :integration => true do
|
|
109
119
|
subject.register
|
110
120
|
subject.receive(invalid_event)
|
111
121
|
expect(subject).not_to receive(:retry_push)
|
112
|
-
subject.
|
122
|
+
subject.close
|
113
123
|
|
114
124
|
@es.indices.refresh
|
115
125
|
sleep(5)
|
@@ -123,7 +133,7 @@ describe "failures in bulk class expected behavior", :integration => true do
|
|
123
133
|
subject.register
|
124
134
|
subject.receive(event1)
|
125
135
|
expect(subject).not_to receive(:retry_push)
|
126
|
-
subject.
|
136
|
+
subject.close
|
127
137
|
@es.indices.refresh
|
128
138
|
sleep(5)
|
129
139
|
Stud::try(10.times) do
|
@@ -136,7 +146,7 @@ describe "failures in bulk class expected behavior", :integration => true do
|
|
136
146
|
subject.register
|
137
147
|
subject.receive(invalid_event)
|
138
148
|
subject.receive(event1)
|
139
|
-
subject.
|
149
|
+
subject.close
|
140
150
|
|
141
151
|
@es.indices.refresh
|
142
152
|
sleep(5)
|
@@ -7,14 +7,17 @@ shared_examples "a routing indexer" do
|
|
7
7
|
let(:flush_size) { rand(200) + 1 }
|
8
8
|
let(:routing) { "not_implemented" }
|
9
9
|
let(:config) { "not_implemented" }
|
10
|
+
subject { LogStash::Outputs::ElasticSearch.new(config) }
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
before do
|
13
|
+
subject.register
|
14
|
+
event_count.times do
|
15
|
+
subject.receive(LogStash::Event.new("message" => "Hello World!", "type" => type))
|
16
|
+
end
|
17
|
+
end
|
14
18
|
|
15
|
-
pipeline = LogStash::Pipeline.new(config)
|
16
|
-
pipeline.run
|
17
19
|
|
20
|
+
it "ships events" do
|
18
21
|
index_url = "http://#{get_host()}:#{get_port()}/#{index}"
|
19
22
|
|
20
23
|
ftw = FTW::Agent.new
|
@@ -36,24 +39,13 @@ describe "(http protocol) index events with static routing", :integration => tru
|
|
36
39
|
it_behaves_like 'a routing indexer' do
|
37
40
|
let(:routing) { "test" }
|
38
41
|
let(:config) {
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
}
|
46
|
-
}
|
47
|
-
output {
|
48
|
-
elasticsearch {
|
49
|
-
hosts => "#{get_host()}"
|
50
|
-
port => "#{get_port()}"
|
51
|
-
index => "#{index}"
|
52
|
-
flush_size => #{flush_size}
|
53
|
-
routing => "#{routing}"
|
54
|
-
}
|
42
|
+
{
|
43
|
+
"hosts" => get_host,
|
44
|
+
"port" => get_port,
|
45
|
+
"index" => index,
|
46
|
+
"flush_size" => flush_size,
|
47
|
+
"routing" => routing
|
55
48
|
}
|
56
|
-
CONFIG
|
57
49
|
}
|
58
50
|
end
|
59
51
|
end
|
@@ -62,24 +54,13 @@ describe "(http_protocol) index events with fieldref in routing value", :integra
|
|
62
54
|
it_behaves_like 'a routing indexer' do
|
63
55
|
let(:routing) { "test" }
|
64
56
|
let(:config) {
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
}
|
72
|
-
}
|
73
|
-
output {
|
74
|
-
elasticsearch {
|
75
|
-
hosts => "#{get_host()}"
|
76
|
-
port => "#{get_port()}"
|
77
|
-
index => "#{index}"
|
78
|
-
flush_size => #{flush_size}
|
79
|
-
routing => "%{message}"
|
80
|
-
}
|
57
|
+
{
|
58
|
+
"hosts" => get_host,
|
59
|
+
"port" => get_port,
|
60
|
+
"index" => index,
|
61
|
+
"flush_size" => flush_size,
|
62
|
+
"routing" => "%{message}"
|
81
63
|
}
|
82
|
-
CONFIG
|
83
64
|
}
|
84
65
|
end
|
85
66
|
end
|
@@ -3,24 +3,39 @@ require "logstash/outputs/elasticsearch/http_client"
|
|
3
3
|
require "java"
|
4
4
|
|
5
5
|
describe LogStash::Outputs::Elasticsearch::HttpClient do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
describe "sniffing" do
|
7
|
+
let(:base_options) { {:hosts => ["127.0.0.1"], :logger => Cabin::Channel.get }}
|
8
|
+
let(:client) { LogStash::Outputs::Elasticsearch::HttpClient.new(base_options.merge(client_opts)) }
|
9
|
+
let(:transport) { client.client.transport }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(transport).to receive(:reload_connections!)
|
13
13
|
end
|
14
|
-
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
context "with sniffing enabled" do
|
16
|
+
let(:client_opts) { {:sniffing => true, :sniffing_delay => 1 } }
|
17
|
+
|
18
|
+
after do
|
19
|
+
client.stop_sniffing!
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should start the sniffer" do
|
23
|
+
expect(client.sniffer_thread).to be_a(Thread)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should periodically sniff the client" do
|
27
|
+
sleep 2
|
28
|
+
expect(transport).to have_received(:reload_connections!)
|
29
|
+
end
|
24
30
|
end
|
31
|
+
|
32
|
+
context "with sniffing disabled" do
|
33
|
+
let(:client_opts) { {:sniffing => false} }
|
34
|
+
|
35
|
+
it "should not start the sniffer" do
|
36
|
+
expect(client.sniffer_thread).to be_nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
25
40
|
end
|
26
41
|
end
|