logstash-input-redis_cluster 1.0.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 +7 -0
- data/CHANGELOG.md +89 -0
- data/CONTRIBUTORS +22 -0
- data/Gemfile +10 -0
- data/LICENSE +202 -0
- data/NOTICE.TXT +5 -0
- data/README.md +99 -0
- data/docs/index.asciidoc +175 -0
- data/lib/logstash/inputs/redis_cluster.rb +395 -0
- data/logstash-input-redis_cluster.gemspec +31 -0
- data/spec/inputs/redis_cluster_spec.rb +478 -0
- metadata +139 -0
@@ -0,0 +1,395 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/inputs/base"
|
4
|
+
require "logstash/inputs/threadable"
|
5
|
+
require 'redis'
|
6
|
+
require 'redis-cluster-client'
|
7
|
+
require "stud/interval"
|
8
|
+
|
9
|
+
# This input will read events from a Redis instance; it supports both Redis channels and lists.
|
10
|
+
# The list command (BLPOP) used by Logstash is supported in Redis v1.3.1+, and
|
11
|
+
# the channel commands used by Logstash are found in Redis v1.3.8+.
|
12
|
+
# While you may be able to make these Redis versions work, the best performance
|
13
|
+
# and stability will be found in more recent stable versions. Versions 2.6.0+
|
14
|
+
# are recommended.
|
15
|
+
#
|
16
|
+
# For more information about Redis, see <http://redis.io/>
|
17
|
+
#
|
18
|
+
# `batch_count` note: If you use the `batch_count` setting, you *must* use a Redis version 2.6.0 or
|
19
|
+
# newer. Anything older does not support the operations used by batching.
|
20
|
+
#
|
21
|
+
module LogStash module Inputs class RedisCluster < LogStash::Inputs::Threadable
|
22
|
+
BATCH_EMPTY_SLEEP = 0.25
|
23
|
+
|
24
|
+
config_name "redis_cluster"
|
25
|
+
|
26
|
+
default :codec, "json"
|
27
|
+
|
28
|
+
# The list of your Redis cluster nodes.
|
29
|
+
config :nodes, :list => true, :validate => :string, :default => []
|
30
|
+
config :fixed_hostname, :validate => :string
|
31
|
+
|
32
|
+
# The port to connect on.
|
33
|
+
# config :port, :validate => :number, :default => 6379
|
34
|
+
|
35
|
+
# SSL
|
36
|
+
config :ssl, :validate => :boolean, :default => false
|
37
|
+
|
38
|
+
# The unix socket path to connect on. Will override host and port if defined.
|
39
|
+
# There is no unix socket path by default.
|
40
|
+
config :path, :validate => :string
|
41
|
+
|
42
|
+
# The Redis database number.
|
43
|
+
config :db, :validate => :number, :default => 0
|
44
|
+
|
45
|
+
# Initial connection timeout in seconds.
|
46
|
+
config :timeout, :validate => :number, :default => 5
|
47
|
+
|
48
|
+
# Password to authenticate with. There is no authentication by default.
|
49
|
+
config :password, :validate => :password
|
50
|
+
|
51
|
+
# The name of a Redis list or channel.
|
52
|
+
config :key, :validate => :string, :required => true
|
53
|
+
|
54
|
+
# Specify either list or channel. If `data_type` is `list`, then we will BLPOP the
|
55
|
+
# key. If `data_type` is `channel`, then we will SUBSCRIBE to the key.
|
56
|
+
# If `data_type` is `pattern_channel`, then we will PSUBSCRIBE to the key.
|
57
|
+
config :data_type, :validate => [ "list", "channel", "pattern_channel" ], :required => true
|
58
|
+
|
59
|
+
# The number of events to return from Redis using EVAL.
|
60
|
+
config :batch_count, :validate => :number, :default => 125
|
61
|
+
|
62
|
+
# Redefined Redis commands to be passed to the Redis client.
|
63
|
+
config :command_map, :validate => :hash, :default => {}
|
64
|
+
|
65
|
+
public
|
66
|
+
|
67
|
+
def register
|
68
|
+
host = self.find_redis_node(@key).split(':')
|
69
|
+
@logger.info("HOST " + host.to_s)
|
70
|
+
@host = host.first
|
71
|
+
@port = host.last
|
72
|
+
@redis_url = @path.nil? ? "redis://#{@password}@#{@host}:#{@port}/#{@db}" : "#{@password}@#{@path}/#{@db}"
|
73
|
+
|
74
|
+
# just switch on data_type once
|
75
|
+
if @data_type == 'list' || @data_type == 'dummy'
|
76
|
+
@run_method = method(:list_runner)
|
77
|
+
@stop_method = method(:list_stop)
|
78
|
+
elsif @data_type == 'channel'
|
79
|
+
@run_method = method(:channel_runner)
|
80
|
+
@stop_method = method(:subscribe_stop)
|
81
|
+
elsif @data_type == 'pattern_channel'
|
82
|
+
@run_method = method(:pattern_channel_runner)
|
83
|
+
@stop_method = method(:subscribe_stop)
|
84
|
+
end
|
85
|
+
|
86
|
+
@list_method = batched? ? method(:list_batch_listener) : method(:list_single_listener)
|
87
|
+
|
88
|
+
@identity = "#{@redis_url} #{@data_type}:#{@key}"
|
89
|
+
puts "Identity: " + @identity
|
90
|
+
@logger.info("Registering Redis", :identity => @identity)
|
91
|
+
end # def register
|
92
|
+
|
93
|
+
def run(output_queue)
|
94
|
+
@run_method.call(output_queue)
|
95
|
+
rescue LogStash::ShutdownSignal
|
96
|
+
# ignore and quit
|
97
|
+
end # def run
|
98
|
+
|
99
|
+
def stop
|
100
|
+
@stop_method.call
|
101
|
+
end
|
102
|
+
|
103
|
+
# private methods -----------------------------
|
104
|
+
private
|
105
|
+
|
106
|
+
def find_redis_node(key)
|
107
|
+
username = 'foo'
|
108
|
+
password = 'bitnami'
|
109
|
+
fixed_hostname = @fixed_hostname
|
110
|
+
@logger.info("Initializing plugin")
|
111
|
+
begin
|
112
|
+
cluster = RedisClient.cluster(nodes: @nodes, fixed_hostname: fixed_hostname).new_client
|
113
|
+
|
114
|
+
puts "Cluster initialized"
|
115
|
+
msg = "Pinging cluster... <= " + cluster.call('PING')
|
116
|
+
@logger.info(msg)
|
117
|
+
puts msg
|
118
|
+
|
119
|
+
slot = cluster.call('CLUSTER', 'KEYSLOT', key).to_i
|
120
|
+
return cluster.call('CLUSTER', 'NODES').lines
|
121
|
+
.map { |line|
|
122
|
+
arr = line.split
|
123
|
+
{ :host => arr[1].split('@').first, :flags => arr[2], :slot => arr.last }
|
124
|
+
}
|
125
|
+
.filter { |arr| arr[:flags].include? 'master' }
|
126
|
+
.map { |arr|
|
127
|
+
range = arr[:slot].split '-'
|
128
|
+
if !!fixed_hostname
|
129
|
+
port = arr[:host].split(':').last
|
130
|
+
host = "#{fixed_hostname}:#{port}"
|
131
|
+
else
|
132
|
+
host = arr[:host]
|
133
|
+
end
|
134
|
+
{ :host => host, :range => Range.new(range.first.to_i, range.last.to_i) }
|
135
|
+
}
|
136
|
+
.filter { |arr| arr[:range].include? slot }
|
137
|
+
.map { |h| h[:host]}
|
138
|
+
.first
|
139
|
+
rescue Exception => e
|
140
|
+
puts e
|
141
|
+
puts e.backtrace
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def batched?
|
146
|
+
@batch_count > 1
|
147
|
+
end
|
148
|
+
|
149
|
+
# private
|
150
|
+
def is_list_type?
|
151
|
+
@data_type == 'list'
|
152
|
+
end
|
153
|
+
|
154
|
+
# private
|
155
|
+
def redis_params
|
156
|
+
params = {
|
157
|
+
:timeout => @timeout,
|
158
|
+
:db => @db,
|
159
|
+
:password => @password.nil? ? nil : @password.value,
|
160
|
+
:ssl => @ssl
|
161
|
+
}
|
162
|
+
|
163
|
+
if @path.nil?
|
164
|
+
params[:host] = @host
|
165
|
+
params[:port] = @port
|
166
|
+
else
|
167
|
+
@logger.warn("Parameter 'path' is set, ignoring parameters: 'host' and 'port'")
|
168
|
+
params[:path] = @path
|
169
|
+
end
|
170
|
+
|
171
|
+
params
|
172
|
+
end
|
173
|
+
|
174
|
+
def new_redis_instance
|
175
|
+
::Redis.new(redis_params)
|
176
|
+
end
|
177
|
+
|
178
|
+
# private
|
179
|
+
def connect
|
180
|
+
redis = new_redis_instance
|
181
|
+
|
182
|
+
# register any renamed Redis commands
|
183
|
+
@command_map.each do |name, renamed|
|
184
|
+
redis._client.command_map[name.to_sym] = renamed.to_sym
|
185
|
+
end
|
186
|
+
|
187
|
+
load_batch_script(redis) if batched? && is_list_type?
|
188
|
+
|
189
|
+
redis
|
190
|
+
end # def connect
|
191
|
+
|
192
|
+
# private
|
193
|
+
def queue_event(msg, output_queue, channel=nil)
|
194
|
+
begin
|
195
|
+
@codec.decode(msg) do |event|
|
196
|
+
decorate(event)
|
197
|
+
event.set("[@metadata][redis_channel]", channel) if !channel.nil?
|
198
|
+
output_queue << event
|
199
|
+
end
|
200
|
+
rescue => e # parse or event creation error
|
201
|
+
@logger.error("Failed to create event", :message => msg, :exception => e, :backtrace => e.backtrace);
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# private
|
206
|
+
def list_stop
|
207
|
+
redis = @redis # might change during method invocation
|
208
|
+
return if redis.nil? || !redis.connected?
|
209
|
+
|
210
|
+
redis.quit rescue nil # does client.disconnect internally
|
211
|
+
# check if input retried while executing
|
212
|
+
list_stop unless redis.equal? @redis
|
213
|
+
@redis = nil
|
214
|
+
end
|
215
|
+
|
216
|
+
# private
|
217
|
+
def list_runner(output_queue)
|
218
|
+
while !stop?
|
219
|
+
begin
|
220
|
+
@redis ||= connect
|
221
|
+
@list_method.call(@redis, output_queue)
|
222
|
+
rescue => e
|
223
|
+
log_error(e)
|
224
|
+
retry if reset_for_error_retry(e)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def list_batch_listener(redis, output_queue)
|
230
|
+
begin
|
231
|
+
results = redis.evalsha(@redis_script_sha, [@key], [@batch_count-1])
|
232
|
+
results.each do |item|
|
233
|
+
queue_event(item, output_queue)
|
234
|
+
end
|
235
|
+
|
236
|
+
if results.size.zero?
|
237
|
+
sleep BATCH_EMPTY_SLEEP
|
238
|
+
end
|
239
|
+
|
240
|
+
# Below is a commented-out implementation of 'batch fetch'
|
241
|
+
# using pipelined LPOP calls. This in practice has been observed to
|
242
|
+
# perform exactly the same in terms of event throughput as
|
243
|
+
# the evalsha method. Given that the EVALSHA implementation uses
|
244
|
+
# one call to Redis instead of N (where N == @batch_count) calls,
|
245
|
+
# I decided to go with the 'evalsha' method of fetching N items
|
246
|
+
# from Redis in bulk.
|
247
|
+
#redis.pipelined do
|
248
|
+
#error, item = redis.lpop(@key)
|
249
|
+
#(@batch_count-1).times { redis.lpop(@key) }
|
250
|
+
#end.each do |item|
|
251
|
+
#queue_event(item, output_queue) if item
|
252
|
+
#end
|
253
|
+
# --- End commented out implementation of 'batch fetch'
|
254
|
+
# further to the above, the LUA script now uses lrange and trim
|
255
|
+
# which should further improve the efficiency of the script
|
256
|
+
rescue ::Redis::CommandError => e
|
257
|
+
if e.to_s =~ /NOSCRIPT/ then
|
258
|
+
@logger.warn("Redis may have been restarted, reloading Redis batch EVAL script", :exception => e);
|
259
|
+
load_batch_script(redis)
|
260
|
+
retry
|
261
|
+
else
|
262
|
+
raise e
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def list_single_listener(redis, output_queue)
|
268
|
+
item = redis.blpop(@key, 0, :timeout => 1)
|
269
|
+
return unless item # from timeout or other conditions
|
270
|
+
|
271
|
+
# blpop returns the 'key' read from as well as the item result
|
272
|
+
# we only care about the result (2nd item in the list).
|
273
|
+
queue_event(item.last, output_queue)
|
274
|
+
end
|
275
|
+
|
276
|
+
# private
|
277
|
+
def subscribe_stop
|
278
|
+
redis = @redis # might change during method invocation
|
279
|
+
return if redis.nil? || !redis.connected?
|
280
|
+
|
281
|
+
if redis.subscribed?
|
282
|
+
if @data_type == 'pattern_channel'
|
283
|
+
redis.punsubscribe
|
284
|
+
else
|
285
|
+
redis.unsubscribe
|
286
|
+
end
|
287
|
+
end
|
288
|
+
redis.close rescue nil # does client.disconnect
|
289
|
+
# check if input retried while executing
|
290
|
+
subscribe_stop unless redis.equal? @redis
|
291
|
+
@redis = nil
|
292
|
+
end
|
293
|
+
|
294
|
+
# private
|
295
|
+
def redis_runner
|
296
|
+
begin
|
297
|
+
@redis ||= connect
|
298
|
+
yield
|
299
|
+
rescue => e
|
300
|
+
log_error(e)
|
301
|
+
retry if reset_for_error_retry(e)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def log_error(e)
|
306
|
+
info = { message: e.message, exception: e.class }
|
307
|
+
info[:backtrace] = e.backtrace if @logger.debug?
|
308
|
+
|
309
|
+
case e
|
310
|
+
when ::Redis::TimeoutError
|
311
|
+
# expected for channels in case no data is available
|
312
|
+
@logger.debug("Redis timeout, retrying", info)
|
313
|
+
when ::Redis::BaseConnectionError, ::Redis::ProtocolError
|
314
|
+
@logger.warn("Redis connection error", info)
|
315
|
+
when ::Redis::BaseError
|
316
|
+
@logger.error("Redis error", info)
|
317
|
+
when ::LogStash::ShutdownSignal
|
318
|
+
@logger.debug("Received shutdown signal")
|
319
|
+
else
|
320
|
+
info[:backtrace] ||= e.backtrace
|
321
|
+
@logger.error("Unexpected error", info)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# @return [true] if operation is fine to retry
|
326
|
+
def reset_for_error_retry(e)
|
327
|
+
return if e.is_a?(::LogStash::ShutdownSignal)
|
328
|
+
|
329
|
+
# Reset the redis variable to trigger reconnect
|
330
|
+
@redis = nil
|
331
|
+
|
332
|
+
Stud.stoppable_sleep(1) { stop? }
|
333
|
+
!stop? # retry if not stop-ing
|
334
|
+
end
|
335
|
+
|
336
|
+
# private
|
337
|
+
def channel_runner(output_queue)
|
338
|
+
redis_runner do
|
339
|
+
channel_listener(output_queue)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# private
|
344
|
+
def channel_listener(output_queue)
|
345
|
+
@redis.subscribe(@key) do |on|
|
346
|
+
on.subscribe do |channel, count|
|
347
|
+
@logger.info("Subscribed", :channel => channel, :count => count)
|
348
|
+
end
|
349
|
+
|
350
|
+
on.message do |channel, message|
|
351
|
+
queue_event(message, output_queue, channel)
|
352
|
+
end
|
353
|
+
|
354
|
+
on.unsubscribe do |channel, count|
|
355
|
+
@logger.info("Unsubscribed", :channel => channel, :count => count)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def pattern_channel_runner(output_queue)
|
361
|
+
redis_runner do
|
362
|
+
pattern_channel_listener(output_queue)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# private
|
367
|
+
def pattern_channel_listener(output_queue)
|
368
|
+
@redis.psubscribe @key do |on|
|
369
|
+
on.psubscribe do |channel, count|
|
370
|
+
@logger.info("Subscribed", :channel => channel, :count => count)
|
371
|
+
end
|
372
|
+
|
373
|
+
on.pmessage do |pattern, channel, message|
|
374
|
+
queue_event(message, output_queue, channel)
|
375
|
+
end
|
376
|
+
|
377
|
+
on.punsubscribe do |channel, count|
|
378
|
+
@logger.info("Unsubscribed", :channel => channel, :count => count)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# private
|
384
|
+
def load_batch_script(redis)
|
385
|
+
#A Redis Lua EVAL script to fetch a count of keys
|
386
|
+
redis_script = <<EOF
|
387
|
+
local batchsize = tonumber(ARGV[1])
|
388
|
+
local result = redis.call(\'#{@command_map.fetch('lrange', 'lrange')}\', KEYS[1], 0, batchsize)
|
389
|
+
redis.call(\'#{@command_map.fetch('ltrim', 'ltrim')}\', KEYS[1], batchsize + 1, -1)
|
390
|
+
return result
|
391
|
+
EOF
|
392
|
+
@redis_script_sha = redis.script(:load, redis_script)
|
393
|
+
end
|
394
|
+
|
395
|
+
end end end # Redis Inputs LogStash
|
@@ -0,0 +1,31 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
|
3
|
+
s.name = 'logstash-input-redis_cluster'
|
4
|
+
s.version = '1.0.0'
|
5
|
+
s.licenses = ['Apache License (2.0)']
|
6
|
+
s.summary = "Reads events from a Redis instance"
|
7
|
+
s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
|
8
|
+
s.authors = ["Elastic"]
|
9
|
+
s.email = 'info@elastic.co'
|
10
|
+
s.homepage = "http://www.elastic.co/guide/en/logstash/current/index.html"
|
11
|
+
s.require_paths = ["lib"]
|
12
|
+
|
13
|
+
# Files
|
14
|
+
s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "VERSION", "docs/**/*"]
|
15
|
+
|
16
|
+
# Tests
|
17
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
18
|
+
|
19
|
+
# Special flag to let us know this is actually a logstash plugin
|
20
|
+
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
21
|
+
|
22
|
+
# Gem dependencies
|
23
|
+
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
|
24
|
+
|
25
|
+
s.add_runtime_dependency 'logstash-codec-json'
|
26
|
+
s.add_runtime_dependency 'redis', '>= 4.0.1', '< 5'
|
27
|
+
|
28
|
+
s.add_development_dependency 'logstash-devutils', '>= 0.0.16'
|
29
|
+
s.add_development_dependency 'redis-cluster-client'
|
30
|
+
end
|
31
|
+
|