logstash-output-amazon_es 2.0.1-java → 6.4.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CONTRIBUTORS +12 -0
- data/Gemfile +8 -0
- data/LICENSE +10 -199
- data/README.md +34 -65
- data/lib/logstash/outputs/amazon_es.rb +218 -423
- data/lib/logstash/outputs/amazon_es/common.rb +347 -0
- data/lib/logstash/outputs/amazon_es/common_configs.rb +141 -0
- data/lib/logstash/outputs/amazon_es/elasticsearch-template-es2x.json +95 -0
- data/lib/logstash/outputs/amazon_es/elasticsearch-template-es5x.json +46 -0
- data/lib/logstash/outputs/amazon_es/elasticsearch-template-es6x.json +45 -0
- data/lib/logstash/outputs/amazon_es/elasticsearch-template-es7x.json +46 -0
- data/lib/logstash/outputs/amazon_es/http_client.rb +359 -74
- data/lib/logstash/outputs/amazon_es/http_client/manticore_adapter.rb +169 -0
- data/lib/logstash/outputs/amazon_es/http_client/pool.rb +457 -0
- data/lib/logstash/outputs/amazon_es/http_client_builder.rb +164 -0
- data/lib/logstash/outputs/amazon_es/template_manager.rb +36 -0
- data/logstash-output-amazon_es.gemspec +13 -22
- data/spec/es_spec_helper.rb +37 -0
- data/spec/unit/http_client_builder_spec.rb +189 -0
- data/spec/unit/outputs/elasticsearch/http_client/manticore_adapter_spec.rb +105 -0
- data/spec/unit/outputs/elasticsearch/http_client/pool_spec.rb +198 -0
- data/spec/unit/outputs/elasticsearch/http_client_spec.rb +222 -0
- data/spec/unit/outputs/elasticsearch/template_manager_spec.rb +25 -0
- data/spec/unit/outputs/elasticsearch_spec.rb +615 -0
- data/spec/unit/outputs/error_whitelist_spec.rb +60 -0
- metadata +49 -110
- data/lib/logstash/outputs/amazon_es/aws_transport.rb +0 -109
- data/lib/logstash/outputs/amazon_es/aws_v4_signer.rb +0 -7
- data/lib/logstash/outputs/amazon_es/aws_v4_signer_impl.rb +0 -62
- data/lib/logstash/outputs/amazon_es/elasticsearch-template.json +0 -41
- data/spec/amazon_es_spec_helper.rb +0 -69
- data/spec/unit/outputs/amazon_es_spec.rb +0 -50
- data/spec/unit/outputs/elasticsearch/protocol_spec.rb +0 -36
- data/spec/unit/outputs/elasticsearch_proxy_spec.rb +0 -58
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'manticore'
|
2
|
+
require 'cgi'
|
3
|
+
require 'aws-sdk-core'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
7
|
+
DEFAULT_HEADERS = { "content-type" => "application/json" }
|
8
|
+
|
9
|
+
CredentialConfig = Struct.new(
|
10
|
+
:access_key_id,
|
11
|
+
:secret_access_key,
|
12
|
+
:session_token,
|
13
|
+
:profile,
|
14
|
+
:instance_profile_credentials_retries,
|
15
|
+
:instance_profile_credentials_timeout,
|
16
|
+
:region)
|
17
|
+
|
18
|
+
class ManticoreAdapter
|
19
|
+
attr_reader :manticore, :logger
|
20
|
+
|
21
|
+
def initialize(logger, options={})
|
22
|
+
@logger = logger
|
23
|
+
options = options.clone || {}
|
24
|
+
options[:ssl] = options[:ssl] || {}
|
25
|
+
|
26
|
+
# We manage our own retries directly, so let's disable them here
|
27
|
+
options[:automatic_retries] = 0
|
28
|
+
# We definitely don't need cookies
|
29
|
+
options[:cookies] = false
|
30
|
+
|
31
|
+
@client_params = {:headers => DEFAULT_HEADERS.merge(options[:headers]|| {}),}
|
32
|
+
|
33
|
+
@port = options[:port] || 9200
|
34
|
+
@protocol = options[:protocol] || 'http'
|
35
|
+
@region = options[:region] || 'us-east-1'
|
36
|
+
@aws_access_key_id = options[:aws_access_key_id] || nil
|
37
|
+
@aws_secret_access_key = options[:aws_secret_access_key] || nil
|
38
|
+
@session_token = options[:session_token] || nil
|
39
|
+
@profile = options[:profile] || 'default'
|
40
|
+
@instance_cred_retries = options[:instance_profile_credentials_retries] || 0
|
41
|
+
@instance_cred_timeout = options[:instance_profile_credentials_timeout] || 1
|
42
|
+
|
43
|
+
if options[:proxy]
|
44
|
+
options[:proxy] = manticore_proxy_hash(options[:proxy])
|
45
|
+
end
|
46
|
+
|
47
|
+
@manticore = ::Manticore::Client.new(options)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Transform the proxy option to a hash. Manticore's support for non-hash
|
51
|
+
# proxy options is broken. This was fixed in https://github.com/cheald/manticore/commit/34a00cee57a56148629ed0a47c329181e7319af5
|
52
|
+
# but this is not yet released
|
53
|
+
def manticore_proxy_hash(proxy_uri)
|
54
|
+
[:scheme, :port, :user, :password, :path].reduce(:host => proxy_uri.host) do |acc,opt|
|
55
|
+
value = proxy_uri.send(opt)
|
56
|
+
acc[opt] = value unless value.nil? || (value.is_a?(String) && value.empty?)
|
57
|
+
acc
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def client
|
62
|
+
@manticore
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
# Performs the request by invoking {Transport::Base#perform_request} with a block.
|
68
|
+
#
|
69
|
+
# @return [Response]
|
70
|
+
# @see Transport::Base#perform_request
|
71
|
+
#
|
72
|
+
def perform_request(url, method, path, params={}, body=nil)
|
73
|
+
# Perform 2-level deep merge on the params, so if the passed params and client params will both have hashes stored on a key they
|
74
|
+
# will be merged as well, instead of choosing just one of the values
|
75
|
+
params = (params || {}).merge(@client_params) { |key, oldval, newval|
|
76
|
+
(oldval.is_a?(Hash) && newval.is_a?(Hash)) ? oldval.merge(newval) : newval
|
77
|
+
}
|
78
|
+
params[:headers] = params[:headers].clone
|
79
|
+
|
80
|
+
|
81
|
+
params[:body] = body if body
|
82
|
+
|
83
|
+
if url.user
|
84
|
+
params[:auth] = {
|
85
|
+
:user => CGI.unescape(url.user),
|
86
|
+
# We have to unescape the password here since manticore won't do it
|
87
|
+
# for us unless its part of the URL
|
88
|
+
:password => CGI.unescape(url.password),
|
89
|
+
:eager => true
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
request_uri = format_url(url, path)
|
94
|
+
|
95
|
+
if @protocol == "https"
|
96
|
+
url = URI::HTTPS.build({:host=>URI(request_uri.to_s).host, :port=>@port.to_s, :path=>path})
|
97
|
+
else
|
98
|
+
url = URI::HTTP.build({:host=>URI(request_uri.to_s).host, :port=>@port.to_s, :path=>path})
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
key = Seahorse::Client::Http::Request.new(options={:endpoint=>url, :http_method => method.to_s.upcase,
|
103
|
+
:headers => params[:headers],:body => params[:body]})
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
credential_config = CredentialConfig.new(@aws_access_key_id, @aws_secret_access_key, @session_token, @profile, @instance_cred_retries, @instance_cred_timeout, @region)
|
108
|
+
|
109
|
+
|
110
|
+
credentials = Aws::CredentialProviderChain.new(credential_config).resolve
|
111
|
+
aws_signer = Aws::Signers::V4.new(credentials, 'es', @region )
|
112
|
+
|
113
|
+
|
114
|
+
signed_key = aws_signer.sign(key)
|
115
|
+
params[:headers] = params[:headers].merge(signed_key.headers)
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
resp = @manticore.send(method.downcase, request_uri.to_s, params)
|
120
|
+
|
121
|
+
# Manticore returns lazy responses by default
|
122
|
+
# We want to block for our usage, this will wait for the repsonse
|
123
|
+
# to finish
|
124
|
+
resp.call
|
125
|
+
# 404s are excluded because they are valid codes in the case of
|
126
|
+
# template installation. We might need a better story around this later
|
127
|
+
# but for our current purposes this is correct
|
128
|
+
if resp.code < 200 || resp.code > 299 && resp.code != 404
|
129
|
+
raise ::LogStash::Outputs::ElasticSearch::HttpClient::Pool::BadResponseCodeError.new(resp.code, request_uri, body, resp.body)
|
130
|
+
end
|
131
|
+
|
132
|
+
resp
|
133
|
+
end
|
134
|
+
|
135
|
+
def format_url(url, path_and_query=nil)
|
136
|
+
request_uri = url.clone
|
137
|
+
|
138
|
+
# We excise auth info from the URL in case manticore itself tries to stick
|
139
|
+
# sensitive data in a thrown exception or log data
|
140
|
+
request_uri.user = nil
|
141
|
+
request_uri.password = nil
|
142
|
+
|
143
|
+
return request_uri.to_s if path_and_query.nil?
|
144
|
+
|
145
|
+
parsed_path_and_query = java.net.URI.new(path_and_query)
|
146
|
+
|
147
|
+
query = request_uri.query
|
148
|
+
parsed_query = parsed_path_and_query.query
|
149
|
+
|
150
|
+
new_query_parts = [request_uri.query, parsed_path_and_query.query].select do |part|
|
151
|
+
part && !part.empty? # Skip empty nil and ""
|
152
|
+
end
|
153
|
+
|
154
|
+
request_uri.query = new_query_parts.join("&") unless new_query_parts.empty?
|
155
|
+
|
156
|
+
request_uri.path = "#{request_uri.path}/#{parsed_path_and_query.path}".gsub(/\/{2,}/, "/")
|
157
|
+
|
158
|
+
request_uri
|
159
|
+
end
|
160
|
+
|
161
|
+
def close
|
162
|
+
@manticore.close
|
163
|
+
end
|
164
|
+
|
165
|
+
def host_unreachable_exceptions
|
166
|
+
[::Manticore::Timeout,::Manticore::SocketException, ::Manticore::ClientProtocolException, ::Manticore::ResolutionFailure, Manticore::SocketTimeout]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end; end; end; end
|
@@ -0,0 +1,457 @@
|
|
1
|
+
module LogStash; module Outputs; class ElasticSearch; class HttpClient;
|
2
|
+
class Pool
|
3
|
+
class NoConnectionAvailableError < Error; end
|
4
|
+
class BadResponseCodeError < Error
|
5
|
+
attr_reader :url, :response_code, :request_body, :response_body
|
6
|
+
|
7
|
+
def initialize(response_code, url, request_body, response_body)
|
8
|
+
@response_code = response_code
|
9
|
+
@url = url
|
10
|
+
@request_body = request_body
|
11
|
+
@response_body = response_body
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
"Got response code '#{response_code}' contacting Elasticsearch at URL '#{@url}'"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class HostUnreachableError < Error;
|
19
|
+
attr_reader :original_error, :url
|
20
|
+
|
21
|
+
def initialize(original_error, url)
|
22
|
+
@original_error = original_error
|
23
|
+
@url = url
|
24
|
+
end
|
25
|
+
|
26
|
+
def message
|
27
|
+
"Elasticsearch Unreachable: [#{@url}][#{original_error.class}] #{original_error.message}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :logger, :adapter, :sniffing, :sniffer_delay, :resurrect_delay, :healthcheck_path, :sniffing_path, :bulk_path
|
32
|
+
|
33
|
+
ROOT_URI_PATH = '/'.freeze
|
34
|
+
|
35
|
+
DEFAULT_OPTIONS = {
|
36
|
+
:healthcheck_path => ROOT_URI_PATH,
|
37
|
+
:sniffing_path => "/_nodes/http",
|
38
|
+
:bulk_path => "/_bulk",
|
39
|
+
:scheme => 'http',
|
40
|
+
:resurrect_delay => 5,
|
41
|
+
:sniffing => false,
|
42
|
+
:sniffer_delay => 10,
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
def initialize(logger, adapter, initial_urls=[], options={})
|
46
|
+
@logger = logger
|
47
|
+
@adapter = adapter
|
48
|
+
@metric = options[:metric]
|
49
|
+
@initial_urls = initial_urls
|
50
|
+
|
51
|
+
raise ArgumentError, "No URL Normalizer specified!" unless options[:url_normalizer]
|
52
|
+
@url_normalizer = options[:url_normalizer]
|
53
|
+
DEFAULT_OPTIONS.merge(options).tap do |merged|
|
54
|
+
@bulk_path = merged[:bulk_path]
|
55
|
+
@sniffing_path = merged[:sniffing_path]
|
56
|
+
@healthcheck_path = merged[:healthcheck_path]
|
57
|
+
@resurrect_delay = merged[:resurrect_delay]
|
58
|
+
@sniffing = merged[:sniffing]
|
59
|
+
@sniffer_delay = merged[:sniffer_delay]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Used for all concurrent operations in this class
|
63
|
+
@state_mutex = Mutex.new
|
64
|
+
|
65
|
+
# Holds metadata about all URLs
|
66
|
+
@url_info = {}
|
67
|
+
@stopping = false
|
68
|
+
end
|
69
|
+
|
70
|
+
def start
|
71
|
+
update_urls(@initial_urls)
|
72
|
+
start_resurrectionist
|
73
|
+
start_sniffer if @sniffing
|
74
|
+
end
|
75
|
+
|
76
|
+
def close
|
77
|
+
@state_mutex.synchronize { @stopping = true }
|
78
|
+
|
79
|
+
logger.debug "Stopping sniffer"
|
80
|
+
stop_sniffer
|
81
|
+
|
82
|
+
logger.debug "Stopping resurrectionist"
|
83
|
+
stop_resurrectionist
|
84
|
+
|
85
|
+
logger.debug "Waiting for in use manticore connections"
|
86
|
+
wait_for_in_use_connections
|
87
|
+
|
88
|
+
logger.debug("Closing adapter #{@adapter}")
|
89
|
+
@adapter.close
|
90
|
+
end
|
91
|
+
|
92
|
+
def wait_for_in_use_connections
|
93
|
+
until in_use_connections.empty?
|
94
|
+
logger.info "Blocked on shutdown to in use connections #{@state_mutex.synchronize {@url_info}}"
|
95
|
+
sleep 1
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def in_use_connections
|
100
|
+
@state_mutex.synchronize { @url_info.values.select {|v| v[:in_use] > 0 } }
|
101
|
+
end
|
102
|
+
|
103
|
+
def alive_urls_count
|
104
|
+
@state_mutex.synchronize { @url_info.values.select {|v| !v[:state] == :alive }.count }
|
105
|
+
end
|
106
|
+
|
107
|
+
def url_info
|
108
|
+
@state_mutex.synchronize { @url_info }
|
109
|
+
end
|
110
|
+
|
111
|
+
def maximum_seen_major_version
|
112
|
+
@state_mutex.synchronize do
|
113
|
+
@maximum_seen_major_version
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def urls
|
118
|
+
url_info.keys
|
119
|
+
end
|
120
|
+
|
121
|
+
def until_stopped(task_name, delay)
|
122
|
+
last_done = Time.now
|
123
|
+
until @state_mutex.synchronize { @stopping }
|
124
|
+
begin
|
125
|
+
now = Time.now
|
126
|
+
if (now - last_done) >= delay
|
127
|
+
last_done = now
|
128
|
+
yield
|
129
|
+
end
|
130
|
+
sleep 1
|
131
|
+
rescue => e
|
132
|
+
logger.warn(
|
133
|
+
"Error while performing #{task_name}",
|
134
|
+
:error_message => e.message,
|
135
|
+
:class => e.class.name,
|
136
|
+
:backtrace => e.backtrace
|
137
|
+
)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def start_sniffer
|
143
|
+
@sniffer = Thread.new do
|
144
|
+
until_stopped("sniffing", sniffer_delay) do
|
145
|
+
begin
|
146
|
+
sniff!
|
147
|
+
rescue NoConnectionAvailableError => e
|
148
|
+
@state_mutex.synchronize { # Synchronize around @url_info
|
149
|
+
logger.warn("Elasticsearch output attempted to sniff for new connections but cannot. No living connections are detected. Pool contains the following current URLs", :url_info => @url_info) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Sniffs the cluster then updates the internal URLs
|
156
|
+
def sniff!
|
157
|
+
update_urls(check_sniff)
|
158
|
+
end
|
159
|
+
|
160
|
+
ES1_SNIFF_RE_URL = /\[([^\/]*)?\/?([^:]*):([0-9]+)\]/
|
161
|
+
ES2_SNIFF_RE_URL = /([^\/]*)?\/?([^:]*):([0-9]+)/
|
162
|
+
# Sniffs and returns the results. Does not update internal URLs!
|
163
|
+
def check_sniff
|
164
|
+
_, url_meta, resp = perform_request(:get, @sniffing_path)
|
165
|
+
@metric.increment(:sniff_requests)
|
166
|
+
parsed = LogStash::Json.load(resp.body)
|
167
|
+
nodes = parsed['nodes']
|
168
|
+
if !nodes || nodes.empty?
|
169
|
+
@logger.warn("Sniff returned no nodes! Will not update hosts.")
|
170
|
+
return nil
|
171
|
+
else
|
172
|
+
case major_version(url_meta[:version])
|
173
|
+
when 5, 6
|
174
|
+
sniff_5x_and_above(nodes)
|
175
|
+
when 2, 1
|
176
|
+
sniff_2x_1x(nodes)
|
177
|
+
else
|
178
|
+
@logger.warn("Could not determine version for nodes in ES cluster!")
|
179
|
+
return nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def major_version(version_string)
|
185
|
+
version_string.split('.').first.to_i
|
186
|
+
end
|
187
|
+
|
188
|
+
def sniff_5x_and_above(nodes)
|
189
|
+
nodes.map do |id,info|
|
190
|
+
# Skip master-only nodes
|
191
|
+
next if info["roles"] && info["roles"] == ["master"]
|
192
|
+
|
193
|
+
if info["http"]
|
194
|
+
uri = LogStash::Util::SafeURI.new(info["http"]["publish_address"])
|
195
|
+
end
|
196
|
+
end.compact
|
197
|
+
end
|
198
|
+
|
199
|
+
def sniff_2x_1x(nodes)
|
200
|
+
nodes.map do |id,info|
|
201
|
+
# TODO Make sure this works with shield. Does that listed
|
202
|
+
# stuff as 'https_address?'
|
203
|
+
|
204
|
+
addr_str = info['http_address'].to_s
|
205
|
+
next unless addr_str # Skip hosts with HTTP disabled
|
206
|
+
|
207
|
+
# Only connect to nodes that serve data
|
208
|
+
# this will skip connecting to client, tribe, and master only nodes
|
209
|
+
# Note that if 'attributes' is NOT set, then that's just a regular node
|
210
|
+
# with master + data + client enabled, so we allow that
|
211
|
+
attributes = info['attributes']
|
212
|
+
next if attributes && attributes['data'] == 'false'
|
213
|
+
|
214
|
+
matches = addr_str.match(ES1_SNIFF_RE_URL) || addr_str.match(ES2_SNIFF_RE_URL)
|
215
|
+
if matches
|
216
|
+
host = matches[1].empty? ? matches[2] : matches[1]
|
217
|
+
port = matches[3]
|
218
|
+
::LogStash::Util::SafeURI.new("#{host}:#{port}")
|
219
|
+
end
|
220
|
+
end.compact
|
221
|
+
end
|
222
|
+
|
223
|
+
def stop_sniffer
|
224
|
+
@sniffer.join if @sniffer
|
225
|
+
end
|
226
|
+
|
227
|
+
def sniffer_alive?
|
228
|
+
@sniffer ? @sniffer.alive? : nil
|
229
|
+
end
|
230
|
+
|
231
|
+
def start_resurrectionist
|
232
|
+
@resurrectionist = Thread.new do
|
233
|
+
until_stopped("resurrection", @resurrect_delay) do
|
234
|
+
healthcheck!
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def healthcheck!
|
240
|
+
# Try to keep locking granularity low such that we don't affect IO...
|
241
|
+
@state_mutex.synchronize { @url_info.select {|url,meta| meta[:state] != :alive } }.each do |url,meta|
|
242
|
+
begin
|
243
|
+
logger.info("Running health check to see if an Elasticsearch connection is working",
|
244
|
+
:healthcheck_url => url, :path => @healthcheck_path)
|
245
|
+
response = perform_request_to_url(url, :head, @healthcheck_path)
|
246
|
+
# If no exception was raised it must have succeeded!
|
247
|
+
logger.warn("Restored connection to ES instance", :url => url.sanitized.to_s)
|
248
|
+
# We reconnected to this node, check its ES version
|
249
|
+
es_version = get_es_version(url)
|
250
|
+
@state_mutex.synchronize do
|
251
|
+
meta[:version] = es_version
|
252
|
+
major = major_version(es_version)
|
253
|
+
if !@maximum_seen_major_version
|
254
|
+
@logger.info("ES Output version determined", :es_version => major)
|
255
|
+
set_new_major_version(major)
|
256
|
+
elsif major > @maximum_seen_major_version
|
257
|
+
@logger.warn("Detected a node with a higher major version than previously observed. This could be the result of an amazon_es cluster upgrade.", :previous_major => @maximum_seen_major_version, :new_major => major, :node_url => url)
|
258
|
+
set_new_major_version(major)
|
259
|
+
end
|
260
|
+
meta[:state] = :alive
|
261
|
+
end
|
262
|
+
rescue HostUnreachableError, BadResponseCodeError => e
|
263
|
+
logger.warn("Attempted to resurrect connection to dead ES instance, but got an error.", url: url.sanitized.to_s, error_type: e.class, error: e.message)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def stop_resurrectionist
|
269
|
+
@resurrectionist.join if @resurrectionist
|
270
|
+
end
|
271
|
+
|
272
|
+
def resurrectionist_alive?
|
273
|
+
@resurrectionist ? @resurrectionist.alive? : nil
|
274
|
+
end
|
275
|
+
|
276
|
+
def perform_request(method, path, params={}, body=nil)
|
277
|
+
with_connection do |url, url_meta|
|
278
|
+
resp = perform_request_to_url(url, method, path, params, body)
|
279
|
+
[url, url_meta, resp]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
[:get, :put, :post, :delete, :patch, :head].each do |method|
|
284
|
+
define_method(method) do |path, params={}, body=nil|
|
285
|
+
_, _, response = perform_request(method, path, params, body)
|
286
|
+
response
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def perform_request_to_url(url, method, path, params={}, body=nil)
|
291
|
+
res = @adapter.perform_request(url, method, path, params, body)
|
292
|
+
rescue *@adapter.host_unreachable_exceptions => e
|
293
|
+
raise HostUnreachableError.new(e, url), "Could not reach host #{e.class}: #{e.message}"
|
294
|
+
end
|
295
|
+
|
296
|
+
def normalize_url(uri)
|
297
|
+
u = @url_normalizer.call(uri)
|
298
|
+
if !u.is_a?(::LogStash::Util::SafeURI)
|
299
|
+
raise "URL Normalizer returned a '#{u.class}' rather than a SafeURI! This shouldn't happen!"
|
300
|
+
end
|
301
|
+
u
|
302
|
+
end
|
303
|
+
|
304
|
+
def update_urls(new_urls)
|
305
|
+
return if new_urls.nil?
|
306
|
+
|
307
|
+
# Normalize URLs
|
308
|
+
new_urls = new_urls.map(&method(:normalize_url))
|
309
|
+
|
310
|
+
# Used for logging nicely
|
311
|
+
state_changes = {:removed => [], :added => []}
|
312
|
+
@state_mutex.synchronize do
|
313
|
+
# Add new connections
|
314
|
+
new_urls.each do |url|
|
315
|
+
# URI objects don't have real hash equality! So, since this isn't perf sensitive we do a linear scan
|
316
|
+
unless @url_info.keys.include?(url)
|
317
|
+
state_changes[:added] << url
|
318
|
+
add_url(url)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Delete connections not in the new list
|
323
|
+
@url_info.each do |url,_|
|
324
|
+
unless new_urls.include?(url)
|
325
|
+
state_changes[:removed] << url
|
326
|
+
remove_url(url)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
if state_changes[:removed].size > 0 || state_changes[:added].size > 0
|
332
|
+
if logger.info?
|
333
|
+
logger.info("Elasticsearch pool URLs updated", :changes => state_changes)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Run an inline healthcheck anytime URLs are updated
|
338
|
+
# This guarantees that during startup / post-startup
|
339
|
+
# sniffing we don't have idle periods waiting for the
|
340
|
+
# periodic sniffer to allow new hosts to come online
|
341
|
+
healthcheck!
|
342
|
+
end
|
343
|
+
|
344
|
+
def size
|
345
|
+
@state_mutex.synchronize { @url_info.size }
|
346
|
+
end
|
347
|
+
|
348
|
+
def es_versions
|
349
|
+
@state_mutex.synchronize { @url_info.size }
|
350
|
+
end
|
351
|
+
|
352
|
+
def add_url(url)
|
353
|
+
@url_info[url] ||= empty_url_meta
|
354
|
+
end
|
355
|
+
|
356
|
+
def remove_url(url)
|
357
|
+
@url_info.delete(url)
|
358
|
+
end
|
359
|
+
|
360
|
+
def empty_url_meta
|
361
|
+
{
|
362
|
+
:in_use => 0,
|
363
|
+
:state => :unknown
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
367
|
+
def with_connection
|
368
|
+
url, url_meta = get_connection
|
369
|
+
|
370
|
+
# Custom error class used here so that users may retry attempts if they receive this error
|
371
|
+
# should they choose to
|
372
|
+
raise NoConnectionAvailableError, "No Available connections" unless url
|
373
|
+
yield url, url_meta
|
374
|
+
rescue HostUnreachableError => e
|
375
|
+
# Mark the connection as dead here since this is likely not transient
|
376
|
+
mark_dead(url, e)
|
377
|
+
raise e
|
378
|
+
rescue BadResponseCodeError => e
|
379
|
+
# These aren't discarded from the pool because these are often very transient
|
380
|
+
# errors
|
381
|
+
raise e
|
382
|
+
rescue => e
|
383
|
+
logger.warn("UNEXPECTED POOL ERROR", :e => e)
|
384
|
+
raise e
|
385
|
+
ensure
|
386
|
+
return_connection(url)
|
387
|
+
end
|
388
|
+
|
389
|
+
def mark_dead(url, error)
|
390
|
+
@state_mutex.synchronize do
|
391
|
+
meta = @url_info[url]
|
392
|
+
# In case a sniff happened removing the metadata just before there's nothing to mark
|
393
|
+
# This is an extreme edge case, but it can happen!
|
394
|
+
return unless meta
|
395
|
+
logger.warn("Marking url as dead. Last error: [#{error.class}] #{error.message}",
|
396
|
+
:url => url, :error_message => error.message, :error_class => error.class.name)
|
397
|
+
meta[:state] = :dead
|
398
|
+
meta[:last_error] = error
|
399
|
+
meta[:last_errored_at] = Time.now
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
def url_meta(url)
|
404
|
+
@state_mutex.synchronize do
|
405
|
+
@url_info[url]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def get_connection
|
410
|
+
@state_mutex.synchronize do
|
411
|
+
# The goal here is to pick a random connection from the least-in-use connections
|
412
|
+
# We want some randomness so that we don't hit the same node over and over, but
|
413
|
+
# we also want more 'fair' behavior in the event of high concurrency
|
414
|
+
eligible_set = nil
|
415
|
+
lowest_value_seen = nil
|
416
|
+
@url_info.each do |url,meta|
|
417
|
+
meta_in_use = meta[:in_use]
|
418
|
+
next if meta[:state] == :dead
|
419
|
+
|
420
|
+
if lowest_value_seen.nil? || meta_in_use < lowest_value_seen
|
421
|
+
lowest_value_seen = meta_in_use
|
422
|
+
eligible_set = [[url, meta]]
|
423
|
+
elsif lowest_value_seen == meta_in_use
|
424
|
+
eligible_set << [url, meta]
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
return nil if eligible_set.nil?
|
429
|
+
|
430
|
+
pick, pick_meta = eligible_set.sample
|
431
|
+
pick_meta[:in_use] += 1
|
432
|
+
|
433
|
+
[pick, pick_meta]
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def return_connection(url)
|
438
|
+
@state_mutex.synchronize do
|
439
|
+
if @url_info[url] # Guard against the condition where the connection has already been deleted
|
440
|
+
@url_info[url][:in_use] -= 1
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def get_es_version(url)
|
446
|
+
request = perform_request_to_url(url, :get, ROOT_URI_PATH)
|
447
|
+
LogStash::Json.load(request.body)["version"]["number"]
|
448
|
+
end
|
449
|
+
|
450
|
+
def set_new_major_version(version)
|
451
|
+
@maximum_seen_major_version = version
|
452
|
+
if @maximum_seen_major_version >= 6
|
453
|
+
@logger.warn("Detected a 6.x and above cluster: the `type` event field won't be used to determine the document _type", :es_version => @maximum_seen_major_version)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end; end; end; end;
|