Skip to content

Commit 145e234

Browse files
authored
feat(consumoor): add core infrastructure for Kafka-to-ClickHouse pipeline (#755)
* feat(consumoor): add core infrastructure for Kafka-to-ClickHouse pipeline Introduce the consumoor service — a Kafka consumer that decodes DecoratedEvent protobufs and writes flat rows to ClickHouse using typed columnar batches (ch-go), replacing Vector's VRL transforms. This commit contains all design-critical code: - CLI entry point and service orchestrator - Kafka ingestion via Benthos kafka_franz with batch/message delivery - ClickHouse writer with per-table batched inserts, retries, and pooling - Event-to-route dispatch engine with conditional routing support - Core interfaces: Route, ColumnarBatch, Writer, WriteErrorClassifier - CommonMetadata extraction from DecoratedEvent proto fields - Conversion helpers for IP/UUID/UInt128/UInt256/decimal types - Vector-compatible SeaHash64 implementation - Route registry (empty — domain packages register in later commits) - chgo-rowgen code generator for typed batch structs - Prometheus telemetry metrics - Example configuration The service compiles and starts but processes 0 events until table routes are registered in subsequent commits. Removes unused BatchBytes config field (writer does not use it). * refactor(consumoor): split large files and improve code organization Split writer.go (1017 lines) into 6 focused files and benthos.go (909 lines) into 4 focused files. Merge catalog package into flattener, split Snapshot() into separate Snapshotter interface, remove redundant resolvedChGoConfig defaults, fix silent flush error discard on shutdown, fix buffer gauge leak in shutdown drain, and harden Benthos error check.
1 parent 8dad694 commit 145e234

32 files changed

Lines changed: 5691 additions & 180 deletions

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.gen.go linguist-generated=true

cmd/consumoor.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//nolint:dupl // disable duplicate code warning for cmds
2+
package cmd
3+
4+
import (
5+
"os"
6+
7+
"github.com/creasty/defaults"
8+
"github.com/ethpandaops/xatu/pkg/consumoor"
9+
"github.com/spf13/cobra"
10+
yaml "gopkg.in/yaml.v3"
11+
)
12+
13+
var (
14+
consumoorCfgFile string
15+
)
16+
17+
// ConsumoorOverride defines a CLI/env override for consumoor config.
18+
type ConsumoorOverride struct {
19+
FlagHelper func(cmd *cobra.Command)
20+
Setter func(cmd *cobra.Command, overrides *consumoor.Override) error
21+
}
22+
23+
// ConsumoorOverrideConfig defines how a single override is wired.
24+
type ConsumoorOverrideConfig struct {
25+
FlagName string
26+
EnvName string
27+
Description string
28+
OverrideFunc func(val string, overrides *consumoor.Override)
29+
}
30+
31+
func createConsumoorOverride(config ConsumoorOverrideConfig) ConsumoorOverride {
32+
return ConsumoorOverride{
33+
FlagHelper: func(cmd *cobra.Command) {
34+
cmd.Flags().String(config.FlagName, "", config.Description+` (env: `+config.EnvName+`)`)
35+
},
36+
Setter: func(cmd *cobra.Command, overrides *consumoor.Override) error {
37+
val := ""
38+
39+
if cmd.Flags().Changed(config.FlagName) {
40+
val = cmd.Flags().Lookup(config.FlagName).Value.String()
41+
}
42+
43+
if os.Getenv(config.EnvName) != "" {
44+
val = os.Getenv(config.EnvName)
45+
}
46+
47+
if val == "" {
48+
return nil
49+
}
50+
51+
config.OverrideFunc(val, overrides)
52+
53+
return nil
54+
},
55+
}
56+
}
57+
58+
// ConsumoorOverrides is the list of CLI/env overrides for consumoor.
59+
var ConsumoorOverrides = []ConsumoorOverride{
60+
createConsumoorOverride(ConsumoorOverrideConfig{
61+
FlagName: "metrics-addr",
62+
EnvName: "METRICS_ADDR",
63+
Description: "sets the metrics address",
64+
OverrideFunc: func(val string, overrides *consumoor.Override) {
65+
overrides.MetricsAddr.Enabled = true
66+
overrides.MetricsAddr.Value = val
67+
},
68+
}),
69+
}
70+
71+
// consumoorCmd represents the consumoor command.
72+
var consumoorCmd = &cobra.Command{
73+
Use: "consumoor",
74+
Short: "Runs Xatu in consumoor mode.",
75+
Long: `Runs Xatu in consumoor mode, which consumes events from Kafka
76+
and writes them directly to ClickHouse, replacing Vector.`,
77+
Run: func(cmd *cobra.Command, args []string) {
78+
initCommon()
79+
80+
config, err := loadConsumoorConfigFromFile(consumoorCfgFile)
81+
if err != nil {
82+
log.Fatal(err)
83+
}
84+
85+
log = getLogger(config.LoggingLevel, "")
86+
87+
log.WithField("location", consumoorCfgFile).Info("Loaded config")
88+
89+
overrides := &consumoor.Override{}
90+
for _, override := range ConsumoorOverrides {
91+
if errr := override.Setter(cmd, overrides); errr != nil {
92+
log.Fatal(errr)
93+
}
94+
}
95+
96+
c, err := consumoor.New(cmd.Context(), log, config, overrides)
97+
if err != nil {
98+
log.Fatal(err)
99+
}
100+
101+
if err := c.Start(cmd.Context()); err != nil {
102+
log.Fatal(err)
103+
}
104+
105+
log.Info("Xatu consumoor exited - cya!")
106+
},
107+
}
108+
109+
func init() {
110+
rootCmd.AddCommand(consumoorCmd)
111+
112+
consumoorCmd.Flags().StringVar(&consumoorCfgFile, "config", "consumoor.yaml", "config file (default is consumoor.yaml)")
113+
114+
for _, override := range ConsumoorOverrides {
115+
override.FlagHelper(consumoorCmd)
116+
}
117+
}
118+
119+
func loadConsumoorConfigFromFile(file string) (*consumoor.Config, error) {
120+
if file == "" {
121+
file = "consumoor.yaml"
122+
}
123+
124+
config := &consumoor.Config{}
125+
126+
if err := defaults.Set(config); err != nil {
127+
return nil, err
128+
}
129+
130+
yamlFile, err := os.ReadFile(file)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
type plain consumoor.Config
136+
137+
if err := yaml.Unmarshal(yamlFile, (*plain)(config)); err != nil {
138+
return nil, err
139+
}
140+
141+
return config, nil
142+
}

example_consumoor.yaml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
logging: "info"
2+
metricsAddr: ":9090"
3+
# pprofAddr: ":6060"
4+
5+
kafka:
6+
brokers:
7+
- localhost:9092
8+
topics:
9+
- "^general-.+"
10+
consumerGroup: xatu-consumoor-general
11+
encoding: json # switch to "protobuf" after Vector removal
12+
# offsetDefault: oldest
13+
# fetchMinBytes: 1
14+
# fetchWaitMaxMs: 500
15+
# maxPartitionFetchBytes: 10485760
16+
# sessionTimeoutMs: 30000
17+
# heartbeatIntervalMs: 3000
18+
commitInterval: 5s # Kafka offset commit interval
19+
# deliveryMode: batch # batch (faster) or message (safer, per-message flush)
20+
# rejectedTopic: xatu-consumoor-rejected
21+
# tls: false
22+
# sasl:
23+
# mechanism: PLAIN
24+
# user: "username"
25+
# password: "password"
26+
27+
clickhouse:
28+
dsn: "clickhouse://localhost:9000/default"
29+
# chgo:
30+
# queryTimeout: 30s
31+
# maxRetries: 3
32+
# retryBaseDelay: 100ms
33+
# retryMaxDelay: 2s
34+
# maxConns: 8
35+
# minConns: 1
36+
# connMaxLifetime: 1h
37+
# connMaxIdleTime: 10m
38+
# healthCheckPeriod: 30s
39+
# poolMetricsInterval: 15s
40+
defaults:
41+
batchSize: 200000
42+
flushInterval: 1s
43+
bufferSize: 200000
44+
tables:
45+
# Canonical tables default to insertSettings.insert_quorum: auto (majority).
46+
# Set insertSettings.insert_quorum explicitly to override that behavior.
47+
beacon_api_eth_v1_beacon_committee:
48+
batchSize: 1000000
49+
bufferSize: 1000000
50+
# insertSettings:
51+
# insert_quorum: 2
52+
# insert_quorum_timeout: 60000
53+
beacon_api_eth_v1_events_attestation:
54+
batchSize: 1000000
55+
flushInterval: 5s
56+
bufferSize: 1000000
57+
# canonical_beacon_block:
58+
# insertSettings:
59+
# insert_quorum: 3
60+
# insert_quorum_timeout: 60000
61+
62+
# Optional: disable specific events (replaces Vector blackhole sinks)
63+
# disabledEvents:
64+
# - BEACON_API_ETH_V1_DEBUG_FORK_CHOICE
65+
# - BEACON_API_ETH_V1_DEBUG_FORK_CHOICE_V2

0 commit comments

Comments
 (0)