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.
Files changed (68) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +6 -0
  3. data/Gemfile +6 -0
  4. data/Gemfile.lock +39 -0
  5. data/MIT-LICENCE +19 -0
  6. data/README.md +10 -0
  7. data/Rakefile +11 -0
  8. data/benchmark/logging.rb +37 -0
  9. data/benchmark/socket.rb +52 -0
  10. data/bin/fluq-rb +8 -0
  11. data/examples/common.rb +3 -0
  12. data/examples/simple.rb +5 -0
  13. data/fluq.gemspec +33 -0
  14. data/lib/fluq.rb +50 -0
  15. data/lib/fluq/buffer.rb +6 -0
  16. data/lib/fluq/buffer/base.rb +51 -0
  17. data/lib/fluq/buffer/file.rb +68 -0
  18. data/lib/fluq/cli.rb +142 -0
  19. data/lib/fluq/dsl.rb +49 -0
  20. data/lib/fluq/dsl/options.rb +27 -0
  21. data/lib/fluq/error.rb +2 -0
  22. data/lib/fluq/event.rb +55 -0
  23. data/lib/fluq/feed.rb +6 -0
  24. data/lib/fluq/feed/base.rb +18 -0
  25. data/lib/fluq/feed/json.rb +28 -0
  26. data/lib/fluq/feed/msgpack.rb +27 -0
  27. data/lib/fluq/feed/tsv.rb +30 -0
  28. data/lib/fluq/handler.rb +6 -0
  29. data/lib/fluq/handler/base.rb +80 -0
  30. data/lib/fluq/handler/log.rb +67 -0
  31. data/lib/fluq/handler/null.rb +4 -0
  32. data/lib/fluq/input.rb +6 -0
  33. data/lib/fluq/input/base.rb +59 -0
  34. data/lib/fluq/input/socket.rb +50 -0
  35. data/lib/fluq/input/socket/connection.rb +41 -0
  36. data/lib/fluq/mixins.rb +6 -0
  37. data/lib/fluq/mixins/loggable.rb +7 -0
  38. data/lib/fluq/mixins/logger.rb +26 -0
  39. data/lib/fluq/reactor.rb +76 -0
  40. data/lib/fluq/testing.rb +26 -0
  41. data/lib/fluq/url.rb +16 -0
  42. data/lib/fluq/version.rb +3 -0
  43. data/spec/fluq/buffer/base_spec.rb +21 -0
  44. data/spec/fluq/buffer/file_spec.rb +47 -0
  45. data/spec/fluq/dsl/options_spec.rb +24 -0
  46. data/spec/fluq/dsl_spec.rb +43 -0
  47. data/spec/fluq/event_spec.rb +25 -0
  48. data/spec/fluq/feed/base_spec.rb +15 -0
  49. data/spec/fluq/feed/json_spec.rb +27 -0
  50. data/spec/fluq/feed/msgpack_spec.rb +27 -0
  51. data/spec/fluq/feed/tsv_spec.rb +27 -0
  52. data/spec/fluq/handler/base_spec.rb +70 -0
  53. data/spec/fluq/handler/log_spec.rb +68 -0
  54. data/spec/fluq/handler/null_spec.rb +11 -0
  55. data/spec/fluq/input/base_spec.rb +29 -0
  56. data/spec/fluq/input/socket/connection_spec.rb +35 -0
  57. data/spec/fluq/input/socket_spec.rb +45 -0
  58. data/spec/fluq/mixins/loggable_spec.rb +10 -0
  59. data/spec/fluq/mixins/logger_spec.rb +25 -0
  60. data/spec/fluq/reactor_spec.rb +58 -0
  61. data/spec/fluq/url_spec.rb +16 -0
  62. data/spec/fluq_spec.rb +11 -0
  63. data/spec/scenario/config/nested/common.rb +3 -0
  64. data/spec/scenario/config/test.rb +3 -0
  65. data/spec/scenario/lib/fluq/handler/custom/test_handler.rb +4 -0
  66. data/spec/spec_helper.rb +12 -0
  67. data/spec/support/configuration.rb +25 -0
  68. metadata +242 -0
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Feed::Json do
4
+
5
+ let(:buffer) { FluQ::Buffer::Base.new }
6
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, "a" => "b") }
7
+
8
+ before do
9
+ io = StringIO.new [event, event, event].map(&:to_json).join("\n")
10
+ buffer.stub(:drain).and_yield(io)
11
+ end
12
+
13
+ subject do
14
+ described_class.new(buffer)
15
+ end
16
+
17
+ it { should be_a(FluQ::Feed::Base) }
18
+ its(:to_a) { should == [event, event, event] }
19
+
20
+ it 'should log invalid inputs' do
21
+ io = StringIO.new [event.to_json, "ABCD", event.to_json].join("\n")
22
+ buffer.stub(:drain).and_yield(io)
23
+ subject.logger.should_receive(:warn).at_least(:once)
24
+ subject.to_a.should == [event, event]
25
+ end
26
+
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Feed::Msgpack do
4
+
5
+ let(:buffer) { FluQ::Buffer::Base.new }
6
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, "a" => "b") }
7
+
8
+ before do
9
+ io = StringIO.new [event, event, event].map(&:to_msgpack).join
10
+ buffer.stub(:drain).and_yield(io)
11
+ end
12
+
13
+ subject do
14
+ described_class.new(buffer)
15
+ end
16
+
17
+ it { should be_a(FluQ::Feed::Base) }
18
+ its(:to_a) { should == [event, event, event] }
19
+
20
+ it 'should log invalid inputs' do
21
+ io = StringIO.new [event.to_msgpack, "ABCD", event.to_msgpack].join
22
+ buffer.stub(:drain).and_yield(io)
23
+ subject.logger.should_receive(:warn).at_least(:once)
24
+ subject.to_a.should == [event, event]
25
+ end
26
+
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Feed::Tsv do
4
+
5
+ let(:buffer) { FluQ::Buffer::Base.new }
6
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, "a" => "b") }
7
+
8
+ before do
9
+ io = StringIO.new [event, event, event].map(&:to_tsv).join("\n")
10
+ buffer.stub(:drain).and_yield(io)
11
+ end
12
+
13
+ subject do
14
+ described_class.new(buffer)
15
+ end
16
+
17
+ it { should be_a(FluQ::Feed::Base) }
18
+ its(:to_a) { should == [event, event, event] }
19
+
20
+ it 'should log invalid inputs' do
21
+ io = StringIO.new [event.to_tsv, "ABCD", event.to_tsv].join("\n")
22
+ buffer.stub(:drain).and_yield(io)
23
+ subject.logger.should_receive(:warn).at_least(:once)
24
+ subject.to_a.should == [event, event]
25
+ end
26
+
27
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Handler::Base do
4
+
5
+ subject { described_class.new reactor }
6
+
7
+ it { should respond_to(:on_events) }
8
+ it { should be_a(FluQ::Mixins::Loggable) }
9
+ its(:reactor) { should be(reactor) }
10
+ its(:config) { should == { pattern: /./ } }
11
+ its(:pattern) { should == /./ }
12
+ its(:name) { should == "base-AxPGxv" }
13
+
14
+ def events(*tags)
15
+ tags.map {|tag| event(tag) }
16
+ end
17
+
18
+ def event(tag)
19
+ FluQ::Event.new(tag, 1313131313, {})
20
+ end
21
+
22
+ it 'should have a type' do
23
+ described_class.type.should == "base"
24
+ end
25
+
26
+ it 'can have custom names' do
27
+ described_class.new(reactor, name: "visitors").name.should == "visitors"
28
+ end
29
+
30
+ it 'should match tags via patters' do
31
+ subject = described_class.new(reactor, pattern: "visits.????.*")
32
+ subject.match?(event("visits.site.1")).should be(true)
33
+ subject.match?(event("visits.page.2")).should be(true)
34
+ subject.match?(event("visits.other.1")).should be(false)
35
+ subject.match?(event("visits.site")).should be(false)
36
+ subject.match?(event("visits.site.")).should be(true)
37
+ subject.match?(event("prefix.visits.site.1")).should be(false)
38
+ subject.match?(event("visits.site.1.suffix")).should be(true)
39
+ end
40
+
41
+ it 'should support "or" patterns' do
42
+ subject = described_class.new(reactor, pattern: "visits.{site,page}.*")
43
+ subject.match?(event("visits.site.1")).should be(true)
44
+ subject.match?(event("visits.page.2")).should be(true)
45
+ subject.match?(event("visits.other.1")).should be(false)
46
+ subject.match?(event("visits.site")).should be(false)
47
+ subject.match?(event("visits.site.")).should be(true)
48
+ subject.match?(event("prefix.visits.site.1")).should be(false)
49
+ subject.match?(event("visits.site.1.suffix")).should be(true)
50
+ end
51
+
52
+ it 'should support regular expression patterns' do
53
+ subject = described_class.new(reactor, pattern: /^visits\.(?:s|p)\w{3}\..*/)
54
+ subject.match?(event("visits.site.1")).should be(true)
55
+ subject.match?(event("visits.page.2")).should be(true)
56
+ subject.match?(event("visits.other.1")).should be(false)
57
+ subject.match?(event("visits.site")).should be(false)
58
+ subject.match?(event("visits.site.")).should be(true)
59
+ subject.match?(event("prefix.visits.site.1")).should be(false)
60
+ subject.match?(event("visits.site.1.suffix")).should be(true)
61
+ end
62
+
63
+ it 'should select events' do
64
+ stream = events("visits.site.1", "visits.page.2", "visits.other.1", "visits.site.2")
65
+ described_class.new(reactor, pattern: "visits.????.*").select(stream).map(&:tag).should == [
66
+ "visits.site.1", "visits.page.2", "visits.site.2"
67
+ ]
68
+ end
69
+
70
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Handler::Log do
4
+
5
+ let(:event) do
6
+ FluQ::Event.new("my.special.tag", 1313131313, { "a" => "1" })
7
+ end
8
+ let(:root) { FluQ.root.join("../scenario/log/raw") }
9
+ subject { described_class.new reactor }
10
+ before { FileUtils.rm_rf(root); FileUtils.mkdir_p(root) }
11
+
12
+ it { should be_a(FluQ::Handler::Base) }
13
+ its("config.keys") { should =~ [:convert, :path, :pattern, :rewrite, :cache_max, :cache_ttl] }
14
+
15
+ it "can log events" do
16
+ subject.on_events [event]
17
+ subject.pool.each_key {|k| subject.pool[k].flush }
18
+ root.join("my/special/tag/20110812/06.log").read.should == %(my.special.tag\t1313131313\t{"a":"1"}\n)
19
+ end
20
+
21
+ it 'can have custom conversions' do
22
+ subject = described_class.new reactor, convert: lambda {|e| e.merge(ts: e.timestamp).map {|k,v| "#{k}=#{v}" }.join(',') }
23
+ subject.on_events [event]
24
+ subject.pool.each_key {|k| subject.pool[k].flush }
25
+ root.join("my/special/tag/20110812/06.log").read.should == "a=1,ts=1313131313\n"
26
+ end
27
+
28
+ it 'can rewrite tags' do
29
+ subject = described_class.new reactor, rewrite: lambda {|t| t.split('.').reverse.first(2).join(".") }
30
+ subject.on_events [event]
31
+ root.join("tag.special/20110812/06.log").should be_file
32
+ end
33
+
34
+ it 'should not fail on temporary file errors' do
35
+ subject.on_events [event]
36
+ subject.pool.each_key {|k| subject.pool[k].close }
37
+ subject.on_events [event]
38
+ subject.pool.each_key {|k| subject.pool[k].flush }
39
+ root.join("my/special/tag/20110812/06.log").read.should have(2).lines
40
+ end
41
+
42
+ describe described_class::FilePool do
43
+ subject { described_class::FilePool.new(max_size: 2) }
44
+ let(:path) { root.join("a.log") }
45
+
46
+ it { should be_a(TimedLRU) }
47
+
48
+ it 'should open files' do
49
+ lambda {
50
+ subject.open(path).should be_instance_of(File)
51
+ }.should change { subject.keys }.from([]).to([path.to_s])
52
+ end
53
+
54
+ it 'should re-use open files' do
55
+ fd = subject.open(path)
56
+ lambda {
57
+ subject.open(path).should be(fd)
58
+ }.should_not change { subject.keys }
59
+ end
60
+
61
+ it 'should auto-close files' do
62
+ fd = subject.open(path)
63
+ fd.should be_autoclose
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Handler::Null do
4
+
5
+ subject { described_class.new reactor }
6
+
7
+ it 'should handle events' do
8
+ subject.on_events []
9
+ end
10
+
11
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Input::Base do
4
+
5
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
6
+ let!(:handler) { reactor.register FluQ::Handler::Test }
7
+ subject { described_class.new(reactor, feed: "json") }
8
+
9
+ it { should be_a(FluQ::Mixins::Loggable) }
10
+ its(:reactor) { should be(reactor) }
11
+ its(:config) { should == {feed: "json", buffer: "file", buffer_options: {}} }
12
+ its(:name) { should == "base" }
13
+ its(:feed_klass) { should == FluQ::Feed::Json }
14
+ its(:buffer_klass) { should == FluQ::Buffer::File }
15
+
16
+ it 'should create new buffers' do
17
+ (b1 = subject.new_buffer).should be_instance_of(FluQ::Buffer::File)
18
+ (b2 = subject.new_buffer).should be_instance_of(FluQ::Buffer::File)
19
+ b1.should_not be(b2)
20
+ end
21
+
22
+ it 'should flush buffers' do
23
+ buf = subject.new_buffer
24
+ buf.write [event, event].map(&:to_json).join("\n")
25
+ subject.flush!(buf)
26
+ handler.should have(2).events
27
+ end
28
+
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Input::Socket::Connection do
4
+
5
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
6
+ let(:input) { FluQ::Input::Socket.new reactor, bind: "tcp://127.0.0.1:26712" }
7
+ before { EventMachine.stub(:set_comm_inactivity_timeout) }
8
+ subject { described_class.new(Time.now.to_i, input) }
9
+
10
+ it { should be_a(EM::Connection) }
11
+
12
+ it 'should set a timeout' do
13
+ EventMachine.should_receive(:set_comm_inactivity_timeout).with(instance_of(Fixnum), 60)
14
+ subject
15
+ end
16
+
17
+ it 'should handle data' do
18
+ subject.receive_data [event, event].map(&:to_msgpack).join
19
+ subject.send(:buffer).size.should == 38
20
+ end
21
+
22
+ it 'should flush when data transfer is complete' do
23
+ subject.receive_data [event, event].map(&:to_msgpack).join
24
+ input.should_receive(:flush!).with(instance_of(FluQ::Buffer::File))
25
+ subject.unbind
26
+ end
27
+
28
+ it 'should recover connection errors' do
29
+ reactor.should_receive(:process).and_raise(Errno::ECONNRESET)
30
+ FluQ.logger.should_receive(:crash)
31
+ subject.receive_data [event, event].map(&:to_msgpack).join
32
+ subject.unbind
33
+ end
34
+
35
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Input::Socket do
4
+
5
+ let(:event) { FluQ::Event.new("some.tag", 1313131313, {}) }
6
+
7
+ def input(reactor)
8
+ described_class.new(reactor, bind: "tcp://127.0.0.1:26712")
9
+ end
10
+
11
+ subject { input(reactor) }
12
+ it { should be_a(FluQ::Input::Base) }
13
+ its(:name) { should == "socket (tcp://127.0.0.1:26712)" }
14
+ its(:config) { should == {feed: "msgpack", buffer: "file", buffer_options: {}, bind: "tcp://127.0.0.1:26712"} }
15
+
16
+ it 'should require bind option' do
17
+ lambda { described_class.new(reactor) }.should raise_error(ArgumentError, /No URL to bind/)
18
+ end
19
+
20
+ it 'should handle requests' do
21
+ with_reactor do |reactor|
22
+ server = input(reactor)
23
+ lambda { TCPSocket.open("127.0.0.1", 26712) }.should raise_error(Errno::ECONNREFUSED)
24
+
25
+ server.run
26
+ client = TCPSocket.open("127.0.0.1", 26712)
27
+
28
+ client.write event.to_msgpack
29
+ client.close
30
+ end
31
+ end
32
+
33
+ it 'should support UDP' do
34
+ h = nil
35
+ with_reactor do |reactor|
36
+ h = reactor.register FluQ::Handler::Test
37
+ reactor.listen described_class, bind: "udp://127.0.0.1:26713"
38
+ client = UDPSocket.new
39
+ client.send event.to_msgpack, 0, "127.0.0.1", 26713
40
+ client.close
41
+ end
42
+ h.should have(1).events
43
+ end
44
+
45
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Mixins::Loggable do
4
+
5
+ subject { FluQ::Handler::Base.new reactor }
6
+
7
+ it { should be_a(described_class) }
8
+ its(:logger) { should be(FluQ.logger) }
9
+
10
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Mixins::Logger do
4
+
5
+ subject do
6
+ logger = Logger.new("/dev/null")
7
+ logger.extend described_class
8
+ logger
9
+ end
10
+
11
+ its(:exception_handlers) { should == [] }
12
+
13
+ it 'should register handlers' do
14
+ subject.exception_handler {|*| }
15
+ subject.should have(1).exception_handlers
16
+ end
17
+
18
+ it 'should apply handlers on crash' do
19
+ str = ""
20
+ subject.exception_handler {|ex| str << ex.message }
21
+ subject.crash("error", StandardError.new("something"))
22
+ str.should == "something"
23
+ end
24
+
25
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe FluQ::Reactor do
4
+
5
+ its(:handlers) { should == [] }
6
+ its(:inputs) { should == [] }
7
+
8
+ def events(*tags)
9
+ tags.map do |tag|
10
+ FluQ::Event.new(tag, 1313131313, {})
11
+ end
12
+ end
13
+
14
+ it "should listen to inputs" do
15
+ with_reactor do |subject|
16
+ subject.listen(FluQ::Input::Socket, bind: "tcp://127.0.0.1:7654")
17
+ subject.should have(1).inputs
18
+ end
19
+ end
20
+
21
+ it "should register handlers" do
22
+ h1 = subject.register(FluQ::Handler::Test)
23
+ subject.should have(1).handlers
24
+
25
+ h2 = subject.register(FluQ::Handler::Test, name: "specific")
26
+ subject.should have(2).handlers
27
+ end
28
+
29
+ it "should prevent duplicates" do
30
+ subject.register(FluQ::Handler::Test)
31
+ lambda {
32
+ subject.register(FluQ::Handler::Test)
33
+ }.should raise_error(ArgumentError)
34
+ end
35
+
36
+ it "should process events" do
37
+ h1 = subject.register(FluQ::Handler::Test)
38
+ h2 = subject.register(FluQ::Handler::Test, pattern: "NONE")
39
+ subject.process(events("tag")).should be(true)
40
+ h1.events.should == [["tag", 1313131313, {}]]
41
+ h2.events.should == []
42
+ end
43
+
44
+ it "should skip not matching events" do
45
+ h1 = subject.register(FluQ::Handler::Test, pattern: "some*")
46
+ subject.process(events("some.tag", "other.tag", "something.else")).should be(true)
47
+ h1.events.should == [["some.tag", 1313131313, {}], ["something.else", 1313131313, {}]]
48
+ end
49
+
50
+ it "should recover crashed handlers gracefully" do
51
+ h1 = subject.register(FluQ::Handler::Test)
52
+ 10.times { subject.process(events("ok.now")) }
53
+ subject.process(events("error.event"))
54
+ 10.times { subject.process(events("ok.now")) }
55
+ h1.should have(20).events
56
+ end
57
+
58
+ end