sbmt-kafka_consumer 2.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/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/Appraisals +23 -0
- data/CHANGELOG.md +292 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +296 -0
- data/Rakefile +12 -0
- data/config.ru +9 -0
- data/dip.yml +84 -0
- data/docker-compose.yml +68 -0
- data/exe/kafka_consumer +16 -0
- data/lefthook-local.dip_example.yml +4 -0
- data/lefthook.yml +6 -0
- data/lib/generators/kafka_consumer/concerns/configuration.rb +30 -0
- data/lib/generators/kafka_consumer/consumer/USAGE +24 -0
- data/lib/generators/kafka_consumer/consumer/consumer_generator.rb +41 -0
- data/lib/generators/kafka_consumer/consumer/templates/consumer.rb.erb +9 -0
- data/lib/generators/kafka_consumer/consumer/templates/consumer_group.yml.erb +13 -0
- data/lib/generators/kafka_consumer/inbox_consumer/USAGE +22 -0
- data/lib/generators/kafka_consumer/inbox_consumer/inbox_consumer_generator.rb +48 -0
- data/lib/generators/kafka_consumer/inbox_consumer/templates/consumer_group.yml.erb +22 -0
- data/lib/generators/kafka_consumer/install/USAGE +9 -0
- data/lib/generators/kafka_consumer/install/install_generator.rb +22 -0
- data/lib/generators/kafka_consumer/install/templates/Kafkafile +3 -0
- data/lib/generators/kafka_consumer/install/templates/kafka_consumer.yml +59 -0
- data/lib/sbmt/kafka_consumer/app_initializer.rb +13 -0
- data/lib/sbmt/kafka_consumer/base_consumer.rb +104 -0
- data/lib/sbmt/kafka_consumer/cli.rb +55 -0
- data/lib/sbmt/kafka_consumer/client_configurer.rb +73 -0
- data/lib/sbmt/kafka_consumer/config/auth.rb +56 -0
- data/lib/sbmt/kafka_consumer/config/consumer.rb +16 -0
- data/lib/sbmt/kafka_consumer/config/consumer_group.rb +9 -0
- data/lib/sbmt/kafka_consumer/config/deserializer.rb +15 -0
- data/lib/sbmt/kafka_consumer/config/kafka.rb +32 -0
- data/lib/sbmt/kafka_consumer/config/metrics.rb +10 -0
- data/lib/sbmt/kafka_consumer/config/probes/endpoints.rb +13 -0
- data/lib/sbmt/kafka_consumer/config/probes/liveness_probe.rb +11 -0
- data/lib/sbmt/kafka_consumer/config/probes/readiness_probe.rb +10 -0
- data/lib/sbmt/kafka_consumer/config/probes.rb +8 -0
- data/lib/sbmt/kafka_consumer/config/topic.rb +14 -0
- data/lib/sbmt/kafka_consumer/config.rb +76 -0
- data/lib/sbmt/kafka_consumer/inbox_consumer.rb +129 -0
- data/lib/sbmt/kafka_consumer/instrumentation/base_monitor.rb +25 -0
- data/lib/sbmt/kafka_consumer/instrumentation/chainable_monitor.rb +31 -0
- data/lib/sbmt/kafka_consumer/instrumentation/listener_helper.rb +47 -0
- data/lib/sbmt/kafka_consumer/instrumentation/liveness_listener.rb +71 -0
- data/lib/sbmt/kafka_consumer/instrumentation/logger_listener.rb +44 -0
- data/lib/sbmt/kafka_consumer/instrumentation/open_telemetry_loader.rb +23 -0
- data/lib/sbmt/kafka_consumer/instrumentation/open_telemetry_tracer.rb +106 -0
- data/lib/sbmt/kafka_consumer/instrumentation/readiness_listener.rb +38 -0
- data/lib/sbmt/kafka_consumer/instrumentation/sentry_tracer.rb +103 -0
- data/lib/sbmt/kafka_consumer/instrumentation/tracer.rb +18 -0
- data/lib/sbmt/kafka_consumer/instrumentation/tracing_monitor.rb +17 -0
- data/lib/sbmt/kafka_consumer/instrumentation/yabeda_metrics_listener.rb +186 -0
- data/lib/sbmt/kafka_consumer/probes/host.rb +75 -0
- data/lib/sbmt/kafka_consumer/probes/probe.rb +33 -0
- data/lib/sbmt/kafka_consumer/railtie.rb +31 -0
- data/lib/sbmt/kafka_consumer/routing/karafka_v1_consumer_mapper.rb +12 -0
- data/lib/sbmt/kafka_consumer/routing/karafka_v2_consumer_mapper.rb +9 -0
- data/lib/sbmt/kafka_consumer/serialization/base_deserializer.rb +19 -0
- data/lib/sbmt/kafka_consumer/serialization/json_deserializer.rb +18 -0
- data/lib/sbmt/kafka_consumer/serialization/null_deserializer.rb +13 -0
- data/lib/sbmt/kafka_consumer/serialization/protobuf_deserializer.rb +27 -0
- data/lib/sbmt/kafka_consumer/server.rb +35 -0
- data/lib/sbmt/kafka_consumer/simple_logging_consumer.rb +11 -0
- data/lib/sbmt/kafka_consumer/testing/shared_contexts/with_sbmt_karafka_consumer.rb +61 -0
- data/lib/sbmt/kafka_consumer/testing.rb +5 -0
- data/lib/sbmt/kafka_consumer/types.rb +15 -0
- data/lib/sbmt/kafka_consumer/version.rb +7 -0
- data/lib/sbmt/kafka_consumer/yabeda_configurer.rb +91 -0
- data/lib/sbmt/kafka_consumer.rb +59 -0
- data/rubocop/rspec.yml +29 -0
- data/sbmt-kafka_consumer.gemspec +70 -0
- metadata +571 -0
data/dip.yml
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
version: '7'
|
2
|
+
|
3
|
+
environment:
|
4
|
+
RUBY_VERSION: '3.2'
|
5
|
+
|
6
|
+
compose:
|
7
|
+
files:
|
8
|
+
- docker-compose.yml
|
9
|
+
|
10
|
+
interaction:
|
11
|
+
bash:
|
12
|
+
description: Open the Bash shell in app's container
|
13
|
+
service: ruby
|
14
|
+
command: /bin/bash
|
15
|
+
|
16
|
+
bundle:
|
17
|
+
description: Run Bundler commands
|
18
|
+
service: ruby
|
19
|
+
command: bundle
|
20
|
+
|
21
|
+
rails:
|
22
|
+
description: Run RoR commands
|
23
|
+
service: ruby
|
24
|
+
command: bundle exec rails
|
25
|
+
|
26
|
+
appraisal:
|
27
|
+
description: Run Appraisal commands
|
28
|
+
service: ruby
|
29
|
+
command: bundle exec appraisal
|
30
|
+
|
31
|
+
rspec:
|
32
|
+
description: Run Rspec commands
|
33
|
+
service: ruby
|
34
|
+
command: bundle exec rspec
|
35
|
+
subcommands:
|
36
|
+
all:
|
37
|
+
command: bundle exec appraisal rspec
|
38
|
+
rails-6.0:
|
39
|
+
command: bundle exec appraisal rails-6.0 rspec
|
40
|
+
rails-6.1:
|
41
|
+
command: bundle exec appraisal rails-6.1 rspec
|
42
|
+
rails-7.0:
|
43
|
+
command: bundle exec appraisal rails-7.0 rspec
|
44
|
+
rails-7.1:
|
45
|
+
command: bundle exec appraisal rails-7.1 rspec
|
46
|
+
|
47
|
+
rubocop:
|
48
|
+
description: Run Ruby linter
|
49
|
+
service: ruby
|
50
|
+
command: bundle exec rubocop
|
51
|
+
|
52
|
+
setup:
|
53
|
+
description: Install deps
|
54
|
+
service: ruby
|
55
|
+
command: bin/setup
|
56
|
+
|
57
|
+
test:
|
58
|
+
description: Run linters, run all tests
|
59
|
+
service: ruby
|
60
|
+
command: bin/test
|
61
|
+
|
62
|
+
kafka-consumer:
|
63
|
+
description: Run kafka consumer
|
64
|
+
service: ruby
|
65
|
+
command: bundle exec kafka_consumer
|
66
|
+
|
67
|
+
kafka-producer:
|
68
|
+
description: Run kafka producer commands
|
69
|
+
service: kafka
|
70
|
+
command: kafka-console-producer.sh --bootstrap-server kafka:9092
|
71
|
+
subcommands:
|
72
|
+
inbox:
|
73
|
+
command: kafka-console-producer.sh --bootstrap-server kafka:9092 --topic topic_with_inbox_items
|
74
|
+
json:
|
75
|
+
command: kafka-console-producer.sh --bootstrap-server kafka:9092 --topic topic_with_json_data
|
76
|
+
protobuf:
|
77
|
+
command: kafka-console-producer.sh --bootstrap-server kafka:9092 --topic topic_with_protobuf_data
|
78
|
+
|
79
|
+
provision:
|
80
|
+
- dip compose down --volumes
|
81
|
+
- cp -f lefthook-local.dip_example.yml lefthook-local.yml
|
82
|
+
- rm -f Gemfile.lock
|
83
|
+
- rm -f gemfiles/*gemfile*
|
84
|
+
- dip setup
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
services:
|
2
|
+
ruby:
|
3
|
+
image: ruby:${RUBY_VERSION:-3.2}
|
4
|
+
environment:
|
5
|
+
HISTFILE: /app/tmp/.bash_history
|
6
|
+
BUNDLE_PATH: /usr/local/bundle
|
7
|
+
BUNDLE_CONFIG: /app/.bundle/config
|
8
|
+
DATABASE_URL: postgres://postgres:@postgres:5432
|
9
|
+
KAFKAFILE: spec/internal/Kafkafile
|
10
|
+
depends_on:
|
11
|
+
kafka:
|
12
|
+
condition: service_started
|
13
|
+
postgres:
|
14
|
+
condition: service_started
|
15
|
+
command: bash
|
16
|
+
working_dir: /app
|
17
|
+
volumes:
|
18
|
+
- .:/app:cached
|
19
|
+
- bundler_data:/usr/local/bundle
|
20
|
+
|
21
|
+
postgres:
|
22
|
+
image: postgres:13
|
23
|
+
environment:
|
24
|
+
POSTGRES_HOST_AUTH_METHOD: trust
|
25
|
+
ports:
|
26
|
+
- 5432
|
27
|
+
healthcheck:
|
28
|
+
test: pg_isready -U postgres -h 127.0.0.1
|
29
|
+
interval: 10s
|
30
|
+
|
31
|
+
kafka:
|
32
|
+
image: bitnami/kafka:2.7.0
|
33
|
+
ports:
|
34
|
+
- '9092:9092'
|
35
|
+
environment:
|
36
|
+
- KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
|
37
|
+
- ALLOW_PLAINTEXT_LISTENER=yes
|
38
|
+
- KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
|
39
|
+
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,INTERNAL:PLAINTEXT
|
40
|
+
- KAFKA_CFG_LISTENERS=CLIENT://:9092,INTERNAL://:9091
|
41
|
+
- KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka:9092,INTERNAL://kafka:9091
|
42
|
+
- KAFKA_INTER_BROKER_LISTENER_NAME=INTERNAL
|
43
|
+
depends_on:
|
44
|
+
- zookeeper
|
45
|
+
healthcheck:
|
46
|
+
# we don't have `nc` installed in kafka image :(
|
47
|
+
test:
|
48
|
+
- CMD-SHELL
|
49
|
+
- echo 'exit' | curl --silent -f telnet://0.0.0.0:9092
|
50
|
+
interval: 15s
|
51
|
+
timeout: 5s
|
52
|
+
retries: 15
|
53
|
+
|
54
|
+
zookeeper:
|
55
|
+
image: bitnami/zookeeper:3.5
|
56
|
+
ports:
|
57
|
+
- '2181:2181'
|
58
|
+
environment:
|
59
|
+
- ALLOW_ANONYMOUS_LOGIN=yes
|
60
|
+
healthcheck:
|
61
|
+
test: ["CMD-SHELL", "echo ruok | nc localhost 2181"]
|
62
|
+
interval: 2s
|
63
|
+
timeout: 2s
|
64
|
+
retries: 15
|
65
|
+
|
66
|
+
volumes:
|
67
|
+
bundler_data:
|
68
|
+
kafka:
|
data/exe/kafka_consumer
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "sbmt/kafka_consumer"
|
6
|
+
|
7
|
+
# rubocop:disable Lint/RescueException
|
8
|
+
begin
|
9
|
+
Sbmt::KafkaConsumer::CLI.start(ARGV)
|
10
|
+
rescue Exception => e
|
11
|
+
warn "KafkaConsumer exited with error"
|
12
|
+
warn(e.message) if e.respond_to?(:message)
|
13
|
+
warn(e.backtrace.join("\n")) if e.respond_to?(:backtrace) && e.backtrace.respond_to?(:join)
|
14
|
+
exit 1
|
15
|
+
end
|
16
|
+
# rubocop:enable Lint/RescueException
|
data/lefthook.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KafkaConsumer
|
4
|
+
module Generators
|
5
|
+
module Concerns
|
6
|
+
module Configuration
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
CONFIG_PATH = "config/kafka_consumer.yml"
|
10
|
+
|
11
|
+
def check_config_file!
|
12
|
+
config_path = File.expand_path(CONFIG_PATH)
|
13
|
+
return if File.exist?(config_path)
|
14
|
+
|
15
|
+
generate = ask "The file #{config_path} does not appear to exist. " \
|
16
|
+
"Would you like to generate it? [Yn]"
|
17
|
+
|
18
|
+
generator_name = "kafka_consumer:install"
|
19
|
+
if (generate.presence || "y").casecmp("y").zero?
|
20
|
+
generate generator_name
|
21
|
+
else
|
22
|
+
raise Rails::Generators::Error, "Please generate #{config_path} " \
|
23
|
+
"by running `bin/rails g #{generator_name}` " \
|
24
|
+
"or add this file manually."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Description:
|
2
|
+
Stubs out a new non-inbox consumer. Pass the consumer name, either
|
3
|
+
CamelCased or under_scored.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
bin/rails generate kafka_consumer:consumer Test
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
app/consumers/test_consumer.rb
|
10
|
+
|
11
|
+
This will optionally insert:
|
12
|
+
'group_key':
|
13
|
+
name: <%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_NAME'){ 'group.name' } %><%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_SUFFIX'){ '' } %>
|
14
|
+
topics:
|
15
|
+
- name: 'topic.name'
|
16
|
+
consumer:
|
17
|
+
klass: "TestConsumer"
|
18
|
+
# init_attrs:
|
19
|
+
# skip_on_error: false # This is the default value
|
20
|
+
deserializer:
|
21
|
+
klass: "Sbmt::KafkaConsumer::Serialization::ProtobufDeserializer"
|
22
|
+
init_attrs:
|
23
|
+
message_decoder_klass: "YourMessageDecoderClassName"
|
24
|
+
# skip_decoding_error: false # This is the default value
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/named_base"
|
4
|
+
require "generators/kafka_consumer/concerns/configuration"
|
5
|
+
|
6
|
+
module KafkaConsumer
|
7
|
+
module Generators
|
8
|
+
class ConsumerGenerator < Rails::Generators::NamedBase
|
9
|
+
include Concerns::Configuration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def insert_consumer_class
|
14
|
+
@consumer_name = "#{name.classify}Consumer"
|
15
|
+
template "consumer.rb.erb", "app/consumers/#{file_path}_consumer.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure_consumer_group
|
19
|
+
@group_key = ask "Would you also configure a consumer group?" \
|
20
|
+
" Type the group's key (e.g. my_consumer_group) or press Enter to skip this action"
|
21
|
+
return if @group_key.blank?
|
22
|
+
|
23
|
+
check_config_file!
|
24
|
+
|
25
|
+
@group_name = ask "Type the group's name (e.g. my.consumer.group)"
|
26
|
+
@topic = ask "Type the group topic's name"
|
27
|
+
insert_into_file CONFIG_PATH, group_template.result(binding), after: "consumer_groups:\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def group_template_path
|
33
|
+
File.join(ConsumerGenerator.source_root, "consumer_group.yml.erb")
|
34
|
+
end
|
35
|
+
|
36
|
+
def group_template
|
37
|
+
ERB.new(File.read(group_template_path), trim_mode: "%-")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
'<%= @group_key %>':
|
2
|
+
name: <%%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_NAME'){ "<%= @group_name %>" } %><%%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_SUFFIX'){ "" } %>
|
3
|
+
topics:
|
4
|
+
- name: "<%= @topic.presence || "insert-your-topic-name-here" %>"
|
5
|
+
consumer:
|
6
|
+
klass: "<%= @consumer_name %>"
|
7
|
+
# init_attrs:
|
8
|
+
# skip_on_error: false # This is the default value
|
9
|
+
deserializer:
|
10
|
+
klass: "Sbmt::KafkaConsumer::Serialization::ProtobufDeserializer"
|
11
|
+
init_attrs:
|
12
|
+
message_decoder_klass: "YourMessageDecoderClassName"
|
13
|
+
# skip_decoding_error: false # This is the default value
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Description:
|
2
|
+
Inserts a consumer group's default configuration.
|
3
|
+
It accepts a group key, a group name and an optional array of topics as arguments.
|
4
|
+
|
5
|
+
|
6
|
+
Example:
|
7
|
+
bin/rails generate kafka_consumer:inbox_consumer group_key group.name topic.name
|
8
|
+
|
9
|
+
This will insert:
|
10
|
+
'group_key':
|
11
|
+
name: <%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_NAME'){ 'group.name' } %><%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_SUFFIX'){ '' } %>
|
12
|
+
topics:
|
13
|
+
- name: 'topic.name'
|
14
|
+
consumer:
|
15
|
+
# Change the line below to the desired consumer
|
16
|
+
# if InboxConsumer doesn't suit your needs
|
17
|
+
klass: "Sbmt::KafkaConsumer::InboxConsumer"
|
18
|
+
init_attrs:
|
19
|
+
name: "test_items"
|
20
|
+
inbox_item: "SomeModelInboxItem" # Change this to your item class name
|
21
|
+
# deserializer: # This deserializer is used by default
|
22
|
+
# klass: "Sbmt::KafkaConsumer::Serialization::NullDeserializer"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/named_base"
|
4
|
+
require "generators/kafka_consumer/concerns/configuration"
|
5
|
+
|
6
|
+
module KafkaConsumer
|
7
|
+
module Generators
|
8
|
+
class InboxConsumerGenerator < Rails::Generators::NamedBase
|
9
|
+
include Concerns::Configuration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
argument :group_name, type: :string, banner: "group.name"
|
14
|
+
argument :topics, type: :array, default: [], banner: "topic topic"
|
15
|
+
|
16
|
+
def process_topics
|
17
|
+
check_config_file!
|
18
|
+
|
19
|
+
@items = {}
|
20
|
+
topics.each do |topic|
|
21
|
+
inbox_item = ask "Would you also add an InboxItem class for topic '#{topic}'?" \
|
22
|
+
" Type item's name in the form of SomeModel::InboxItem or press Enter" \
|
23
|
+
" to skip creating item's class"
|
24
|
+
@items[topic] = if inbox_item.blank?
|
25
|
+
nil
|
26
|
+
else
|
27
|
+
generate "outbox:item", inbox_item, "--kind inbox"
|
28
|
+
inbox_item.classify
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def insert_consumer_group
|
34
|
+
insert_into_file CONFIG_PATH, group_template.result(binding), after: "consumer_groups:\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def group_template_path
|
40
|
+
File.join(InboxConsumerGenerator.source_root, "consumer_group.yml.erb")
|
41
|
+
end
|
42
|
+
|
43
|
+
def group_template
|
44
|
+
ERB.new(File.read(group_template_path), trim_mode: "%-")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
'<%= file_name %>':
|
2
|
+
name: <%%= ENV.fetch('ENV_VARIABLE_WITH_GROUP_NAME'){ '<%= group_name %>' } %><%%= ENV.fetch('CONSUMER_GROUP_SUFFIX'){ '' } %>
|
3
|
+
<%- if @items.empty? -%>
|
4
|
+
topics: []
|
5
|
+
<%- else -%>
|
6
|
+
topics:
|
7
|
+
<%- @items.each do |topic, item_name| -%>
|
8
|
+
<%- next if topic.blank? -%>
|
9
|
+
<%- inbox_item = item_name.presence || "YourModelName::InboxItem" -%>
|
10
|
+
<%- consumer_name = inbox_item.split('::').first.presence || "#{topic}_item" -%>
|
11
|
+
- name: "<%= topic %>"
|
12
|
+
consumer:
|
13
|
+
# Change the line below to the desired consumer
|
14
|
+
# if InboxConsumer doesn't suit your needs
|
15
|
+
klass: "Sbmt::KafkaConsumer::InboxConsumer"
|
16
|
+
init_attrs:
|
17
|
+
name: "<%= consumer_name.underscore.pluralize %>"
|
18
|
+
inbox_item: "<%= inbox_item %>"
|
19
|
+
# deserializer: # This deserializer is used by default
|
20
|
+
# klass: "Sbmt::KafkaConsumer::Serialization::NullDeserializer"
|
21
|
+
<%- end -%>
|
22
|
+
<%- end -%>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "generators/kafka_consumer/concerns/configuration"
|
5
|
+
|
6
|
+
module KafkaConsumer
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
include Concerns::Configuration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def create_kafkafile
|
14
|
+
copy_file "Kafkafile", "./Kafkafile"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_kafka_consumer_yml
|
18
|
+
copy_file "kafka_consumer.yml", CONFIG_PATH
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
default: &default
|
2
|
+
client_id: 'some-name'
|
3
|
+
max_wait_time: 1
|
4
|
+
shutdown_timeout: 60
|
5
|
+
concurrency: 4
|
6
|
+
pause_timeout: 1
|
7
|
+
pause_max_timeout: 30
|
8
|
+
pause_with_exponential_backoff: true
|
9
|
+
auth:
|
10
|
+
kind: plaintext
|
11
|
+
kafka:
|
12
|
+
servers: "kafka:9092"
|
13
|
+
heartbeat_timeout: 5
|
14
|
+
session_timeout: 30
|
15
|
+
reconnect_timeout: 3
|
16
|
+
connect_timeout: 5
|
17
|
+
socket_timeout: 30
|
18
|
+
kafka_options:
|
19
|
+
allow.auto.create.topics: true
|
20
|
+
consumer_groups:
|
21
|
+
# group_ref_id_1:
|
22
|
+
# name: cg_with_single_topic
|
23
|
+
# topics:
|
24
|
+
# - name: topic_with_inbox_items
|
25
|
+
# consumer:
|
26
|
+
# klass: "Sbmt::KafkaConsumer::InboxConsumer"
|
27
|
+
# init_attrs:
|
28
|
+
# name: "test_items"
|
29
|
+
# inbox_item: "TestInboxItem"
|
30
|
+
# deserializer:
|
31
|
+
# klass: "Sbmt::KafkaConsumer::Serialization::NullDeserializer"
|
32
|
+
# group_ref_id_2:
|
33
|
+
# name: cg_with_multiple_topics
|
34
|
+
# topics:
|
35
|
+
# - name: topic_with_json_data
|
36
|
+
# consumer:
|
37
|
+
# klass: "Sbmt::KafkaConsumer::SimpleLoggingConsumer"
|
38
|
+
# deserializer:
|
39
|
+
# klass: "Sbmt::KafkaConsumer::Serialization::JsonDeserializer"
|
40
|
+
# - name: topic_with_protobuf_data
|
41
|
+
# consumer:
|
42
|
+
# klass: "Sbmt::KafkaConsumer::SimpleLoggingConsumer"
|
43
|
+
# deserializer:
|
44
|
+
# klass: "Sbmt::KafkaConsumer::Serialization::ProtobufDeserializer"
|
45
|
+
# init_attrs:
|
46
|
+
# message_decoder_klass: "Sso::UserRegistration"
|
47
|
+
# skip_decoding_error: true
|
48
|
+
probes:
|
49
|
+
port: 9394
|
50
|
+
|
51
|
+
development:
|
52
|
+
<<: *default
|
53
|
+
test:
|
54
|
+
<<: *default
|
55
|
+
deliver: false
|
56
|
+
staging: &staging
|
57
|
+
<<: *default
|
58
|
+
production:
|
59
|
+
<<: *staging
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
class BaseConsumer < Karafka::BaseConsumer
|
6
|
+
attr_reader :trace_id
|
7
|
+
|
8
|
+
def self.consumer_klass(skip_on_error: false)
|
9
|
+
Class.new(self) do
|
10
|
+
const_set(:SKIP_ON_ERROR, skip_on_error)
|
11
|
+
|
12
|
+
def self.name
|
13
|
+
superclass.name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def consume
|
19
|
+
::Rails.application.executor.wrap do
|
20
|
+
messages.each do |message|
|
21
|
+
with_instrumentation(message) { do_consume(message) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def with_instrumentation(message)
|
29
|
+
@trace_id = SecureRandom.base58
|
30
|
+
|
31
|
+
logger.tagged(
|
32
|
+
trace_id: trace_id,
|
33
|
+
topic: message.metadata.topic, partition: message.metadata.partition,
|
34
|
+
key: message.metadata.key, offset: message.metadata.offset
|
35
|
+
) do
|
36
|
+
::Sbmt::KafkaConsumer.monitor.instrument(
|
37
|
+
"consumer.consumed_one",
|
38
|
+
caller: self, message: message, trace_id: trace_id
|
39
|
+
) do
|
40
|
+
do_consume(message)
|
41
|
+
rescue SkipUndeserializableMessage => ex
|
42
|
+
instrument_error(ex, message)
|
43
|
+
logger.warn("skipping undeserializable message: #{ex.message}")
|
44
|
+
rescue => ex
|
45
|
+
instrument_error(ex, message)
|
46
|
+
|
47
|
+
if skip_on_error
|
48
|
+
logger.warn("skipping unprocessable message: #{ex.message}, message: #{message_payload(message).inspect}")
|
49
|
+
else
|
50
|
+
raise ex
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def do_consume(message)
|
57
|
+
log_message(message) if log_payload?
|
58
|
+
|
59
|
+
# deserialization process is lazy (and cached)
|
60
|
+
# so we trigger it explicitly to catch undeserializable message early
|
61
|
+
message.payload
|
62
|
+
|
63
|
+
process_message(message)
|
64
|
+
|
65
|
+
mark_as_consumed!(message)
|
66
|
+
end
|
67
|
+
|
68
|
+
def skip_on_error
|
69
|
+
self.class::SKIP_ON_ERROR
|
70
|
+
end
|
71
|
+
|
72
|
+
# can be overridden in consumer to enable message logging
|
73
|
+
def log_payload?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
def logger
|
78
|
+
::Sbmt::KafkaConsumer.logger
|
79
|
+
end
|
80
|
+
|
81
|
+
def process_message(_message)
|
82
|
+
raise NotImplementedError, "Implement this in a subclass"
|
83
|
+
end
|
84
|
+
|
85
|
+
def log_message(message)
|
86
|
+
logger.info("#{message_payload(message).inspect}, message_key: #{message.metadata.key}, message_headers: #{message.metadata.headers}")
|
87
|
+
end
|
88
|
+
|
89
|
+
def instrument_error(error, message)
|
90
|
+
::Sbmt::KafkaConsumer.monitor.instrument(
|
91
|
+
"error.occurred",
|
92
|
+
error: error,
|
93
|
+
caller: self,
|
94
|
+
message: message,
|
95
|
+
type: "consumer.base.consume_one"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def message_payload(message)
|
100
|
+
message.payload || message.raw_payload
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
class CLI < Thor
|
6
|
+
def self.exit_on_failure?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
default_command :start
|
11
|
+
|
12
|
+
desc "start", "Start kafka_consumer worker"
|
13
|
+
option :consumer_group_id,
|
14
|
+
aliases: "-g",
|
15
|
+
desc: "Consumer group id to start",
|
16
|
+
repeatable: true
|
17
|
+
option :concurrency,
|
18
|
+
aliases: "-c",
|
19
|
+
type: :numeric,
|
20
|
+
default: 5,
|
21
|
+
desc: "Number of threads, overrides global kafka.concurrency config"
|
22
|
+
def start
|
23
|
+
$stdout.puts "Initializing KafkaConsumer"
|
24
|
+
$stdout.puts "Version: #{VERSION}"
|
25
|
+
|
26
|
+
load_environment
|
27
|
+
|
28
|
+
$stdout.sync = true
|
29
|
+
|
30
|
+
$stdout.puts "Configuring client"
|
31
|
+
ClientConfigurer.configure!(
|
32
|
+
consumer_groups: options[:consumer_group_id],
|
33
|
+
concurrency: options[:concurrency]
|
34
|
+
)
|
35
|
+
$stdout.puts "Client configured routes: #{ClientConfigurer.routes.inspect}"
|
36
|
+
|
37
|
+
$stdout.puts "Starting probes/metrics http-server"
|
38
|
+
Sbmt::KafkaConsumer::Probes::Host.run_async
|
39
|
+
|
40
|
+
Sbmt::KafkaConsumer::Server.run
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def load_environment
|
46
|
+
env_file_path = ENV["KAFKAFILE"] || "#{Dir.pwd}/Kafkafile"
|
47
|
+
|
48
|
+
if File.exist?(env_file_path)
|
49
|
+
$stdout.puts "Loading env from Kafkafile: #{env_file_path}"
|
50
|
+
load(env_file_path)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|