logstash-output-cassandra 0.9.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.
@@ -0,0 +1,35 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-output-cassandra'
4
+ s.version = '0.9.0'
5
+ s.licenses = [ 'Apache License (2.0)' ]
6
+ s.summary = 'Store events into Cassandra'
7
+ s.description = 'This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program'
8
+ s.authors = [ 'PerimeterX' ]
9
+ s.email = [ 'elad@perimeterx.com' ]
10
+ s.homepage = 'https://github.com/PerimeterX/logstash-output-cassandra'
11
+ s.require_paths = [ 'lib' ]
12
+
13
+ # Files
14
+ s.files = Dir[ 'lib/**/*', 'spec/**/*', 'vendor/**/*', '*.gemspec', '*.md', 'CONTRIBUTORS', 'Gemfile', 'LICENSE', 'NOTICE.TXT' ]
15
+ # Tests
16
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
+
18
+ # Special flag to let us know this is actually a logstash plugin
19
+ s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'output' }
20
+
21
+ # Gem dependencies
22
+ s.add_runtime_dependency 'concurrent-ruby'
23
+ s.add_runtime_dependency 'logstash-core', '>= 2.0.0', '< 3.0.0'
24
+ s.add_runtime_dependency 'cassandra-driver', '>= 2.0.0', '< 3.0.0'
25
+ s.add_development_dependency 'cabin', ['~> 0.6']
26
+ s.add_development_dependency 'longshoreman'
27
+ s.add_development_dependency 'logstash-devutils'
28
+ s.add_development_dependency 'logstash-codec-plain'
29
+ s.add_development_dependency 'simplecov'
30
+ s.add_development_dependency 'simplecov-rcov'
31
+ s.add_development_dependency 'unparser', '0.2.4'
32
+ s.add_development_dependency 'metric_fu'
33
+ s.add_development_dependency 'coveralls'
34
+ s.add_development_dependency 'gems'
35
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ require 'logstash/devutils/rspec/spec_helper'
3
+ require 'logstash/event'
4
+ require 'simplecov'
5
+ require 'simplecov-rcov'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ SimpleCov::Formatter::RcovFormatter
10
+ ])
11
+
12
+ SimpleCov.start do
13
+ add_filter '/spec/'
14
+ end
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+ require_relative './integration_helper'
3
+ require 'logstash/outputs/cassandra'
4
+
5
+ module Helper
6
+ def self.get_assert_timestamp_equallity
7
+ Proc.new do |expect, row, type_to_test|
8
+ expect.call(row['value_column'].to_s).to(eq(Time.at(type_to_test[:value]).to_s))
9
+ end
10
+ end
11
+
12
+ def self.get_assert_set_equallity
13
+ Proc.new do |expect, row, type_to_test|
14
+ set_from_cassandra = row['value_column']
15
+ original_value = type_to_test[:value]
16
+ expect.call(set_from_cassandra.size).to(eq(original_value.size))
17
+ set_from_cassandra.to_a.each { |item|
18
+ expect.call(original_value).to(include(item.to_s))
19
+ }
20
+ end
21
+ end
22
+ end
23
+
24
+ describe 'client create actions', :docker => true do
25
+ before(:each) do
26
+ get_session.execute("CREATE KEYSPACE test WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };")
27
+ end
28
+
29
+ after(:each) do
30
+ get_session.execute('DROP KEYSPACE test;')
31
+ end
32
+
33
+ def get_sut
34
+ options = {
35
+ 'hosts' => [get_host_ip],
36
+ 'port' => get_port,
37
+ 'keyspace' => 'test',
38
+ 'table' => '%{[cassandra_table]}',
39
+ 'username' => 'cassandra',
40
+ 'password' => 'cassandra',
41
+ 'filter_transform_event_key' => 'cassandra_filter'
42
+ }
43
+ sut = LogStash::Outputs::CassandraOutput.new(options)
44
+ return sut
45
+ end
46
+
47
+ def create_table(type_to_test)
48
+ get_session.execute("
49
+ CREATE TABLE test.simple(
50
+ idish_column text,
51
+ value_column #{type_to_test[:type]},
52
+ PRIMARY KEY (idish_column)
53
+ );")
54
+ end
55
+
56
+ def build_event(type_to_test)
57
+ options = {
58
+ 'cassandra_table' => 'simple',
59
+ 'idish_field' => 'some text',
60
+ 'value_field' => type_to_test[:value],
61
+ 'cassandra_filter' => [
62
+ { 'event_key' => 'idish_field', 'column_name' => 'idish_column' },
63
+ { 'event_key' => 'value_field', 'column_name' => 'value_column', 'cassandra_type' => type_to_test[:type] }
64
+ ]
65
+ }
66
+ LogStash::Event.new(options)
67
+ end
68
+
69
+ def assert_proper_insert(type_to_test)
70
+ result = get_session.execute('SELECT * FROM test.simple')
71
+ expect(result.size).to((eq(1)))
72
+ result.each { |row|
73
+ expect(row['idish_column']).to(eq('some text'))
74
+ if type_to_test.has_key?(:assert_override)
75
+ expect_proc = Proc.new do |value|
76
+ return expect(value)
77
+ end
78
+ type_to_test[:assert_override].call(expect_proc, row, type_to_test)
79
+ else
80
+ expect(row['value_column'].to_s).to(eq(type_to_test[:value].to_s))
81
+ end
82
+ }
83
+ end
84
+
85
+ [
86
+ { type: 'timestamp', value: 1457606758, assert_override: Helper::get_assert_timestamp_equallity() },
87
+ { type: 'inet', value: '192.168.99.100' },
88
+ { type: 'float', value: '10.050000190734863' },
89
+ { type: 'varchar', value: 'some chars' },
90
+ { type: 'text', value: 'some text' },
91
+ { type: 'blob', value: 'a blob' },
92
+ { type: 'ascii', value: 'some ascii' },
93
+ { type: 'bigint', value: '123456789' },
94
+ { type: 'int', value: '12345' },
95
+ { type: 'varint', value: '12345678' },
96
+ { type: 'boolean', value: 'true' },
97
+ { type: 'decimal', value: '0.1015E2' },
98
+ { type: 'double', value: '200.54' },
99
+ { type: 'timeuuid', value: 'd2177dd0-eaa2-11de-a572-001b779c76e3' },
100
+ { type: 'set<timeuuid>',
101
+ value: %w(d2177dd0-eaa2-11de-a572-001b779c76e3 d2177dd0-eaa2-11de-a572-001b779c76e4 d2177dd0-eaa2-11de-a572-001b779c76e5), assert_override: Helper::get_assert_set_equallity }
102
+ ].each { |type_to_test|
103
+ it "properly inserts data of type #{type_to_test[:type]}" do
104
+ create_table(type_to_test)
105
+ sut = get_sut
106
+ sut.register
107
+ event = build_event(type_to_test)
108
+
109
+ sut.receive(event)
110
+ sut.flush
111
+
112
+ assert_proper_insert(type_to_test)
113
+ end
114
+ }
115
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+ require_relative '../../cassandra_spec_helper'
3
+ require 'longshoreman'
4
+ require 'cassandra'
5
+
6
+ CONTAINER_NAME = "logstash-output-cassandra-#{rand(999).to_s}"
7
+ CONTAINER_IMAGE = 'cassandra'
8
+ CONTAINER_TAG = '2.2'
9
+
10
+ module CassandraHelper
11
+ def get_host_ip
12
+ Longshoreman.new.get_host_ip
13
+ end
14
+
15
+ def get_port
16
+ container = Longshoreman::Container.new
17
+ container.get(CONTAINER_NAME)
18
+ container.rport(9042)
19
+ end
20
+
21
+ def get_session
22
+ cluster = ::Cassandra.cluster(
23
+ username: 'cassandra',
24
+ password: 'cassandra',
25
+ port: get_port,
26
+ hosts: [get_host_ip]
27
+ )
28
+ cluster.connect
29
+ end
30
+ end
31
+
32
+
33
+ RSpec.configure do |config|
34
+ config.include CassandraHelper
35
+
36
+ # this :all hook gets run before every describe block that is tagged with :integration => true.
37
+ config.before(:all, :docker => true) do
38
+ # check if container exists already before creating new one.
39
+ begin
40
+ ls = Longshoreman::new
41
+ ls.container.get(CONTAINER_NAME)
42
+ rescue Docker::Error::NotFoundError
43
+ create_retry = 0
44
+ begin
45
+ Longshoreman.new("#{CONTAINER_IMAGE}:#{CONTAINER_TAG}", CONTAINER_NAME, {
46
+ 'HostConfig' => {
47
+ 'PublishAllPorts' => true
48
+ }
49
+ })
50
+ connect_retry = 0
51
+ begin
52
+ get_session
53
+ rescue ::Cassandra::Errors::NoHostsAvailable
54
+ # retry connecting for a minute
55
+ connect_retry += 1
56
+ if connect_retry <= 60
57
+ sleep(1)
58
+ retry
59
+ else
60
+ raise
61
+ end
62
+ end
63
+ rescue Docker::Error::NotFoundError
64
+ # try to pull the image once if it does not exist
65
+ create_retry += 1
66
+ if create_retry <= 1
67
+ Longshoreman.pull_image(CONTAINER_IMAGE, CONTAINER_TAG)
68
+ retry
69
+ else
70
+ raise
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # we want to do a final cleanup after all :integration runs,
77
+ # but we don't want to clean up before the last block.
78
+ # This is a final blind check to see if the ES docker container is running and
79
+ # needs to be cleaned up. If no container can be found and/or docker is not
80
+ # running on the system, we do nothing.
81
+ config.after(:suite) do
82
+ # only cleanup docker container if system has docker and the container is running
83
+ begin
84
+ ls = Longshoreman::new
85
+ ls.container.get(CONTAINER_NAME)
86
+ ls.cleanup
87
+ rescue Docker::Error::NotFoundError, Excon::Errors::SocketError
88
+ # do nothing
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+ require_relative '../../cassandra_spec_helper'
3
+ require 'logstash/outputs/cassandra/backoff_retry_policy'
4
+
5
+ RSpec.describe ::Cassandra::Retry::Policies::Backoff do
6
+ let(:sut) { ::Cassandra::Retry::Policies::Backoff }
7
+ let(:linear_backoff) {
8
+ logger = double
9
+ allow(logger).to(receive(:error))
10
+ {
11
+ 'logger' => logger,
12
+ 'backoff_type' => '*',
13
+ 'backoff_size' => 5,
14
+ 'retry_limit' => 10
15
+ }
16
+ }
17
+ let(:exponential_backoff) {
18
+ linear_backoff.merge({
19
+ 'backoff_type' => '**',
20
+ 'backoff_size' => 2,
21
+ 'retry_limit' => 10
22
+ })
23
+ }
24
+
25
+ describe '#retry_with_backoff' do
26
+ describe 'retry limit not reached' do
27
+ it 'decides to try again with the same consistency level' do
28
+ sut_instance = sut.new(linear_backoff)
29
+
30
+ decision = sut_instance.retry_with_backoff({ :retries => 0, :consistency => :one })
31
+
32
+ expect(decision).to(be_an_instance_of(::Cassandra::Retry::Decisions::Retry))
33
+ expect(decision.consistency).to(be(:one))
34
+ end
35
+
36
+ it 'waits _before_ retrying' do
37
+ sut_instance = sut.new(linear_backoff)
38
+ expect(Kernel).to(receive(:sleep))
39
+
40
+ sut_instance.retry_with_backoff({ :retries => 0 })
41
+ end
42
+
43
+ it 'allows for an infinite amount of retries if configured with -1 as the retry limit' do
44
+ sut_instance = sut.new(linear_backoff.merge({ 'retry_limit' => -1 }))
45
+ expect(Kernel).to(receive(:sleep))
46
+
47
+ sut_instance.retry_with_backoff({ :retries => 1000000 })
48
+ end
49
+
50
+ it 'allows for exponential backoffs' do
51
+ sut_instance = sut.new(exponential_backoff)
52
+ test_retry = exponential_backoff['retry_limit'] - 1
53
+ expect(Kernel).to(receive(:sleep).with(exponential_backoff['backoff_size'] ** test_retry))
54
+
55
+ sut_instance.retry_with_backoff({ :retries => test_retry })
56
+ end
57
+
58
+ it 'allows for linear backoffs' do
59
+ sut_instance = sut.new(linear_backoff)
60
+ test_retry = exponential_backoff['retry_limit'] - 1
61
+ expect(Kernel).to(receive(:sleep).with(linear_backoff['backoff_size'] * test_retry))
62
+
63
+ sut_instance.retry_with_backoff({ :retries => test_retry })
64
+ end
65
+
66
+ it 'fails for unknown backoff types' do
67
+ sut_instance = sut.new(linear_backoff.merge({ 'backoff_type' => '^' }))
68
+
69
+ expect { sut_instance.retry_with_backoff({ :retries => 0}) }.to raise_error ArgumentError
70
+ end
71
+ end
72
+
73
+ describe 'retry limit reached' do
74
+ it 'decides to reraise' do
75
+ sut_instance = sut.new(linear_backoff)
76
+
77
+ decision = sut_instance.retry_with_backoff({ :retries => linear_backoff['retry_limit'] + 1 })
78
+
79
+ expect(decision).to(be_an_instance_of(::Cassandra::Retry::Decisions::Reraise))
80
+ end
81
+
82
+ it 'does not wait' do
83
+ sut_instance = sut.new(linear_backoff)
84
+
85
+ expect(Kernel).not_to(receive(:sleep))
86
+
87
+ sut_instance.retry_with_backoff({ :retries => linear_backoff['retry_limit'] + 1 })
88
+ end
89
+ end
90
+ end
91
+
92
+ [
93
+ {
94
+ :method_name=> 'read_timeout',
95
+ :expected_opts => { :statement => 'statement', :consistency => :one, :required => 1, :received => 0,
96
+ :retrieved => false, :retries => 0 },
97
+ :call_args => ['statement', :one, 1, 0, false, 0]
98
+ },
99
+ {
100
+ :method_name=> 'write_timeout',
101
+ :expected_opts => { :statement => 'statement', :consistency => :one, :type => :prepared,
102
+ :required => 1, :received => 2, :retries => 5 },
103
+ :call_args => ['statement', :one, :prepared, 1, 2, 5]
104
+ },
105
+ {
106
+ :method_name=> 'unavailable',
107
+ :expected_opts => { :statement => 'statement', :consistency => :one, :required => 3,
108
+ :alive => 2, :retries => 4},
109
+ :call_args => ['statement', :one, 3, 2, 4]
110
+ }
111
+ ].each { |use_case|
112
+ describe '#{use_case[:method_name]}' do
113
+ it 'properly calls #retry_with_backoff' do
114
+ sut_instance = sut.new(linear_backoff)
115
+ expect(sut_instance).to(receive(:retry_with_backoff).with(use_case[:expected_opts]))
116
+
117
+ sut_instance.send(use_case[:method_name], *use_case[:call_args])
118
+ end
119
+
120
+ it 'returns the decision it got' do
121
+ sut_instance = sut.new(linear_backoff)
122
+ expected_result = double
123
+ expect(sut_instance).to(receive(:retry_with_backoff).and_return(expected_result))
124
+
125
+ result = sut_instance.send(use_case[:method_name], *use_case[:call_args])
126
+
127
+ expect(result).to(be(expected_result))
128
+ end
129
+ end
130
+ }
131
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/cassandra/buffer"
3
+ require "cabin"
4
+
5
+ describe LogStash::Outputs::Cassandra::Buffer do
6
+ class OperationTarget # Used to track buffer flushesn
7
+ attr_reader :buffer, :buffer_history, :receive_count
8
+ def initialize
9
+ @buffer = nil
10
+ @buffer_history = []
11
+ @receive_count = 0
12
+ end
13
+
14
+ def receive(buffer)
15
+ @receive_count += 1
16
+ @buffer_history << buffer.clone
17
+ @buffer = buffer
18
+ end
19
+ end
20
+
21
+ let(:logger) { Cabin::Channel.get }
22
+ let(:max_size) { 10 }
23
+ let(:flush_interval) { 2 }
24
+ # Used to track flush count
25
+ let(:operation_target) { OperationTarget.new() }
26
+ let(:operation) { proc {|buffer| operation_target.receive(buffer) } }
27
+ subject(:buffer){ LogStash::Outputs::Cassandra::Buffer.new(logger, max_size, flush_interval, &operation) }
28
+
29
+ after(:each) do
30
+ buffer.stop(do_flush=false)
31
+ end
32
+
33
+ it "should initialize cleanly" do
34
+ expect(buffer).to be_a(LogStash::Outputs::Cassandra::Buffer)
35
+ end
36
+
37
+ shared_examples("a buffer with two items inside") do
38
+ it "should add a pushed item to the buffer" do
39
+ buffer.synchronize do |data|
40
+ expect(data).to include(item1)
41
+ expect(data).to include(item2)
42
+ end
43
+ end
44
+
45
+ describe "interval flushing" do
46
+ before do
47
+ sleep flush_interval + 1
48
+ end
49
+
50
+ it "should flush the buffer after the interval has passed" do
51
+ expect(operation_target.receive_count).to eql(1)
52
+ end
53
+
54
+ it "should clear the buffer after a successful flush" do
55
+ expect(operation_target.buffer).to eql([])
56
+ end
57
+ end
58
+
59
+ describe "interval flushing a stopped buffer" do
60
+ before do
61
+ buffer.stop(do_flush=false)
62
+ sleep flush_interval + 1
63
+ end
64
+
65
+ it "should not flush if the buffer is stopped" do
66
+ expect(operation_target.receive_count).to eql(0)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "with a buffer push" do
72
+ let(:item1) { "foo" }
73
+ let(:item2) { "bar" }
74
+
75
+ describe "a buffer with two items pushed to it separately" do
76
+ before do
77
+ buffer << item1
78
+ buffer << item2
79
+ end
80
+
81
+ include_examples("a buffer with two items inside")
82
+ end
83
+
84
+ describe "a buffer with two items pushed to it in one operation" do
85
+ before do
86
+ buffer.push_multi([item1, item2])
87
+ end
88
+
89
+ include_examples("a buffer with two items inside")
90
+ end
91
+ end
92
+
93
+ describe "with an empty buffer" do
94
+ it "should not perform an operation if the buffer is empty" do
95
+ buffer.flush
96
+ expect(operation_target.receive_count).to eql(0)
97
+ end
98
+ end
99
+
100
+ describe "flushing with an operation that raises an error" do
101
+ class TestError < StandardError; end
102
+ let(:operation) { proc {|buffer| raise TestError, "A test" } }
103
+ let(:item) { double("item") }
104
+
105
+ before do
106
+ buffer << item
107
+ end
108
+
109
+ it "should raise an exception" do
110
+ expect { buffer.flush }.to raise_error(TestError)
111
+ end
112
+
113
+ it "should not clear the buffer" do
114
+ expect do
115
+ buffer.flush rescue TestError
116
+ end.not_to change(buffer, :contents)
117
+ end
118
+ end
119
+ end