fluq 0.7.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.
- data/.gitignore +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +39 -0
- data/MIT-LICENCE +19 -0
- data/README.md +10 -0
- data/Rakefile +11 -0
- data/benchmark/logging.rb +37 -0
- data/benchmark/socket.rb +52 -0
- data/bin/fluq-rb +8 -0
- data/examples/common.rb +3 -0
- data/examples/simple.rb +5 -0
- data/fluq.gemspec +33 -0
- data/lib/fluq.rb +50 -0
- data/lib/fluq/buffer.rb +6 -0
- data/lib/fluq/buffer/base.rb +51 -0
- data/lib/fluq/buffer/file.rb +68 -0
- data/lib/fluq/cli.rb +142 -0
- data/lib/fluq/dsl.rb +49 -0
- data/lib/fluq/dsl/options.rb +27 -0
- data/lib/fluq/error.rb +2 -0
- data/lib/fluq/event.rb +55 -0
- data/lib/fluq/feed.rb +6 -0
- data/lib/fluq/feed/base.rb +18 -0
- data/lib/fluq/feed/json.rb +28 -0
- data/lib/fluq/feed/msgpack.rb +27 -0
- data/lib/fluq/feed/tsv.rb +30 -0
- data/lib/fluq/handler.rb +6 -0
- data/lib/fluq/handler/base.rb +80 -0
- data/lib/fluq/handler/log.rb +67 -0
- data/lib/fluq/handler/null.rb +4 -0
- data/lib/fluq/input.rb +6 -0
- data/lib/fluq/input/base.rb +59 -0
- data/lib/fluq/input/socket.rb +50 -0
- data/lib/fluq/input/socket/connection.rb +41 -0
- data/lib/fluq/mixins.rb +6 -0
- data/lib/fluq/mixins/loggable.rb +7 -0
- data/lib/fluq/mixins/logger.rb +26 -0
- data/lib/fluq/reactor.rb +76 -0
- data/lib/fluq/testing.rb +26 -0
- data/lib/fluq/url.rb +16 -0
- data/lib/fluq/version.rb +3 -0
- data/spec/fluq/buffer/base_spec.rb +21 -0
- data/spec/fluq/buffer/file_spec.rb +47 -0
- data/spec/fluq/dsl/options_spec.rb +24 -0
- data/spec/fluq/dsl_spec.rb +43 -0
- data/spec/fluq/event_spec.rb +25 -0
- data/spec/fluq/feed/base_spec.rb +15 -0
- data/spec/fluq/feed/json_spec.rb +27 -0
- data/spec/fluq/feed/msgpack_spec.rb +27 -0
- data/spec/fluq/feed/tsv_spec.rb +27 -0
- data/spec/fluq/handler/base_spec.rb +70 -0
- data/spec/fluq/handler/log_spec.rb +68 -0
- data/spec/fluq/handler/null_spec.rb +11 -0
- data/spec/fluq/input/base_spec.rb +29 -0
- data/spec/fluq/input/socket/connection_spec.rb +35 -0
- data/spec/fluq/input/socket_spec.rb +45 -0
- data/spec/fluq/mixins/loggable_spec.rb +10 -0
- data/spec/fluq/mixins/logger_spec.rb +25 -0
- data/spec/fluq/reactor_spec.rb +58 -0
- data/spec/fluq/url_spec.rb +16 -0
- data/spec/fluq_spec.rb +11 -0
- data/spec/scenario/config/nested/common.rb +3 -0
- data/spec/scenario/config/test.rb +3 -0
- data/spec/scenario/lib/fluq/handler/custom/test_handler.rb +4 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/configuration.rb +25 -0
- metadata +242 -0
data/lib/fluq/dsl.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class FluQ::DSL
|
2
|
+
attr_reader :path, :reactor, :inputs, :handlers
|
3
|
+
|
4
|
+
# @param [FluQ::Reactor] reactor
|
5
|
+
# @param [String] DSL script file path
|
6
|
+
def initialize(reactor, path)
|
7
|
+
@reactor = reactor
|
8
|
+
@path = Pathname.new(path)
|
9
|
+
@inputs = []
|
10
|
+
@handlers = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param [Array<Symbol>] input type path, e.g. :socket
|
14
|
+
def input(*type, &block)
|
15
|
+
klass = constantize(:input, *type)
|
16
|
+
inputs.push [klass, FluQ::DSL::Options.new(&block).to_hash]
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Array<Symbol>] handler type path, e.g. :log, :counter
|
20
|
+
def handler(*type, &block)
|
21
|
+
klass = constantize(:handler, *type)
|
22
|
+
handlers.push [klass, FluQ::DSL::Options.new(&block).to_hash]
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [String] relative relative path
|
26
|
+
def import(relative)
|
27
|
+
instance_eval(path.dirname.join(relative).read)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Starts the components. Handlers first, then inputs.
|
31
|
+
def run
|
32
|
+
instance_eval(path.read)
|
33
|
+
handlers.each {|klass, options| reactor.register(klass, options) }
|
34
|
+
inputs.each {|klass, options| reactor.listen(klass, options) }
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def constantize(*path)
|
40
|
+
require([:fluq, *path].join('/'))
|
41
|
+
names = path.map {|p| p.to_s.split('_').map(&:capitalize).join }
|
42
|
+
names.inject(FluQ) {|klass, name| klass.const_get(name) }
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
%w'options'.each do |name|
|
48
|
+
require "fluq/dsl/#{name}"
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class FluQ::DSL::Options
|
2
|
+
|
3
|
+
# Constructor
|
4
|
+
# @yield options assigment
|
5
|
+
def initialize(&block)
|
6
|
+
@opts = {}
|
7
|
+
instance_eval(&block) if block
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Hash] options hash
|
11
|
+
def to_hash
|
12
|
+
@opts
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def method_missing(name, *args, &block)
|
18
|
+
value = args[0]
|
19
|
+
if value && block
|
20
|
+
@opts[name.to_sym] = value
|
21
|
+
@opts[:"#{name}_options"] = self.class.new(&block).to_hash
|
22
|
+
else
|
23
|
+
@opts[name.to_sym] = value || block || true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/fluq/error.rb
ADDED
data/lib/fluq/event.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class FluQ::Event < Hash
|
2
|
+
|
3
|
+
attr_reader :tag, :timestamp
|
4
|
+
|
5
|
+
# @param [String] tag the event tag
|
6
|
+
# @param [Integer] timestamp the UNIX timestamp
|
7
|
+
# @param [Hash] record the attribute pairs
|
8
|
+
def initialize(tag = "", timestamp = 0, record = {})
|
9
|
+
@tag, @timestamp = tag.to_s, timestamp.to_i
|
10
|
+
super()
|
11
|
+
update(record) if Hash === record
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Time] UTC time
|
15
|
+
def time
|
16
|
+
@time ||= Time.at(timestamp).utc
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Array] tuple
|
20
|
+
def to_a
|
21
|
+
[tag, timestamp, self]
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Boolean] true if comparable
|
25
|
+
def ==(other)
|
26
|
+
case other
|
27
|
+
when Array
|
28
|
+
to_a == other
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
alias :eql? :==
|
34
|
+
|
35
|
+
# @return [String] tab-separated string
|
36
|
+
def to_tsv
|
37
|
+
[tag, timestamp, Oj.dump(self)].join("\t")
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String] JSON encoded
|
41
|
+
def to_json
|
42
|
+
Oj.dump merge("=" => tag, "@" => timestamp)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String] mgspack encoded bytes
|
46
|
+
def to_msgpack
|
47
|
+
MessagePack.pack merge("=" => tag, "@" => timestamp)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] inspection
|
51
|
+
def inspect
|
52
|
+
[tag, timestamp, Hash.new.update(self)].inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/lib/fluq/feed.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class FluQ::Feed::Base
|
2
|
+
include Enumerable
|
3
|
+
include FluQ::Mixins::Loggable
|
4
|
+
|
5
|
+
# @attr_reader [FluQ::Buffer::Base] buffer
|
6
|
+
attr_reader :buffer
|
7
|
+
|
8
|
+
# @param [FluQ::Buffer::Base] buffer
|
9
|
+
def initialize(buffer)
|
10
|
+
@buffer = buffer
|
11
|
+
end
|
12
|
+
|
13
|
+
# @abstract enumerator
|
14
|
+
# @yield ober a feed of events
|
15
|
+
# @yieldparam [FluQ::Event] event
|
16
|
+
def each
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class FluQ::Feed::Json < FluQ::Feed::Base
|
2
|
+
|
3
|
+
# @see [FluQ::Feed::Base] each
|
4
|
+
def each
|
5
|
+
buffer.drain do |io|
|
6
|
+
while line = io.gets
|
7
|
+
event = to_event(line)
|
8
|
+
yield event if event
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def to_event(line)
|
16
|
+
case hash = Oj.load(line)
|
17
|
+
when Hash
|
18
|
+
FluQ::Event.new hash.delete("="), hash.delete("@"), hash
|
19
|
+
else
|
20
|
+
logger.warn "buffer contained invalid event #{hash.inspect}"
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
rescue Oj::ParseError
|
24
|
+
logger.warn "buffer contained invalid line #{line.inspect}"
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class FluQ::Feed::Msgpack < FluQ::Feed::Base
|
2
|
+
|
3
|
+
# @see [FluQ::Feed::Base] each
|
4
|
+
def each
|
5
|
+
buffer.drain do |io|
|
6
|
+
pac = MessagePack::Unpacker.new(io)
|
7
|
+
pac.each do |hash|
|
8
|
+
event = to_event(hash)
|
9
|
+
yield event if event
|
10
|
+
end
|
11
|
+
end
|
12
|
+
rescue EOFError
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def to_event(hash)
|
18
|
+
case hash
|
19
|
+
when Hash
|
20
|
+
FluQ::Event.new hash.delete("="), hash.delete("@"), hash
|
21
|
+
else
|
22
|
+
logger.warn "buffer contained invalid event #{hash.inspect}"
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class FluQ::Feed::Tsv < FluQ::Feed::Base
|
2
|
+
|
3
|
+
# @see [FluQ::Feed::Base] each
|
4
|
+
def each
|
5
|
+
buffer.drain do |io|
|
6
|
+
while line = io.gets
|
7
|
+
event = to_event(line)
|
8
|
+
yield event if event
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def to_event(line)
|
16
|
+
tag, timestamp, json = line.split("\t")
|
17
|
+
|
18
|
+
case hash = Oj.load(json)
|
19
|
+
when Hash
|
20
|
+
FluQ::Event.new tag, timestamp, hash
|
21
|
+
else
|
22
|
+
logger.warn "buffer contained invalid event #{[tag, timestamp, hash].inspect}"
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
rescue Oj::ParseError, ArgumentError
|
26
|
+
logger.warn "buffer contained invalid line #{line.inspect}"
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/lib/fluq/handler.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
class FluQ::Handler::Base
|
4
|
+
include FluQ::Mixins::Loggable
|
5
|
+
|
6
|
+
# @return [String] handler type
|
7
|
+
def self.type
|
8
|
+
@type ||= name.split("::")[-1].downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
# @attr_reader [FluQ::Reactor] reactor
|
12
|
+
attr_reader :reactor
|
13
|
+
|
14
|
+
# @attr_reader [String] name unique name
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# @attr_reader [Hash] config
|
18
|
+
attr_reader :config
|
19
|
+
|
20
|
+
# @attr_reader [Regexp] pattern
|
21
|
+
attr_reader :pattern
|
22
|
+
|
23
|
+
# @param [Hash] options
|
24
|
+
# @option options [String] :name a (unique) handler identifier
|
25
|
+
# @option options [String] :pattern tag pattern to match
|
26
|
+
# @example
|
27
|
+
#
|
28
|
+
# class MyHandler < FluQ::Handler::Base
|
29
|
+
# end
|
30
|
+
# MyHandler.new(reactor, pattern: "visits.*")
|
31
|
+
#
|
32
|
+
def initialize(reactor, options = {})
|
33
|
+
@reactor = reactor
|
34
|
+
@config = defaults.merge(options)
|
35
|
+
@name = config[:name] || generate_name
|
36
|
+
@pattern = generate_pattern
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean] true if event matches
|
40
|
+
def match?(event)
|
41
|
+
!!(pattern =~ event.tag)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Array<FluQ::Event>] events
|
45
|
+
# @return [Array<FluQ::Event>] matching events
|
46
|
+
def select(events)
|
47
|
+
events.select &method(:match?)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @abstract callback, called on each event
|
51
|
+
# @param [Array<FluQ::Event>] the event stream
|
52
|
+
def on_events(events)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# Configuration defaults
|
58
|
+
def defaults
|
59
|
+
{ pattern: /./ }
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String] generated name
|
63
|
+
def generate_name
|
64
|
+
suffix = [Digest::MD5.digest(config[:pattern].to_s)].pack("m0").tr('+/=lIO0', 'pqrsxyz')[0,6]
|
65
|
+
[self.class.type, suffix].join("-")
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_pattern
|
69
|
+
return config[:pattern] if Regexp === config[:pattern]
|
70
|
+
|
71
|
+
string = Regexp.quote(config[:pattern])
|
72
|
+
string.gsub!("\\*", ".*")
|
73
|
+
string.gsub!("\\?", ".")
|
74
|
+
string.gsub!(/\\\{(.+?)\\\}/) do |match|
|
75
|
+
"(?:#{$1.split(",").join("|")})"
|
76
|
+
end
|
77
|
+
Regexp.new "^#{string}$"
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class FluQ::Handler::Log < FluQ::Handler::Base
|
2
|
+
|
3
|
+
class FilePool < TimedLRU
|
4
|
+
|
5
|
+
def open(path)
|
6
|
+
path = path.to_s
|
7
|
+
self[path.to_s] ||= begin
|
8
|
+
FileUtils.mkdir_p File.dirname(path)
|
9
|
+
file = File.open(path, "a+")
|
10
|
+
file.autoclose = true
|
11
|
+
file
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
# @attr_reader [FluQ::Handler::Log::FilePool] file pool
|
18
|
+
attr_reader :pool
|
19
|
+
|
20
|
+
# @see FluQ::Handler::Base#initialize
|
21
|
+
def initialize(*)
|
22
|
+
super
|
23
|
+
@full_path = FluQ.root.join(config[:path]).to_s.freeze
|
24
|
+
@rewrite = config[:rewrite]
|
25
|
+
@convert = config[:convert]
|
26
|
+
@pool = FilePool.new max_size: config[:cache_max], ttl: config[:cache_ttl]
|
27
|
+
end
|
28
|
+
|
29
|
+
# @see FluQ::Handler::Base#on_events
|
30
|
+
def on_events(events)
|
31
|
+
partition(events).each {|path, slice| write(path, slice) }
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Configuration defaults
|
37
|
+
def defaults
|
38
|
+
super.merge \
|
39
|
+
path: "log/raw/%t/%Y%m%d/%H.log",
|
40
|
+
rewrite: lambda {|tag| tag.gsub(".", "/") },
|
41
|
+
convert: lambda {|event| event.to_tsv },
|
42
|
+
cache_max: 100,
|
43
|
+
cache_ttl: 300
|
44
|
+
end
|
45
|
+
|
46
|
+
def write(path, slice, attepts = 0)
|
47
|
+
io = @pool.open(path)
|
48
|
+
slice.each do |event|
|
49
|
+
io.write "#{@convert.call(event)}\n"
|
50
|
+
end
|
51
|
+
rescue IOError
|
52
|
+
@pool.delete path.to_s
|
53
|
+
(attepts+=1) < 3 ? retry : raise
|
54
|
+
end
|
55
|
+
|
56
|
+
def partition(events)
|
57
|
+
paths = {}
|
58
|
+
events.each do |event|
|
59
|
+
tag = @rewrite.call(event.tag)
|
60
|
+
path = event.time.strftime(@full_path.gsub("%t", tag))
|
61
|
+
paths[path] ||= []
|
62
|
+
paths[path] << event
|
63
|
+
end
|
64
|
+
paths
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
data/lib/fluq/input.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
class FluQ::Input::Base
|
2
|
+
include FluQ::Mixins::Loggable
|
3
|
+
|
4
|
+
# @attr_reader [FluQ::Reactor] reactor reference
|
5
|
+
attr_reader :reactor
|
6
|
+
|
7
|
+
# @attr_reader [Hash] config
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
# @param [FluQ::Reactor] reactor
|
11
|
+
# @param [Hash] options various configuration options
|
12
|
+
def initialize(reactor, options = {})
|
13
|
+
super()
|
14
|
+
@reactor = reactor
|
15
|
+
@config = defaults.merge(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [String] descriptive name
|
19
|
+
def name
|
20
|
+
@name ||= self.class.name.split("::")[-1].downcase
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start the input
|
24
|
+
def run
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates a new buffer object
|
28
|
+
# @return [FluQ::Buffer::Base] a new buffer
|
29
|
+
def new_buffer
|
30
|
+
buffer_klass.new config[:buffer_options]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Flushes and closes a buffer
|
34
|
+
# @param [FluQ::Buffer::Base] buffer
|
35
|
+
def flush!(buffer)
|
36
|
+
feed_klass.new(buffer).each_slice(10_000) do |events|
|
37
|
+
reactor.process(events)
|
38
|
+
end
|
39
|
+
rescue => ex
|
40
|
+
logger.crash "#{self.class.name} failure: #{ex.message} (#{ex.class.name})", ex
|
41
|
+
ensure
|
42
|
+
buffer.close if buffer
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def buffer_klass
|
48
|
+
@buffer_klass ||= FluQ::Buffer.const_get(config[:buffer].to_s.capitalize)
|
49
|
+
end
|
50
|
+
|
51
|
+
def feed_klass
|
52
|
+
@feed_klass ||= FluQ::Feed.const_get(config[:feed].to_s.capitalize)
|
53
|
+
end
|
54
|
+
|
55
|
+
def defaults
|
56
|
+
{ buffer: "file", feed: "msgpack", buffer_options: {} }
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|