julewire-karafka 1.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/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +58 -0
- data/docs/advanced-configuration.md +8 -0
- data/docs/configuration.md +49 -0
- data/docs/consumer-events.md +33 -0
- data/docs/message-context.md +54 -0
- data/docs/silencing.md +35 -0
- data/docs/waterdrop.md +24 -0
- data/julewire-karafka.gemspec +41 -0
- data/lib/julewire/karafka/configuration.rb +52 -0
- data/lib/julewire/karafka/event_payload.rb +91 -0
- data/lib/julewire/karafka/event_severity.rb +108 -0
- data/lib/julewire/karafka/fork_hooks.rb +46 -0
- data/lib/julewire/karafka/installer.rb +31 -0
- data/lib/julewire/karafka/message_context.rb +42 -0
- data/lib/julewire/karafka/message_execution.rb +54 -0
- data/lib/julewire/karafka/messaging_attributes.rb +56 -0
- data/lib/julewire/karafka/monitor_listener.rb +82 -0
- data/lib/julewire/karafka/monitor_subscription.rb +166 -0
- data/lib/julewire/karafka/payload_reader.rb +74 -0
- data/lib/julewire/karafka/version.rb +7 -0
- data/lib/julewire/karafka/waterdrop_installer.rb +60 -0
- data/lib/julewire/karafka/waterdrop_middleware.rb +41 -0
- data/lib/julewire/karafka.rb +45 -0
- data/lib/julewire-karafka.rb +3 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 32290d2a9bb3d886763322d94fded8636dfe961d1c5378a265293acb36d7b932
|
|
4
|
+
data.tar.gz: 131b772cff7114eed297cebfa075c785c8b7bfc2afbb480684aab813acefc763
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 56184e4abdb26392059b31b5f29323311c331374350d1c88969f98ce3fb3e4c8d3c1eb2d4dd26913244245777236f5d3d1e796ba661b65c354ac499b62088da8
|
|
7
|
+
data.tar.gz: 2693077aabaa0e6ad1bd28e9fe176e533e8e860d244fcc943a3a632a34b4972270713206bf594ad72f5c72206a82ed305882cbed3906bd744a5a6970694bbbe6
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Grebennik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Julewire Karafka
|
|
2
|
+
|
|
3
|
+
Karafka and WaterDrop integration for Julewire.
|
|
4
|
+
|
|
5
|
+
It uses structured integration points only: Karafka monitor events, WaterDrop
|
|
6
|
+
middleware, WaterDrop monitor events, and explicit per-message context
|
|
7
|
+
restoration.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
Consumer events:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class KarafkaApp < Karafka::App
|
|
15
|
+
setup do |config|
|
|
16
|
+
Julewire::Karafka.install!(monitor: config.monitor)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Message processing:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
def consume
|
|
25
|
+
messages.each do |message|
|
|
26
|
+
Julewire::Karafka.with_message(message) do
|
|
27
|
+
process(message)
|
|
28
|
+
mark_as_consumed(message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Producer headers and WaterDrop events:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Julewire::Karafka.install!(consumer: false, producer: producer)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Default behavior:
|
|
41
|
+
|
|
42
|
+
- important consumer and producer monitor events become point records
|
|
43
|
+
- message headers carry Julewire propagation carriers
|
|
44
|
+
- `with_message` restores message context and adds message attributes
|
|
45
|
+
- Karafka fork hooks call `Julewire.after_fork!`
|
|
46
|
+
- text logger listeners are not parsed or deduplicated
|
|
47
|
+
|
|
48
|
+
Inbound Kafka carriers are trusted by default for internal service traffic. Set
|
|
49
|
+
`carrier_filter` when consuming topics that external producers can write to.
|
|
50
|
+
|
|
51
|
+
## Docs
|
|
52
|
+
|
|
53
|
+
- [Configuration](docs/configuration.md)
|
|
54
|
+
- [Advanced Configuration](docs/advanced-configuration.md)
|
|
55
|
+
- [Consumer Events](docs/consumer-events.md)
|
|
56
|
+
- [Message Context](docs/message-context.md)
|
|
57
|
+
- [WaterDrop](docs/waterdrop.md)
|
|
58
|
+
- [Silencing](docs/silencing.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Advanced Configuration
|
|
2
|
+
|
|
3
|
+
| Option | Default | Purpose |
|
|
4
|
+
| --- | --- | --- |
|
|
5
|
+
| `carrier_key` | `Julewire::Core::Propagation::Carrier::DEFAULT_KEY` | Carrier key inside propagation envelopes. |
|
|
6
|
+
|
|
7
|
+
Change `carrier_key` only when an existing Kafka header contract requires a
|
|
8
|
+
different envelope key.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
## Default Path
|
|
4
|
+
|
|
5
|
+
| Option | Default | Purpose |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `enabled` | `true` | Install the integration. |
|
|
8
|
+
| `consumer_events` | `true` | Subscribe Karafka monitor events. |
|
|
9
|
+
| `producer_events` | `true` | Subscribe WaterDrop monitor events. |
|
|
10
|
+
| `propagation` | `true` | Inject and restore Julewire carriers. |
|
|
11
|
+
| `source` | `"karafka"` | Source value on Karafka and WaterDrop records. |
|
|
12
|
+
|
|
13
|
+
## Common Knobs
|
|
14
|
+
|
|
15
|
+
| Option | Default | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `carrier_max_bytes` | `nil` | Omit oversized carriers from Kafka headers. |
|
|
18
|
+
| `carrier_filter` | `nil` | Optional inbound header filter before restoring a message carrier. |
|
|
19
|
+
| `consumer_event_names` | `:important` | Consumer monitor event profile or explicit event list. |
|
|
20
|
+
| `producer_event_names` | `:important` | Producer monitor event profile or explicit event list. |
|
|
21
|
+
|
|
22
|
+
`consumer_event_names` and `producer_event_names` accept:
|
|
23
|
+
|
|
24
|
+
- `:important`
|
|
25
|
+
- `:all`
|
|
26
|
+
- an explicit event-name list
|
|
27
|
+
|
|
28
|
+
`:all` uses the monitor's registered event list when available. If the monitor
|
|
29
|
+
cannot expose that list, Julewire uses the important profile.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Julewire::Karafka.configure do |config|
|
|
35
|
+
config.consumer_event_names = :all
|
|
36
|
+
config.carrier_max_bytes = 16_384
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`carrier_filter` receives `(headers, message:)` and must return the headers to
|
|
41
|
+
trust. Leave it unset for internal topics. Return `{}` for untrusted messages:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
Julewire::Karafka.configure do |config|
|
|
45
|
+
config.carrier_filter = ->(headers, message:) {
|
|
46
|
+
internal_topic?(message[:topic]) ? headers : {}
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Consumer Events
|
|
2
|
+
|
|
3
|
+
`Julewire::Karafka.install!` subscribes to an `:important` event profile by default.
|
|
4
|
+
Set `consumer_event_names = :all` to subscribe to Karafka's registered monitor
|
|
5
|
+
events, or pass an explicit event list for application policy. If the monitor
|
|
6
|
+
cannot expose its registered events, `:all` uses the important profile.
|
|
7
|
+
|
|
8
|
+
`install!` also subscribes lightweight handlers for
|
|
9
|
+
`swarm.node.after_fork` and `swarm.manager.after_fork`. Those handlers call
|
|
10
|
+
`Julewire.after_fork!` so inherited mutexes, counters, async transports, and
|
|
11
|
+
process-local context are reset in Karafka forked processes.
|
|
12
|
+
|
|
13
|
+
Monitor events use the severity exposed by the event payload when Karafka
|
|
14
|
+
provides one. Otherwise the listener follows Karafka's logger-listener severity
|
|
15
|
+
conventions: normal lifecycle/consumer events are info, polling and swarm
|
|
16
|
+
control events are debug, selected fatal framework errors are fatal, and other
|
|
17
|
+
errors are error.
|
|
18
|
+
|
|
19
|
+
Consumer batch lifecycle records such as `consumer.consumed` include batch
|
|
20
|
+
metadata when Karafka exposes it: consumer class/id, group ids, topic,
|
|
21
|
+
partition, message count, offset range, and lag metrics.
|
|
22
|
+
|
|
23
|
+
Generic Kafka metadata also appears in the record's `neutral` section as
|
|
24
|
+
`messaging.*` formatter-coordination fields. Full Karafka or WaterDrop event
|
|
25
|
+
metadata remains under `attributes.karafka` or `attributes.waterdrop`.
|
|
26
|
+
|
|
27
|
+
These records describe the batch lifecycle. They are not the propagation
|
|
28
|
+
boundary. Generic array values in monitor payloads are summarized as
|
|
29
|
+
`{ count: n }` to keep records bounded.
|
|
30
|
+
|
|
31
|
+
Karafka `error.occurred` records use Julewire's normal top-level `error` shape,
|
|
32
|
+
including the core backtrace policy. Error event attributes still carry Karafka
|
|
33
|
+
metadata such as event type and consumer/batch details.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Message Context
|
|
2
|
+
|
|
3
|
+
Kafka batches can contain messages from different upstream requests. Do not use
|
|
4
|
+
batch events as the propagation boundary. Wrap each message while processing it:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
def consume
|
|
8
|
+
messages.each do |message|
|
|
9
|
+
Julewire::Karafka.with_message(message) do
|
|
10
|
+
process(message)
|
|
11
|
+
mark_as_consumed(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`with_message` restores the Julewire carrier from that message's headers and
|
|
18
|
+
adds message attributes for records emitted inside the block. It does not emit a
|
|
19
|
+
success log or summary by default.
|
|
20
|
+
|
|
21
|
+
Generic Kafka metadata appears in the record's `neutral` section as
|
|
22
|
+
`messaging.*` formatter-coordination fields. Full Karafka message metadata
|
|
23
|
+
remains under `attributes.karafka`.
|
|
24
|
+
|
|
25
|
+
By default, inbound Kafka carriers are trusted because most Kafka traffic is
|
|
26
|
+
internal service-to-service traffic. Set `carrier_filter` when consuming topics
|
|
27
|
+
that external producers can write to.
|
|
28
|
+
|
|
29
|
+
Code inside the block can enqueue jobs, produce messages, or emit records. Core
|
|
30
|
+
propagation carries upstream context and execution metadata, not the Karafka
|
|
31
|
+
message attributes.
|
|
32
|
+
|
|
33
|
+
Use `with_message_execution` only when consumer code chooses an explicit message
|
|
34
|
+
unit of work and wants a summary for that block:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
def consume
|
|
38
|
+
messages.each do |message|
|
|
39
|
+
Julewire::Karafka.with_message_execution(message) do
|
|
40
|
+
process(message)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This is intentionally opt-in. Batch-level summaries can conflate unrelated
|
|
47
|
+
upstream request contexts, and per-message summaries are useful only when the
|
|
48
|
+
consumer treats each message as an observable unit of work.
|
|
49
|
+
|
|
50
|
+
`with_message_execution` accepts `type:`, `id:`, `emit_summary:`,
|
|
51
|
+
`summary_event:`, `summary_severity:`, and `summary_source:`. Other keyword
|
|
52
|
+
arguments become execution fields. By default, type is `:karafka_message`, id is
|
|
53
|
+
`"topic:partition:offset"` when those message fields are present, and the
|
|
54
|
+
summary event is `"message.completed"`.
|
data/docs/silencing.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Silencing
|
|
2
|
+
|
|
3
|
+
Karafka and WaterDrop logger listeners are application-installed monitor
|
|
4
|
+
listeners, not global framework subscribers. The clean replacement is to remove
|
|
5
|
+
the native logger-listener subscription where the application installs it:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Karafka.monitor.subscribe(Karafka::Instrumentation::LoggerListener.new)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Replace that with:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
Julewire::Karafka.install!(monitor: Karafka.monitor)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If application boot code needs to keep the native listener for some
|
|
18
|
+
environments, keep a reference and unsubscribe that same listener before
|
|
19
|
+
installing Julewire:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
native_logger = Karafka::Instrumentation::LoggerListener.new
|
|
23
|
+
Karafka.monitor.subscribe(native_logger)
|
|
24
|
+
|
|
25
|
+
Karafka.monitor.unsubscribe(native_logger)
|
|
26
|
+
Julewire::Karafka.install!(monitor: Karafka.monitor)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For WaterDrop, do the same with `WaterDrop::Instrumentation::LoggerListener`
|
|
30
|
+
on the producer monitor and use
|
|
31
|
+
`Julewire::Karafka.install!(consumer: false, producer: producer)`.
|
|
32
|
+
|
|
33
|
+
This gem does not parse text logs or filter duplicate logger messages. Explicit
|
|
34
|
+
logger calls made by application code remain normal Julewire `event: "log"`
|
|
35
|
+
records.
|
data/docs/waterdrop.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# WaterDrop
|
|
2
|
+
|
|
3
|
+
Add the middleware before producing messages:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Julewire::Karafka.install!(consumer: false, producer: producer)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The middleware injects the current Julewire propagation carrier into message
|
|
10
|
+
headers. The listener turns important WaterDrop monitor events into Julewire
|
|
11
|
+
point records.
|
|
12
|
+
|
|
13
|
+
Set `producer_event_names = :all` for every WaterDrop monitor event exposed by
|
|
14
|
+
the monitor, or pass an explicit event-name list. If the monitor cannot expose
|
|
15
|
+
its registered events, `:all` uses the important profile. WaterDrop event
|
|
16
|
+
severity is read from the event payload when present; otherwise errors are
|
|
17
|
+
error and normal producer events are info.
|
|
18
|
+
|
|
19
|
+
Set `carrier_max_bytes` to omit oversized propagation carriers from Kafka
|
|
20
|
+
headers. When omitted, the message is still produced normally; it just does not
|
|
21
|
+
carry upstream Julewire context.
|
|
22
|
+
|
|
23
|
+
Message keys, headers, and payload fields are raw unless an application installs
|
|
24
|
+
a processor policy before output.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/julewire/karafka/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "julewire-karafka"
|
|
7
|
+
spec.version = Julewire::Karafka::VERSION
|
|
8
|
+
spec.authors = ["Alexander Grebennik"]
|
|
9
|
+
spec.email = ["slbug@users.noreply.github.com", "sl.bug.sl@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Karafka and WaterDrop integration for Julewire structured logging."
|
|
12
|
+
spec.description =
|
|
13
|
+
"Karafka monitor event capture, per-message context restoration, " \
|
|
14
|
+
"and WaterDrop propagation middleware for Julewire."
|
|
15
|
+
spec.homepage = "https://github.com/slbug/julewire"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.4"
|
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/slbug/julewire/tree/main/gems/karafka"
|
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/slbug/julewire/blob/main/gems/karafka/CHANGELOG.md"
|
|
21
|
+
|
|
22
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
|
+
Dir[
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE.txt",
|
|
28
|
+
"README.md",
|
|
29
|
+
"docs/**/*.md",
|
|
30
|
+
"julewire-karafka.gemspec",
|
|
31
|
+
"lib/**/*.rb"
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
spec.executables = []
|
|
35
|
+
spec.require_paths = ["lib"]
|
|
36
|
+
|
|
37
|
+
spec.add_dependency "julewire-core", ">= 1.0"
|
|
38
|
+
spec.add_dependency "karafka", ">= 2.5"
|
|
39
|
+
spec.add_dependency "waterdrop", ">= 2.10"
|
|
40
|
+
spec.add_dependency "zeitwerk", ">= 2.8.1"
|
|
41
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
class Configuration
|
|
6
|
+
IMPORTANT_CONSUMER_EVENT_NAMES = %w[
|
|
7
|
+
app.initialized
|
|
8
|
+
app.running
|
|
9
|
+
app.quiet
|
|
10
|
+
app.stopped
|
|
11
|
+
consumer.consumed
|
|
12
|
+
consumer.revoked
|
|
13
|
+
consumer.shutdown
|
|
14
|
+
dead_letter_queue.dispatched
|
|
15
|
+
filtering.throttled
|
|
16
|
+
rebalance.partitions_assigned
|
|
17
|
+
rebalance.partitions_revoked
|
|
18
|
+
swarm.node.after_fork
|
|
19
|
+
worker.completed
|
|
20
|
+
error.occurred
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
IMPORTANT_PRODUCER_EVENT_NAMES = %w[
|
|
24
|
+
producer.connected
|
|
25
|
+
producer.closed
|
|
26
|
+
message.produced_async
|
|
27
|
+
message.produced_sync
|
|
28
|
+
message.acknowledged
|
|
29
|
+
messages.produced_async
|
|
30
|
+
messages.produced_sync
|
|
31
|
+
transaction.committed
|
|
32
|
+
transaction.aborted
|
|
33
|
+
buffer.flushed_async
|
|
34
|
+
buffer.flushed_sync
|
|
35
|
+
error.occurred
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
include Julewire::Core::Integration::Settings
|
|
39
|
+
|
|
40
|
+
setting :enabled, default: true, predicate: true
|
|
41
|
+
setting :consumer_events, default: true, predicate: true
|
|
42
|
+
setting :producer_events, default: true, predicate: true
|
|
43
|
+
setting :propagation, default: true, predicate: true
|
|
44
|
+
setting :carrier_filter
|
|
45
|
+
setting :carrier_key, default: Julewire::Core::Propagation::Carrier::DEFAULT_KEY
|
|
46
|
+
setting :carrier_max_bytes, validate: byte_limit
|
|
47
|
+
setting :source, default: "karafka"
|
|
48
|
+
setting :consumer_event_names, default: :important
|
|
49
|
+
setting :producer_event_names, default: :important
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Karafka
|
|
7
|
+
module EventPayload
|
|
8
|
+
CONSUMER_BATCH_EVENTS = %w[
|
|
9
|
+
consumer.consume
|
|
10
|
+
consumer.consumed
|
|
11
|
+
].freeze
|
|
12
|
+
CONSUMER_ERROR_TYPE_PATTERN = /\Aconsumer\..*\.error\z/
|
|
13
|
+
private_constant :CONSUMER_BATCH_EVENTS, :CONSUMER_ERROR_TYPE_PATTERN
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(name, event)
|
|
17
|
+
payload = event_payload(event)
|
|
18
|
+
return {} unless payload.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
payload.each_with_object(consumer_payload(name, payload)) do |(key, value), result|
|
|
21
|
+
safe = safe_value(key, value)
|
|
22
|
+
result[key] = safe unless safe.nil?
|
|
23
|
+
end
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ payload_error: { exception_class: e.class.name } }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def event_payload(event)
|
|
29
|
+
return event.payload if event.respond_to?(:payload)
|
|
30
|
+
|
|
31
|
+
event.to_h if event.respond_to?(:to_h)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def error(event)
|
|
35
|
+
payload = event_payload(event)
|
|
36
|
+
|
|
37
|
+
PayloadReader.value(payload, :error) || PayloadReader.value(payload, :exception)
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def consumer_payload(name, payload)
|
|
43
|
+
return {} unless consumer_batch_event?(name, payload)
|
|
44
|
+
|
|
45
|
+
PayloadReader.consumer_payload(payload)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def consumer_batch_event?(name, payload)
|
|
49
|
+
event_name = name.to_s
|
|
50
|
+
return true if CONSUMER_BATCH_EVENTS.include?(event_name)
|
|
51
|
+
return false unless event_name == "error.occurred"
|
|
52
|
+
|
|
53
|
+
type = PayloadReader.value(payload, :type).to_s
|
|
54
|
+
type.match?(CONSUMER_ERROR_TYPE_PATTERN)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def safe_value(key, value)
|
|
58
|
+
case key.to_sym
|
|
59
|
+
when :caller
|
|
60
|
+
{ class: value.class.name }
|
|
61
|
+
when :message
|
|
62
|
+
PayloadReader.message_payload(value)
|
|
63
|
+
when :messages
|
|
64
|
+
{ count: Array(value).size }
|
|
65
|
+
when :error, :exception
|
|
66
|
+
nil
|
|
67
|
+
else
|
|
68
|
+
primitive_value(value)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def primitive_value(value)
|
|
73
|
+
case value
|
|
74
|
+
when nil, true, false, String, Symbol, Numeric
|
|
75
|
+
value
|
|
76
|
+
when Time
|
|
77
|
+
value.utc.iso8601(9)
|
|
78
|
+
when Hash
|
|
79
|
+
value.transform_keys(&:to_sym)
|
|
80
|
+
when Array
|
|
81
|
+
{ count: value.size }
|
|
82
|
+
else
|
|
83
|
+
{ class: value.class.name }
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError
|
|
86
|
+
{ class: value.class.name }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module EventSeverity
|
|
6
|
+
FATAL_ERROR_TYPES = %w[
|
|
7
|
+
runner.call.error
|
|
8
|
+
swarm.supervisor.error
|
|
9
|
+
worker.process.error
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
DEBUG_CONSUMER_EVENTS = %w[
|
|
13
|
+
connection.listener.fetch_loop
|
|
14
|
+
statistics.emitted
|
|
15
|
+
swarm.manager.before_fork
|
|
16
|
+
swarm.manager.control
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
ERROR_CONSUMER_EVENTS = %w[
|
|
20
|
+
swarm.manager.stopping
|
|
21
|
+
swarm.manager.terminating
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
DEBUG_PRODUCER_EVENTS = %w[
|
|
25
|
+
oauthbearer.token_refresh
|
|
26
|
+
statistics.emitted
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def consumer(name, event:, payload:)
|
|
31
|
+
payload_severity(payload) || default_consumer_severity(name.to_s, event, payload)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def producer(name, payload)
|
|
35
|
+
payload_severity(payload) || default_producer_severity(name.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def payload_severity(payload)
|
|
39
|
+
value = payload_value(payload, :severity) || payload_value(payload, :level)
|
|
40
|
+
Julewire::Core::Records::Severity.normalize(value) if value
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def default_consumer_severity(name, event, payload)
|
|
46
|
+
return error_severity(event, payload) if name == "error.occurred"
|
|
47
|
+
return fetch_loop_received_severity(event, payload) if name == "connection.listener.fetch_loop.received"
|
|
48
|
+
return notice_signal_severity(event, payload) if name == "process.notice_signal"
|
|
49
|
+
return :error if ERROR_CONSUMER_EVENTS.include?(name)
|
|
50
|
+
return :debug if DEBUG_CONSUMER_EVENTS.include?(name)
|
|
51
|
+
|
|
52
|
+
:info
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_producer_severity(name)
|
|
56
|
+
return :error if name == "error.occurred"
|
|
57
|
+
return :debug if DEBUG_PRODUCER_EVENTS.include?(name)
|
|
58
|
+
|
|
59
|
+
:info
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def error_severity(event, payload)
|
|
63
|
+
type = event_value(event, payload, :type).to_s
|
|
64
|
+
FATAL_ERROR_TYPES.include?(type) ? :fatal : :error
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fetch_loop_received_severity(event, payload)
|
|
68
|
+
messages = event_value(event, payload, :messages_buffer)
|
|
69
|
+
count = collection_count(messages)
|
|
70
|
+
|
|
71
|
+
count&.zero? ? :debug : :info
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def notice_signal_severity(event, payload)
|
|
75
|
+
signal = event_value(event, payload, :signal).to_s.upcase
|
|
76
|
+
signal.end_with?("TTIN") ? :warn : :info
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def event_value(event, payload, key)
|
|
80
|
+
payload_value(payload, key) || raw_event_value(event, key)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def payload_value(payload, key)
|
|
84
|
+
Core::Integration::Values::Read.value(payload, key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def raw_event_value(event, key)
|
|
88
|
+
raw = EventPayload.event_payload(event)
|
|
89
|
+
return raw[key] if raw.respond_to?(:key?) && raw.key?(key)
|
|
90
|
+
return raw[key.to_s] if raw.respond_to?(:key?) && raw.key?(key.to_s)
|
|
91
|
+
return event[key] if event.respond_to?(:[])
|
|
92
|
+
|
|
93
|
+
nil
|
|
94
|
+
rescue StandardError
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def collection_count(value)
|
|
99
|
+
return value[:count] if value.is_a?(Hash) && value.key?(:count)
|
|
100
|
+
return value["count"] if value.is_a?(Hash) && value.key?("count")
|
|
101
|
+
return value.size if value.respond_to?(:size)
|
|
102
|
+
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module ForkHooks
|
|
6
|
+
EVENTS = %w[
|
|
7
|
+
swarm.node.after_fork
|
|
8
|
+
swarm.manager.after_fork
|
|
9
|
+
].freeze
|
|
10
|
+
INSTALL_STATE = Core::Integration::IvarState.new(:@julewire_karafka_fork_hooks_state)
|
|
11
|
+
private_constant :EVENTS, :INSTALL_STATE
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def subscribe!(monitor, configuration: Configuration.new)
|
|
15
|
+
return unless configuration.enabled?
|
|
16
|
+
return unless monitor.respond_to?(:subscribe)
|
|
17
|
+
|
|
18
|
+
state = INSTALL_STATE.fetch_or_store(monitor) { { events: [].freeze }.freeze }
|
|
19
|
+
subscribed_events = Array(state[:events]).dup
|
|
20
|
+
EVENTS.each do |event_name|
|
|
21
|
+
next if subscribed_events.include?(event_name)
|
|
22
|
+
|
|
23
|
+
subscribed_events << event_name if subscribe_event(monitor, event_name)
|
|
24
|
+
end
|
|
25
|
+
INSTALL_STATE.store(monitor, { events: subscribed_events.freeze }.freeze)
|
|
26
|
+
monitor
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle(event_name, _event)
|
|
30
|
+
IntegrationHealth.with_failure_health(action: :after_fork, component: :fork_hooks, event: event_name) do
|
|
31
|
+
Julewire.after_fork!
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def subscribe_event(monitor, event_name)
|
|
38
|
+
IntegrationHealth.with_failure_health(action: :subscribe, component: :fork_hooks, event: event_name) do
|
|
39
|
+
monitor.subscribe(event_name) { handle(event_name, it) }
|
|
40
|
+
true
|
|
41
|
+
end || false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Karafka
|
|
5
|
+
module Installer
|
|
6
|
+
class << self
|
|
7
|
+
def install!(app: nil, monitor: nil, configuration: Configuration.new)
|
|
8
|
+
return false unless configuration.enabled?
|
|
9
|
+
|
|
10
|
+
monitor ||= monitor_for(app)
|
|
11
|
+
raise Error, "Karafka monitor is not available" unless monitor
|
|
12
|
+
|
|
13
|
+
ForkHooks.subscribe!(monitor, configuration: configuration)
|
|
14
|
+
if configuration.consumer_events?
|
|
15
|
+
MonitorSubscription.install!(monitor, configuration: configuration, profile: :consumer)
|
|
16
|
+
end
|
|
17
|
+
monitor
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def monitor_for(app)
|
|
23
|
+
app ||= defined?(::Karafka::App) ? ::Karafka::App : nil
|
|
24
|
+
app.config.monitor if app.respond_to?(:config)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|