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
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Instrumentation
|
6
|
+
class LoggerListener < Karafka::Instrumentation::LoggerListener
|
7
|
+
include ListenerHelper
|
8
|
+
CUSTOM_ERROR_TYPES = %w[consumer.base.consume_one consumer.inbox.consume_one].freeze
|
9
|
+
|
10
|
+
def on_error_occurred(event)
|
11
|
+
type = event[:type]
|
12
|
+
error = event[:error]
|
13
|
+
|
14
|
+
# catch here only consumer-specific errors
|
15
|
+
# and let default handler to process other
|
16
|
+
return super unless CUSTOM_ERROR_TYPES.include?(type)
|
17
|
+
|
18
|
+
tags = {}
|
19
|
+
tags[:status] = event[:status] if type == "consumer.inbox.consume_one"
|
20
|
+
|
21
|
+
logger.tagged(
|
22
|
+
type: type,
|
23
|
+
**tags
|
24
|
+
) do
|
25
|
+
logger.error(error_message(error))
|
26
|
+
log_backtrace(error)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# BaseConsumer events
|
31
|
+
def on_consumer_consumed_one(event)
|
32
|
+
logger.info("Successfully consumed message in #{event.payload[:time]} ms")
|
33
|
+
end
|
34
|
+
|
35
|
+
# InboxConsumer events
|
36
|
+
def on_consumer_inbox_consumed_one(event)
|
37
|
+
logger.tagged(status: event[:status]) do
|
38
|
+
logger.info("Successfully consumed message with uuid: #{event[:message_uuid]} in #{event.payload[:time]} ms")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry"
|
4
|
+
require "opentelemetry-common"
|
5
|
+
require "opentelemetry-instrumentation-base"
|
6
|
+
|
7
|
+
require_relative "open_telemetry_tracer"
|
8
|
+
|
9
|
+
module Sbmt
|
10
|
+
module KafkaConsumer
|
11
|
+
module Instrumentation
|
12
|
+
class OpenTelemetryLoader < OpenTelemetry::Instrumentation::Base
|
13
|
+
install do |_config|
|
14
|
+
OpenTelemetryTracer.enabled = true
|
15
|
+
end
|
16
|
+
|
17
|
+
present do
|
18
|
+
defined?(OpenTelemetryTracer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tracer"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module KafkaConsumer
|
7
|
+
module Instrumentation
|
8
|
+
class OpenTelemetryTracer < ::Sbmt::KafkaConsumer::Instrumentation::Tracer
|
9
|
+
class << self
|
10
|
+
def enabled?
|
11
|
+
!!@enabled
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_writer :enabled
|
15
|
+
end
|
16
|
+
|
17
|
+
def enabled?
|
18
|
+
self.class.enabled?
|
19
|
+
end
|
20
|
+
|
21
|
+
def trace(&block)
|
22
|
+
return handle_consumed_one(&block) if @event_id == "consumer.consumed_one"
|
23
|
+
return handle_inbox_consumed_one(&block) if @event_id == "consumer.inbox.consumed_one"
|
24
|
+
return handle_error(&block) if @event_id == "error.occurred"
|
25
|
+
|
26
|
+
yield
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_consumed_one
|
30
|
+
return yield unless enabled?
|
31
|
+
|
32
|
+
consumer = @payload[:caller]
|
33
|
+
message = @payload[:message]
|
34
|
+
|
35
|
+
parent_context = ::OpenTelemetry.propagation.extract(message.headers, getter: ::OpenTelemetry::Context::Propagation.text_map_getter)
|
36
|
+
span_context = ::OpenTelemetry::Trace.current_span(parent_context).context
|
37
|
+
links = [::OpenTelemetry::Trace::Link.new(span_context)] if span_context.valid?
|
38
|
+
|
39
|
+
::OpenTelemetry::Context.with_current(parent_context) do
|
40
|
+
tracer.in_span("consume #{message.topic}", links: links, attributes: consumer_attrs(consumer, message), kind: :consumer) do
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_inbox_consumed_one
|
47
|
+
return yield unless enabled?
|
48
|
+
|
49
|
+
inbox_name = @payload[:inbox_name]
|
50
|
+
event_name = @payload[:event_name]
|
51
|
+
status = @payload[:status]
|
52
|
+
|
53
|
+
inbox_attributes = {
|
54
|
+
"inbox.inbox_name" => inbox_name,
|
55
|
+
"inbox.event_name" => event_name,
|
56
|
+
"inbox.status" => status
|
57
|
+
}.compact
|
58
|
+
|
59
|
+
tracer.in_span("inbox #{inbox_name} process", attributes: inbox_attributes, kind: :consumer) do
|
60
|
+
yield
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_error
|
65
|
+
return yield unless enabled?
|
66
|
+
|
67
|
+
current_span = OpenTelemetry::Trace.current_span
|
68
|
+
current_span&.status = OpenTelemetry::Trace::Status.error
|
69
|
+
|
70
|
+
yield
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def tracer
|
76
|
+
::Sbmt::KafkaConsumer::Instrumentation::OpenTelemetryLoader.instance.tracer
|
77
|
+
end
|
78
|
+
|
79
|
+
def consumer_attrs(consumer, message)
|
80
|
+
attributes = {
|
81
|
+
"messaging.system" => "kafka",
|
82
|
+
"messaging.destination" => message.topic,
|
83
|
+
"messaging.destination_kind" => "topic",
|
84
|
+
"messaging.kafka.consumer_group" => consumer.topic.consumer_group.id,
|
85
|
+
"messaging.kafka.partition" => message.partition,
|
86
|
+
"messaging.kafka.offset" => message.offset
|
87
|
+
}
|
88
|
+
|
89
|
+
message_key = extract_message_key(message.key)
|
90
|
+
attributes["messaging.kafka.message_key"] = message_key if message_key
|
91
|
+
|
92
|
+
attributes.compact
|
93
|
+
end
|
94
|
+
|
95
|
+
def extract_message_key(key)
|
96
|
+
# skip encode if already valid utf8
|
97
|
+
return key if key.nil? || (key.encoding == Encoding::UTF_8 && key.valid_encoding?)
|
98
|
+
|
99
|
+
key.encode(Encoding::UTF_8)
|
100
|
+
rescue Encoding::UndefinedConversionError
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Instrumentation
|
6
|
+
class ReadinessListener
|
7
|
+
include ListenerHelper
|
8
|
+
include KafkaConsumer::Probes::Probe
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
setup_subscription
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_app_running(_event)
|
15
|
+
@ready = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_app_stopping(_event)
|
19
|
+
@ready = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def probe(_env)
|
23
|
+
ready? ? probe_ok(ready: true) : probe_error(ready: false)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ready?
|
29
|
+
@ready
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_subscription
|
33
|
+
Karafka::App.monitor.subscribe(self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sentry-ruby"
|
4
|
+
require_relative "tracer"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module KafkaConsumer
|
8
|
+
module Instrumentation
|
9
|
+
class SentryTracer < ::Sbmt::KafkaConsumer::Instrumentation::Tracer
|
10
|
+
CONSUMER_ERROR_TYPES = %w[
|
11
|
+
consumer.base.consume_one
|
12
|
+
consumer.inbox.consume_one
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
def trace(&block)
|
16
|
+
return handle_consumed_one(&block) if @event_id == "consumer.consumed_one"
|
17
|
+
return handle_error(&block) if @event_id == "error.occurred"
|
18
|
+
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_consumed_one
|
23
|
+
return yield unless ::Sentry.initialized?
|
24
|
+
|
25
|
+
consumer = @payload[:caller]
|
26
|
+
message = @payload[:message]
|
27
|
+
trace_id = @payload[:trace_id]
|
28
|
+
|
29
|
+
scope, transaction = start_transaction(trace_id, consumer, message)
|
30
|
+
|
31
|
+
begin
|
32
|
+
yield
|
33
|
+
rescue
|
34
|
+
finish_transaction(transaction, 500)
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
|
38
|
+
finish_transaction(transaction, 200)
|
39
|
+
scope.clear
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_error
|
43
|
+
return yield unless ::Sentry.initialized?
|
44
|
+
|
45
|
+
exception = @payload[:error]
|
46
|
+
return yield unless exception.respond_to?(:message)
|
47
|
+
|
48
|
+
::Sentry.with_scope do |scope|
|
49
|
+
if detailed_logging_enabled?
|
50
|
+
message = @payload[:message]
|
51
|
+
if message.present?
|
52
|
+
contexts = {
|
53
|
+
payload: message_payload(message),
|
54
|
+
metadata: message.metadata
|
55
|
+
}
|
56
|
+
scope.set_contexts(contexts: contexts)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
::Sentry.capture_exception(exception)
|
60
|
+
end
|
61
|
+
|
62
|
+
yield
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def start_transaction(trace_id, consumer, message)
|
68
|
+
scope = ::Sentry.get_current_scope
|
69
|
+
scope.set_tags(trace_id: trace_id, topic: message.topic, offset: message.offset)
|
70
|
+
scope.set_transaction_name("Sbmt/KafkaConsumer/#{consumer.class.name}")
|
71
|
+
|
72
|
+
transaction = ::Sentry.start_transaction(name: scope.transaction_name, op: "kafka-consumer")
|
73
|
+
|
74
|
+
scope.set_span(transaction) if transaction
|
75
|
+
|
76
|
+
[scope, transaction]
|
77
|
+
end
|
78
|
+
|
79
|
+
def finish_transaction(transaction, status)
|
80
|
+
return unless transaction
|
81
|
+
|
82
|
+
transaction.set_http_status(status)
|
83
|
+
transaction.finish
|
84
|
+
end
|
85
|
+
|
86
|
+
def detailed_logging_enabled?
|
87
|
+
consumer = @payload[:caller]
|
88
|
+
event_type = @payload[:type]
|
89
|
+
|
90
|
+
CONSUMER_ERROR_TYPES.include?(event_type) && consumer.send(:log_payload?)
|
91
|
+
end
|
92
|
+
|
93
|
+
def message_payload(message)
|
94
|
+
message.payload
|
95
|
+
rescue => _ex
|
96
|
+
# payload triggers deserialization error
|
97
|
+
# so in that case we return raw_payload
|
98
|
+
message.raw_payload
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Instrumentation
|
6
|
+
class Tracer
|
7
|
+
def initialize(event_id, payload)
|
8
|
+
@event_id = event_id
|
9
|
+
@payload = payload
|
10
|
+
end
|
11
|
+
|
12
|
+
def trace(&block)
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Instrumentation
|
6
|
+
class TracingMonitor < ChainableMonitor
|
7
|
+
def initialize
|
8
|
+
tracers = []
|
9
|
+
tracers << OpenTelemetryTracer if defined?(OpenTelemetryTracer)
|
10
|
+
tracers << SentryTracer if defined?(SentryTracer)
|
11
|
+
|
12
|
+
super(tracers)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Instrumentation
|
6
|
+
class YabedaMetricsListener
|
7
|
+
include ListenerHelper
|
8
|
+
|
9
|
+
delegate :logger, to: ::Sbmt::KafkaConsumer
|
10
|
+
|
11
|
+
def on_statistics_emitted(event)
|
12
|
+
# statistics.emitted is being executed in the main rdkafka thread
|
13
|
+
# so we have to do it in async way to prevent thread's hang issues
|
14
|
+
report_rdkafka_stats(event)
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_consumer_consumed(event)
|
18
|
+
# batch processed
|
19
|
+
consumer = event[:caller]
|
20
|
+
|
21
|
+
Yabeda.kafka_consumer.batch_size
|
22
|
+
.measure(
|
23
|
+
consumer_base_tags(consumer),
|
24
|
+
consumer.messages.count
|
25
|
+
)
|
26
|
+
|
27
|
+
Yabeda.kafka_consumer.process_batch_latency
|
28
|
+
.measure(
|
29
|
+
consumer_base_tags(consumer),
|
30
|
+
time_elapsed_sec(event)
|
31
|
+
)
|
32
|
+
|
33
|
+
Yabeda.kafka_consumer.time_lag
|
34
|
+
.set(
|
35
|
+
consumer_base_tags(consumer),
|
36
|
+
consumer.messages.metadata.consumption_lag
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_consumer_consumed_one(event)
|
41
|
+
# one message processed by any consumer
|
42
|
+
|
43
|
+
consumer = event[:caller]
|
44
|
+
Yabeda.kafka_consumer.process_messages
|
45
|
+
.increment(consumer_base_tags(consumer))
|
46
|
+
Yabeda.kafka_consumer.process_message_latency
|
47
|
+
.measure(
|
48
|
+
consumer_base_tags(consumer),
|
49
|
+
time_elapsed_sec(event)
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_consumer_inbox_consumed_one(event)
|
54
|
+
# one message processed by InboxConsumer
|
55
|
+
Yabeda
|
56
|
+
.kafka_consumer
|
57
|
+
.inbox_consumes
|
58
|
+
.increment(consumer_inbox_tags(event))
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_error_occurred(event)
|
62
|
+
caller = event[:caller]
|
63
|
+
|
64
|
+
return unless caller.respond_to?(:messages)
|
65
|
+
|
66
|
+
# caller is a BaseConsumer subclass
|
67
|
+
case event[:type]
|
68
|
+
when "consumer.revoked.error"
|
69
|
+
Yabeda.kafka_consumer.leave_group_errors
|
70
|
+
.increment(consumer_base_tags(caller))
|
71
|
+
when "consumer.consume.error"
|
72
|
+
Yabeda.kafka_consumer.process_batch_errors
|
73
|
+
.increment(consumer_base_tags(caller))
|
74
|
+
when "consumer.base.consume_one"
|
75
|
+
Yabeda.kafka_consumer.process_message_errors
|
76
|
+
.increment(consumer_base_tags(caller))
|
77
|
+
when "consumer.inbox.consume_one"
|
78
|
+
Yabeda.kafka_consumer.inbox_consumes
|
79
|
+
.increment(consumer_inbox_tags(event))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def consumer_base_tags(consumer)
|
86
|
+
{
|
87
|
+
client: Karafka::App.config.client_id,
|
88
|
+
group_id: consumer.topic.consumer_group.id,
|
89
|
+
topic: consumer.messages.metadata.topic,
|
90
|
+
partition: consumer.messages.metadata.partition
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def consumer_inbox_tags(event)
|
95
|
+
caller = event[:caller]
|
96
|
+
|
97
|
+
consumer_base_tags(caller)
|
98
|
+
.merge(inbox_tags(event))
|
99
|
+
end
|
100
|
+
|
101
|
+
def report_rdkafka_stats(event, async: true)
|
102
|
+
thread = Thread.new do
|
103
|
+
# https://github.com/confluentinc/librdkafka/blob/master/STATISTICS.md
|
104
|
+
stats = event.payload[:statistics]
|
105
|
+
consumer_group_id = event.payload[:consumer_group_id]
|
106
|
+
consumer_group_stats = stats["cgrp"]
|
107
|
+
broker_stats = stats["brokers"]
|
108
|
+
topic_stats = stats["topics"]
|
109
|
+
|
110
|
+
report_broker_stats(broker_stats)
|
111
|
+
report_consumer_group_stats(consumer_group_id, consumer_group_stats)
|
112
|
+
report_topic_stats(consumer_group_id, topic_stats)
|
113
|
+
rescue => e
|
114
|
+
logger.error("exception happened while reporting rdkafka metrics: #{e.message}")
|
115
|
+
logger.error(e.backtrace&.join("\n"))
|
116
|
+
end
|
117
|
+
|
118
|
+
thread.join unless async
|
119
|
+
end
|
120
|
+
|
121
|
+
def report_broker_stats(brokers)
|
122
|
+
brokers.each_value do |broker_statistics|
|
123
|
+
# Skip bootstrap nodes
|
124
|
+
next if broker_statistics["nodeid"] == -1
|
125
|
+
|
126
|
+
broker_tags = {
|
127
|
+
client: Karafka::App.config.client_id,
|
128
|
+
broker: broker_statistics["nodename"]
|
129
|
+
}
|
130
|
+
|
131
|
+
Yabeda.kafka_api.calls
|
132
|
+
.increment(broker_tags, by: broker_statistics["tx"])
|
133
|
+
Yabeda.kafka_api.latency
|
134
|
+
.measure(broker_tags, broker_statistics["rtt"]["avg"])
|
135
|
+
Yabeda.kafka_api.request_size
|
136
|
+
.measure(broker_tags, broker_statistics["txbytes"])
|
137
|
+
Yabeda.kafka_api.response_size
|
138
|
+
.measure(broker_tags, broker_statistics["rxbytes"])
|
139
|
+
Yabeda.kafka_api.errors
|
140
|
+
.increment(broker_tags, by: broker_statistics["txerrs"] + broker_statistics["rxerrs"])
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def report_consumer_group_stats(group_id, group_stats)
|
145
|
+
return if group_stats.blank?
|
146
|
+
|
147
|
+
cg_tags = {
|
148
|
+
client: Karafka::App.config.client_id,
|
149
|
+
group_id: group_id,
|
150
|
+
state: group_stats["state"]
|
151
|
+
}
|
152
|
+
|
153
|
+
Yabeda.kafka_consumer.consumer_group_rebalances
|
154
|
+
.increment(cg_tags, by: group_stats["rebalance_cnt"])
|
155
|
+
end
|
156
|
+
|
157
|
+
def report_topic_stats(group_id, topic_stats)
|
158
|
+
return if topic_stats.blank?
|
159
|
+
|
160
|
+
topic_stats.each do |topic_name, topic_values|
|
161
|
+
topic_values["partitions"].each do |partition_name, partition_statistics|
|
162
|
+
next if partition_name == "-1"
|
163
|
+
|
164
|
+
# Skip until lag info is available
|
165
|
+
offset_lag = partition_statistics["consumer_lag"]
|
166
|
+
next if offset_lag == -1
|
167
|
+
|
168
|
+
Yabeda.kafka_consumer.offset_lag
|
169
|
+
.set({
|
170
|
+
client: Karafka::App.config.client_id,
|
171
|
+
group_id: group_id,
|
172
|
+
topic: topic_name,
|
173
|
+
partition: partition_name
|
174
|
+
},
|
175
|
+
offset_lag)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def time_elapsed_sec(event)
|
181
|
+
(event.payload[:time] || 0) / 1000.0
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Probes
|
6
|
+
class Host
|
7
|
+
class << self
|
8
|
+
def run_async
|
9
|
+
config = Sbmt::KafkaConsumer::Config.new
|
10
|
+
if config.probes[:port] == config.metrics[:port]
|
11
|
+
start_on_single_port(config)
|
12
|
+
else
|
13
|
+
start_on_different_ports(config)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def health_check_app(config)
|
20
|
+
::HttpHealthCheck::RackApp.configure do |c|
|
21
|
+
c.logger Rails.logger unless Rails.env.production?
|
22
|
+
|
23
|
+
liveness = config[:liveness]
|
24
|
+
if liveness[:enabled]
|
25
|
+
c.probe liveness[:path], Sbmt::KafkaConsumer::Instrumentation::LivenessListener.new(
|
26
|
+
timeout_sec: liveness[:timeout]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
readiness = config[:readiness]
|
31
|
+
if readiness[:enabled]
|
32
|
+
c.probe readiness[:path], Sbmt::KafkaConsumer::Instrumentation::ReadinessListener.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def start_on_single_port(config)
|
38
|
+
app = health_check_app(config.probes[:endpoints])
|
39
|
+
middlewares = defined?(Yabeda) ? {::Yabeda::Prometheus::Exporter => {path: config.metrics[:path]}} : {}
|
40
|
+
start_webrick(app, middlewares: middlewares, port: config.probes[:port])
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_on_different_ports(config)
|
44
|
+
::HttpHealthCheck.run_server_async(
|
45
|
+
port: config.probes[:port],
|
46
|
+
rack_app: health_check_app(config.probes[:endpoints])
|
47
|
+
)
|
48
|
+
if defined?(Yabeda)
|
49
|
+
start_webrick(
|
50
|
+
Yabeda::Prometheus::Mmap::Exporter::NOT_FOUND_HANDLER,
|
51
|
+
middlewares: {::Yabeda::Prometheus::Exporter => {path: config.metrics[:path]}},
|
52
|
+
port: config.metrics[:port]
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def start_webrick(app, middlewares:, port:)
|
58
|
+
Thread.new do
|
59
|
+
::Rack::Handler::WEBrick.run(
|
60
|
+
::Rack::Builder.new do
|
61
|
+
middlewares.each do |middleware, options|
|
62
|
+
use middleware, **options
|
63
|
+
end
|
64
|
+
run app
|
65
|
+
end,
|
66
|
+
Host: "0.0.0.0",
|
67
|
+
Port: port
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module KafkaConsumer
|
5
|
+
module Probes
|
6
|
+
module Probe
|
7
|
+
HEADERS = {"Content-Type" => "application/json"}.freeze
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
with_error_handler { probe(env) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def meta
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
|
17
|
+
def probe_ok(extra_meta = {})
|
18
|
+
[200, HEADERS, [meta.merge(extra_meta).to_json]]
|
19
|
+
end
|
20
|
+
|
21
|
+
def probe_error(extra_meta = {})
|
22
|
+
[500, HEADERS, [meta.merge(extra_meta).to_json]]
|
23
|
+
end
|
24
|
+
|
25
|
+
def with_error_handler
|
26
|
+
yield
|
27
|
+
rescue => error
|
28
|
+
probe_error(error_class: error.class.name, error_message: error.message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module KafkaConsumer
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
initializer "sbmt_kafka_consumer_yabeda.configure_rails_initialization" do
|
9
|
+
YabedaConfigurer.configure
|
10
|
+
end
|
11
|
+
|
12
|
+
# it must be consistent with sbmt_karafka initializers' name
|
13
|
+
initializer "sbmt_kafka_consumer_karafka_init.configure_rails_initialization",
|
14
|
+
before: "karafka.require_karafka_boot_file" do
|
15
|
+
# skip loading native karafka.rb, because we want custom init process
|
16
|
+
Karafka.instance_eval do
|
17
|
+
def boot_file; false; end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer "sbmt_kafka_consumer_opentelemetry_init.configure_rails_initialization",
|
22
|
+
after: "opentelemetry.configure" do
|
23
|
+
require "sbmt/kafka_consumer/instrumentation/open_telemetry_loader" if defined?(::OpenTelemetry)
|
24
|
+
end
|
25
|
+
|
26
|
+
config.after_initialize do
|
27
|
+
require "sbmt/kafka_consumer/instrumentation/sentry_tracer" if defined?(::Sentry)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Sbmt
|
2
|
+
module KafkaConsumer
|
3
|
+
module Routing
|
4
|
+
class KarafkaV1ConsumerMapper < Karafka::Routing::ConsumerMapper
|
5
|
+
def call(raw_consumer_group_name)
|
6
|
+
client_id = ActiveSupport::Inflector.underscore(Karafka::App.config.client_id).tr("/", "_")
|
7
|
+
"#{client_id}_#{raw_consumer_group_name}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|