krump 0.3.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f8ef5b85176e1456ef7da988aebb76654794be6c
4
+ data.tar.gz: 1249ca52f23eb3fa715838c563e053423ff88654
5
+ SHA512:
6
+ metadata.gz: f51b81533a0946b478a2f4977444f6737d54c26fa05fc1a1f99d1788e6a5d76e49254b1629ff5f9148bf602bcd0499adde582bf56b555d1965fb5210621dd405
7
+ data.tar.gz: af0993ef4d7219585e66a462a1caa4eecaee941d241c26866967b85a16b61d070623ba8c2772e92f118755de9203be8e18ccb49cb807ec0d2ecdb6bfee633dfd
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ 2.2.2
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in krump.gemspec
4
+ gemspec
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2016, Funding Circle Ltd.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,117 @@
1
+ # Krump
2
+
3
+ A Kafka consumer focused on convenience.
4
+
5
+ This application was written because of a need for a `tail`-like way to consume Kafka messages. `kafka-console-consumer.sh`, which is distributed with Kafka, allows you to either read _all_ messages in a topic, or any _new_ message. However, most often I want to to see the most recent few messages from a topic.
6
+
7
+ `krump` provides that as well as a number of other conveniences.
8
+
9
+ ## Feature Overview
10
+
11
+ 1. See the most recent `n` messages in a topic
12
+ 2. See a specific range of messages
13
+ 3. See messages from specific partitions
14
+ 4. Get a count of messages for a particular topic/partition
15
+ 5. Automatically set up SSH tunnels so it can be run locally
16
+
17
+ ## Installation
18
+
19
+ $ gem install krump
20
+
21
+ ## Usage
22
+
23
+ Here are some examples on how to use the application. In these examples no broker information is given so it just connects to `localhost:9092`.
24
+
25
+ **See the most recent 2 messages from the topic `sometopic`:**
26
+
27
+ ```bash
28
+ $ krump --topic sometopic --offset -2
29
+ ===== Topic: sometopic = Partition: 0 =======
30
+ {"id":"111"}
31
+ {"id":"222"}
32
+ ===== Topic: sometopic = Partition: 1 =======
33
+ {"id":"333"}
34
+ {"id":"444"}
35
+ ===== Topic: sometopic = Partition: 2 =======
36
+ {"id":"555"}
37
+ {"id":"666"}
38
+ ===== Topic: sometopic = Partition: 3 =======
39
+ {"id":"777"}
40
+ {"id":"888"}
41
+ ```
42
+
43
+ **See 3 messages starting at offset 100 on partitions 1 & 2:**
44
+
45
+ ```bash
46
+ $ krump --topic sometopic --partitions 1 2 --offset 100 --read-count 3
47
+ ===== Topic: sometopic = Partition: 0 =======
48
+ {"id":"123"}
49
+ {"id":"456"}
50
+ {"id":"789"}
51
+ ===== Topic: sometopic = Partition: 1 =======
52
+ {"id":"abc"}
53
+ {"id":"def"}
54
+ {"id":"fff"}
55
+ ```
56
+
57
+ **Show how many messages are in each partition:**
58
+
59
+ ```bash
60
+ $ krump --topic sometopic --count-messages
61
+ sometopic | Partition 0 | 29043 messages
62
+ sometopic | Partition 1 | 29776 messages
63
+ sometopic | Partition 2 | 29118 messages
64
+ sometopic | Partition 3 | 27406 messages
65
+ ```
66
+
67
+ Use `krump --help` to see all options.
68
+
69
+ ### Config File
70
+
71
+ You can set configurations for different environments in the config file (`~/.krump` by default).
72
+
73
+ This is especially useful if your cluster is behind a gateway server (e.g. an AWS VPC).
74
+
75
+ Example config file:
76
+
77
+ # Directly access a Kafka cluster on the Internet
78
+ dev.kafka_broker=51.120.33.24:9092
79
+
80
+ # Or connect to a Kafka cluster behind a gateway server
81
+ staging.gateway_hostname=51.150.99.142
82
+ staging.gateway_user=ec2-user
83
+ staging.gateway_identityfile=~/.ssh/mykey.pem
84
+ staging.kafka_broker=10.0.100.1:9092
85
+ staging.kafka_broker=10.0.100.2:9092
86
+ staging.kafka_broker=10.0.100.3:9092
87
+
88
+ # You can also use a host alias and it will get the connection info from
89
+ # /etc/ssh/config or ~/.ssh/config
90
+ uat.gateway_host=uat-bastion
91
+ uat.kafka_broker=10.0.105.1:9092
92
+ uat.kafka_broker=10.0.105.2:9092
93
+ uat.kafka_broker=10.0.105.3:9092
94
+
95
+ Use the `--environment` flag to use the connection settings for that environment, for example:
96
+
97
+ ```bash
98
+ $ krump --environment staging --topic sometopic --latest-offset
99
+ ```
100
+
101
+ If an environment's settings include gateway particulars, `krump` will handle setting up temporary SSH tunnels.
102
+
103
+
104
+ ## Development
105
+
106
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
107
+
108
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
109
+
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it ( https://github.com/[my-github-username]/krump/fork )
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create a new Pull Request
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+
4
+ task :default => [:spec]
5
+ spec = RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "krump"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env ruby
2
+ require 'net/ssh'
3
+ require 'trollop'
4
+ require 'krump/config_parser'
5
+ require 'krump/kafka_consumer'
6
+ require 'krump/local_open_port'
7
+ require 'krump/ssh_tunnels'
8
+
9
+
10
+ def main(config, retries_left = 10)
11
+ if config[:gateway_hostname]
12
+ ssh_tunnels = Krump::SshTunnels.new(
13
+ config[:gateway_hostname],
14
+ config[:gateway_user],
15
+ config[:gateway_identityfile],
16
+ config[:ssh_tunnel_info]
17
+ )
18
+ ssh_tunnels.open { run_task(config) }
19
+ else
20
+ run_task(config)
21
+ end
22
+
23
+ # Between the time a local port is assigned for an SSH tunnel and that tunnel is actually
24
+ # opened, it's possible for a different application to use that port.
25
+ rescue Errno::EADDRINUSE => e
26
+ raise e if retries_left == 0
27
+ set_new_port_for_port_in_use!(config, e)
28
+ retries_left =- 1
29
+ retry
30
+
31
+ rescue Interrupt
32
+ end
33
+
34
+
35
+ def set_new_port_for_port_in_use!(config, e)
36
+ port_in_use = e.message[/port \d{5}/].split.last.to_i
37
+ new_port = Krump::LocalOpenPort.find
38
+
39
+ config[:brokers] = config[:brokers].map do |broker|
40
+ broker.split(':').last.to_i == port_in_use ? "localhost:#{new_port}" : broker
41
+ end
42
+
43
+ config[:ssh_tunnel_info] = config[:ssh_tunnel_info].map do |info|
44
+ info.local_port = new_port if info.local_port == port_in_use
45
+ info
46
+ end
47
+ end
48
+
49
+
50
+ def init_config
51
+ cmd_line_opts = parse_opts
52
+ config = Krump::ConfigParser.new(cmd_line_opts[:config_file],
53
+ cmd_line_opts[:environment]).parse
54
+
55
+ # Command line options will supercede those in the config file
56
+ cmd_line_opts.keys.each do |key|
57
+ config[key] = cmd_line_opts[key] unless cmd_line_opts[key].nil?
58
+ end
59
+
60
+ if config[:gateway_host]
61
+ host_config = Net::SSH::Config.for(config[:gateway_host])
62
+ config[:gateway_hostname] = host_config[:host_name]
63
+ config[:gateway_user] = host_config[:user]
64
+ config[:gateway_identityfile] = host_config[:keys].first
65
+ end
66
+
67
+ set_defaults!(config)
68
+ config
69
+
70
+ rescue Interrupt
71
+ end
72
+
73
+
74
+ def parse_opts
75
+ opts = Trollop::options do
76
+ opt :environment, 'Which environment to use from the config file', :type => :string
77
+ opt :brokers, 'List (space separated) of Kafka brokers', :type => :strings
78
+ opt :topic, 'Kafka topic', :type => :string, :required => true
79
+ opt :partitions, 'List (space separated) of partions to consider', :type => :integers,
80
+ :default => []
81
+ opt :offset, 'Print messages starting from this offset (use a negative offset for the ' +
82
+ 'most recent n messages)', :default => -10
83
+ opt :earliest_offset, 'Print messages starting from the earliest offset'
84
+ opt :latest_offset, 'Print messages starting from most recent offset'
85
+ opt :read_count, 'Max number of messages to read (exits after reading currently ' +
86
+ 'available messages even if this number is not reached)', :type => :integer
87
+ opt :print_offset, 'Include offset number in output'
88
+ opt :dump, 'Exit after printing the available messages'
89
+ opt :count_messages, 'Display the number of messages on a topic'
90
+ opt :min_max_offset, 'Display the minimum and maximum offsets on a topic'
91
+ opt :config_file, 'File containing environment settings', :default => '~/.krump'
92
+ opt :gateway_host, 'Host alias (from SSH config file) for gateway server in front of the ' +
93
+ 'Kafka cluster', :type => :string
94
+ opt :gateway_hostname, 'Hostname for gateway server in front of the Kafka cluster',
95
+ :type => :string
96
+ opt :gateway_user, 'User for gateway server in front of the Kafka cluster',
97
+ :type => :string
98
+ opt :gateway_identityfile, 'Path to key pair for gateway server in front of the Kafka ' +
99
+ 'cluster', :type => :string
100
+ opt :skip_header, "Don't print header showing the partition and offset for a given group " +
101
+ 'of messages'
102
+ conflicts :offset, :earliest_offset, :latest_offset, :count_messages
103
+ conflicts :dump, :read_count, :count_messages
104
+ conflicts :gateway_host, :gateway_hostname
105
+ conflicts :gateway_host, :gateway_user
106
+ conflicts :gateway_host, :gateway_identityfile
107
+ end
108
+
109
+ if opts[:min_max_offset] && !opts[:count_messages]
110
+ Trollop::die :min_max_offset, "cannot be set unless --count-messages is set"
111
+ end
112
+
113
+ opts[:offset] = :earliest_offset if opts[:earliest_offset] || opts[:count_messages]
114
+ opts[:offset] = :latest_offset if opts[:latest_offset]
115
+ opts
116
+ end
117
+
118
+
119
+ # Normally you'd set defaults in the Trollop::options block. However, we want
120
+ # the command line options to supercede those in the config file, but not if
121
+ # the command line option is just the default (not explicitly set).
122
+ def set_defaults!(config)
123
+ config[:brokers] ||= ['localhost:9092']
124
+ config[:partition] ||= 0
125
+ end
126
+
127
+
128
+ def run_task(config)
129
+ consumers = init_consumers(config)
130
+
131
+ if config[:count_messages]
132
+ print_message_count(consumers, config)
133
+ else
134
+ print_messages(consumers, config)
135
+ end
136
+ end
137
+
138
+
139
+ def init_consumers(config)
140
+ if config[:partitions].empty?
141
+ init_consumers_for_all_partitions(config)
142
+
143
+ else
144
+ config[:partitions].map do |partition|
145
+ Krump::KafkaConsumer.new(
146
+ config[:brokers],
147
+ config[:topic],
148
+ partition,
149
+ config[:offset]
150
+ )
151
+ end
152
+ end
153
+ end
154
+
155
+
156
+ # Normally you'd need to know the Zookeeper particulars to get a list of
157
+ # available partitions. To keep the configuration simple, this simply loops
158
+ # until it exceeds the max partition.
159
+ def init_consumers_for_all_partitions(config)
160
+ consumers = []
161
+ partition = 0
162
+
163
+ loop do
164
+ consumers << Krump::KafkaConsumer.new(
165
+ config[:brokers],
166
+ config[:topic],
167
+ partition,
168
+ config[:offset]
169
+ )
170
+ partition += 1
171
+ end
172
+ rescue Poseidon::Errors::UnknownTopicOrPartition
173
+ consumers
174
+ end
175
+
176
+
177
+ def print_message_count(consumers, config)
178
+ config[:offset] = :latest_offset
179
+
180
+ earliest_offset_consumers = consumers
181
+ latest_offset_consumers = init_consumers(config)
182
+
183
+ earliest_offset_consumers.zip(latest_offset_consumers).each do |earliest, latest|
184
+ msg_count = latest.consumer.next_offset - earliest.consumer.next_offset
185
+ output = "#{config[:topic]} | Partition #{latest.partition} | #{msg_count} messages"
186
+ if config[:min_max_offset]
187
+ output += " (#{earliest.consumer.next_offset}, #{latest.consumer.next_offset})"
188
+ end
189
+ puts output
190
+ end
191
+ end
192
+
193
+
194
+ def print_messages(consumers, config)
195
+ loop do
196
+ consumers.each do |consumer|
197
+ messages = consumer.fetch
198
+
199
+ if consumer.last_fetch_size > 0
200
+ print_header(config, consumer) unless config[:skip_header]
201
+
202
+ messages.each do |msg|
203
+ output = format_message(config, msg)
204
+ puts output if config[:read_count].nil? || consumer.messages_read < config[:read_count]
205
+ consumer.messages_read += 1
206
+ end
207
+ end
208
+ end
209
+
210
+ break if finished_consuming?(config, consumers)
211
+ sleep 1 if nothing_fetched?(consumers)
212
+ end
213
+ end
214
+
215
+
216
+ def print_header(config, consumer)
217
+ puts "===== Topic: #{config[:topic]} = Partition: #{consumer.partition} ======="
218
+ end
219
+
220
+
221
+ def format_message(config, msg)
222
+ if config[:print_offset]
223
+ "#{msg.offset} | #{msg.value}"
224
+ else
225
+ msg.value
226
+ end
227
+ end
228
+
229
+
230
+ def finished_consuming?(config, consumers)
231
+ (config[:read_count] && all_consumers_exceeded_read_count?(config[:read_count], consumers)) ||
232
+ (config[:dump] && nothing_fetched?(consumers))
233
+ end
234
+
235
+
236
+ def all_consumers_exceeded_read_count?(read_count, consumers)
237
+ consumers.all? { |consumer| consumer.messages_read >= read_count }
238
+ end
239
+
240
+
241
+ def nothing_fetched?(consumers)
242
+ consumers.all? { |consumer| consumer.last_fetch_size == 0 }
243
+ end
244
+
245
+
246
+ main(init_config)
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'krump/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'krump'
8
+ spec.version = Krump::VERSION
9
+ spec.authors = ['Dean Morin']
10
+ spec.email = ['dean.morin@fundingcircle.com']
11
+
12
+ spec.summary = %q{A Kafka consumer focused on convenience.}
13
+ spec.description = %q{This application was written because of a need for a tail-like way to consume Kafka messages. kafka-console-consumer.sh, which is distributed with Kafka, allows you to either read all messages in a topic, or any new message. However, most often I want to to see the most recent few messages from a topic. Krump provides that as well as a number of other conveniences.
14
+ }
15
+ spec.homepage = 'https://github.com/fundingcircle/krump'
16
+ spec.license = 'BSD 3-Clause'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = 'bin'
20
+ spec.executables = 'krump'
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'net-ssh-gateway', '~> 1.2'
24
+ spec.add_dependency 'poseidon', '0.0.5'
25
+ spec.add_dependency 'trollop', '~> 2.1'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.9'
28
+ spec.add_development_dependency 'pry', '>= 0.10.1'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.3'
31
+
32
+ spec.required_ruby_version = '>= 2.0.0'
33
+ end
@@ -0,0 +1,2 @@
1
+ require 'krump/kafka_consumer'
2
+ require 'krump/ssh_tunnels'
@@ -0,0 +1,104 @@
1
+ require 'krump/local_open_port'
2
+ require 'krump/ssh_tunnel_info'
3
+
4
+
5
+ module Krump
6
+ class ConfigParser
7
+
8
+ def initialize(filename, environment)
9
+ @filename =
10
+ if !filename.nil? && filename.start_with?('~/')
11
+ "#{Dir.home}/#{filename.split('/', 2).last}"
12
+ else
13
+ filename
14
+ end
15
+ @environment = environment
16
+ end
17
+
18
+ def parse
19
+ if @filename && @environment
20
+ File.open(@filename, 'r') { |fh| parse_config(fh.readlines) }
21
+ else
22
+ {}
23
+ end
24
+ rescue Errno::ENOENT => e
25
+ # Ignore file-not-found error if it's the default filename
26
+ raise unless @filename == default_config
27
+ {}
28
+ rescue StandardError => e
29
+ STDERR.puts 'There is an error in your config file'
30
+ raise
31
+ end
32
+
33
+ private
34
+
35
+ def parse_config(lines)
36
+ lines.keep_if { |line| line.start_with?(@environment) }
37
+
38
+ if lines.empty?
39
+ fail "No configuration for environment '#{@environment}'"
40
+ end
41
+
42
+ config = {}
43
+ config[:brokers] = []
44
+ config[:ssh_tunnel_info] = []
45
+
46
+ lines.each do |line|
47
+ key = line.split("#{@environment}.").last.split('=').first
48
+ value = line.split('=').last.chomp
49
+
50
+ case key
51
+ when 'gateway_host' then config[:gateway_host] = value
52
+ when 'gateway_hostname' then config[:gateway_hostname] = value
53
+ when 'gateway_user' then config[:gateway_user] = value
54
+ when 'gateway_identityfile' then config[:gateway_identityfile] = value
55
+ when 'kafka_broker' then add_broker_to_config!(config, value)
56
+ else
57
+ fail "#{key} is not a supported config option"
58
+ end
59
+ end
60
+
61
+ fail_if_invalid_config(config)
62
+ config
63
+ end
64
+
65
+ def add_broker_to_config!(config, value)
66
+ host = value.split(':').first
67
+ port = value.split(':').last
68
+
69
+ if broker_behind_gateway?(config)
70
+ local_port = LocalOpenPort.find
71
+ config[:ssh_tunnel_info] << SshTunnelInfo.new(host, port, local_port)
72
+ config[:brokers] << "localhost:#{local_port}"
73
+ else
74
+ config[:brokers] << "#{host}:#{port}"
75
+ end
76
+ end
77
+
78
+ def broker_behind_gateway?(config)
79
+ config[:gateway_host] || config[:gateway_hostname]
80
+ end
81
+
82
+ def fail_if_invalid_config(config)
83
+ fail_if_incompatible_settings(config[:gateway_host], config[:gateway_hostname])
84
+ fail_if_incompatible_settings(config[:gateway_host], config[:gateway_user])
85
+ fail_if_incompatible_settings(config[:gateway_host], config[:gateway_identityfile])
86
+
87
+ gateway_credential_keys = [:gateway_hostname, :gateway_user, :gateway_identityfile]
88
+
89
+ if gateway_credential_keys.any? { |key| config[key] }
90
+ unless gateway_credential_keys.all? { |key| config[key] }
91
+ fail "If any of (#{gateway_credential_keys.join(',')}) are set then all need to be set"
92
+ end
93
+ end
94
+ end
95
+
96
+ def fail_if_incompatible_settings(setting1, setting2)
97
+ fail "Both #{setting1} and #{setting2} cannot be set" if setting1 && setting2
98
+ end
99
+
100
+ def default_config
101
+ "#{Dir.home}/.krump"
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,75 @@
1
+ require 'poseidon'
2
+
3
+
4
+ module Krump
5
+ class KafkaConsumer
6
+ attr_accessor :messages_read
7
+ attr_reader :broker, :consumer, :last_fetch_size, :offset, :partition, :topic
8
+
9
+ def initialize(brokers, topic, partition, offset)
10
+ @topic = topic
11
+ @partition = partition
12
+ @offset = offset
13
+ @broker = find_broker_for_partition_or_fail(brokers.clone)
14
+ @consumer = init_consumer
15
+ @messages_read = 0
16
+ @last_fetch_size = 0
17
+ end
18
+
19
+ def fetch
20
+ messages = @consumer.fetch
21
+ @last_fetch_size = messages.size
22
+ messages
23
+ end
24
+
25
+ private
26
+
27
+ # Poseiden expects that you know which broker to find a particular
28
+ # partition on. This method simply tries them all. Errors are silently ignored,
29
+ # unless all brokers are checked and the requested topic/partition is still
30
+ # not found.
31
+ #
32
+ # It uses a negative offset because a positive one won't fail on consumer.next_offset.
33
+ #
34
+ def find_broker_for_partition_or_fail(brokers)
35
+ broker = brokers.shift
36
+ host = broker.split(':').first
37
+ port = broker.split(':').last
38
+
39
+ consumer = Poseidon::PartitionConsumer.new(
40
+ "krump-kafka-test-consumer_#{@topic}_#{@partition}_#{DateTime.now}",
41
+ host,
42
+ port,
43
+ @topic,
44
+ @partition,
45
+ -1
46
+ )
47
+ consumer.next_offset
48
+ consumer.close
49
+
50
+ broker
51
+
52
+ rescue Poseidon::Errors::NotLeaderForPartition => e
53
+ retry if brokers.size > 0
54
+ raise e
55
+ rescue Poseidon::Errors::UnknownTopicOrPartition => e
56
+ retry if brokers.size > 0
57
+ raise e
58
+ end
59
+
60
+ def init_consumer
61
+ host = @broker.split(':').first
62
+ port = @broker.split(':').last
63
+
64
+ consumer = Poseidon::PartitionConsumer.new(
65
+ "krump-kafka-consumer_#{@topic}_#{@partition}_#{DateTime.now}",
66
+ host,
67
+ port,
68
+ @topic,
69
+ @partition,
70
+ @offset
71
+ )
72
+ consumer
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,13 @@
1
+ require 'socket'
2
+
3
+ module Krump
4
+ class LocalOpenPort
5
+
6
+ # Return an open port from the ephemeral port range
7
+ def self.find
8
+ socket = Socket.new(:INET, :STREAM, 0)
9
+ socket.bind(Addrinfo.tcp("127.0.0.1", 0))
10
+ socket.local_address.ip_port
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Krump
2
+ class SshTunnelInfo
3
+ attr_accessor :host, :port, :local_port
4
+
5
+ def initialize(host, port, local_port)
6
+ @host = host
7
+ @port = port.to_i
8
+ @local_port = local_port.to_i
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ require 'net/ssh/gateway'
2
+
3
+
4
+ module Krump
5
+ class SshTunnels
6
+
7
+ def initialize(gateway_hostname, gateway_user, gateway_identityfile, ssh_tunnel_info)
8
+ @gateway_hostname = gateway_hostname
9
+ @gateway_user = gateway_user
10
+ @gateway_identityfile = gateway_identityfile
11
+ @ssh_tunnel_info = Array(ssh_tunnel_info)
12
+ end
13
+
14
+ def open(&block)
15
+ block_given? ? open_with_block(&block) : open_without_block
16
+ end
17
+
18
+ private
19
+
20
+ def open_with_block(&block)
21
+ gateway = open_without_block
22
+ yield gateway
23
+ ensure
24
+ gateway.shutdown! unless gateway.nil?
25
+ end
26
+
27
+ def open_without_block
28
+ gateway = Net::SSH::Gateway.new(
29
+ @gateway_hostname,
30
+ @gateway_user,
31
+ :keys => [@gateway_identityfile]
32
+ )
33
+
34
+ @ssh_tunnel_info.each do |info|
35
+ gateway.open(info.host, info.port, info.local_port)
36
+ end
37
+
38
+ gateway
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Krump
2
+ VERSION = '0.3.1'
3
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: krump
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Dean Morin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh-gateway
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: poseidon
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: trollop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.10.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.10.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.3'
111
+ description: |
112
+ This application was written because of a need for a tail-like way to consume Kafka messages. kafka-console-consumer.sh, which is distributed with Kafka, allows you to either read all messages in a topic, or any new message. However, most often I want to to see the most recent few messages from a topic. Krump provides that as well as a number of other conveniences.
113
+ email:
114
+ - dean.morin@fundingcircle.com
115
+ executables:
116
+ - krump
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".gitignore"
121
+ - ".rspec"
122
+ - ".ruby-version"
123
+ - ".travis.yml"
124
+ - Gemfile
125
+ - LICENSE.txt
126
+ - README.md
127
+ - Rakefile
128
+ - bin/console
129
+ - bin/krump
130
+ - bin/setup
131
+ - krump.gemspec
132
+ - lib/krump.rb
133
+ - lib/krump/config_parser.rb
134
+ - lib/krump/kafka_consumer.rb
135
+ - lib/krump/local_open_port.rb
136
+ - lib/krump/ssh_tunnel_info.rb
137
+ - lib/krump/ssh_tunnels.rb
138
+ - lib/krump/version.rb
139
+ homepage: https://github.com/fundingcircle/krump
140
+ licenses:
141
+ - BSD 3-Clause
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 2.0.0
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.4.5
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: A Kafka consumer focused on convenience.
163
+ test_files: []