-
Notifications
You must be signed in to change notification settings - Fork 212
refactor(ingest): use fixed Kafka event topic #5017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
|
|
||
| "github.com/openmeterio/openmeter/app/config" | ||
| ) | ||
|
|
||
| func TestNewEventTopic(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| ingestConfig config.KafkaIngestConfiguration | ||
| namespaceConfig config.NamespaceConfiguration | ||
| expected EventTopic | ||
| }{ | ||
| { | ||
| name: "explicit topic", | ||
| ingestConfig: config.KafkaIngestConfiguration{ | ||
| EventsTopic: "events", | ||
| EventsTopicTemplate: "om_%s_events", | ||
| }, | ||
| namespaceConfig: config.NamespaceConfiguration{Default: "default"}, | ||
| expected: EventTopic("events"), | ||
| }, | ||
| { | ||
| name: "default namespace fallback", | ||
| ingestConfig: config.KafkaIngestConfiguration{ | ||
| EventsTopicTemplate: "om_%s_events", | ||
| }, | ||
| namespaceConfig: config.NamespaceConfiguration{Default: "default"}, | ||
| expected: EventTopic("om_default_events"), | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| assert.Equal(t, test.expected, NewEventTopic(test.ingestConfig, test.namespaceConfig)) | ||
| }) | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,6 @@ import ( | |
| "go.opentelemetry.io/otel/trace" | ||
|
|
||
| "github.com/openmeterio/openmeter/openmeter/ingest/kafkaingest/serializer" | ||
| "github.com/openmeterio/openmeter/openmeter/ingest/kafkaingest/topicresolver" | ||
| "github.com/openmeterio/openmeter/pkg/clock" | ||
| pkgkafka "github.com/openmeterio/openmeter/pkg/kafka" | ||
| kafkametrics "github.com/openmeterio/openmeter/pkg/kafka/metrics" | ||
|
|
@@ -40,7 +39,7 @@ func FromIngestedAt(s string) (time.Time, error) { | |
| type Collector struct { | ||
| Producer *kafka.Producer | ||
| Serializer serializer.Serializer | ||
| TopicResolver topicresolver.Resolver | ||
| Topic string | ||
| TopicProvisioner pkgkafka.TopicProvisioner | ||
| TopicPartitions int | ||
|
|
||
|
|
@@ -51,7 +50,7 @@ type Collector struct { | |
| func NewCollector( | ||
| producer *kafka.Producer, | ||
| serializer serializer.Serializer, | ||
| resolver topicresolver.Resolver, | ||
| topic string, | ||
| provisioner pkgkafka.TopicProvisioner, | ||
| partitions int, | ||
| logger *slog.Logger, | ||
|
|
@@ -63,8 +62,8 @@ func NewCollector( | |
| if serializer == nil { | ||
| return nil, fmt.Errorf("serializer is required") | ||
| } | ||
| if resolver == nil { | ||
| return nil, fmt.Errorf("topic name resolver is required") | ||
| if topic == "" { | ||
| return nil, fmt.Errorf("topic is required") | ||
| } | ||
|
|
||
| if provisioner == nil { | ||
|
|
@@ -80,7 +79,7 @@ func NewCollector( | |
| return &Collector{ | ||
| Producer: producer, | ||
| Serializer: serializer, | ||
| TopicResolver: resolver, | ||
| Topic: topic, | ||
| TopicProvisioner: provisioner, | ||
| TopicPartitions: partitions, | ||
| Logger: logger, | ||
|
|
@@ -105,12 +104,7 @@ func (s Collector) Ingest(ctx context.Context, namespace string, ev event.Event) | |
| span.End() | ||
| }() | ||
|
|
||
| span.AddEvent("resolved namespace to kafka topic") | ||
| topicName, err := s.TopicResolver.Resolve(ctx, namespace) | ||
| if err != nil { | ||
| err = fmt.Errorf("failed to resolve namespace to topic name: %w", err) | ||
| return err | ||
| } | ||
| topicName := s.Topic | ||
| span.SetAttributes(semconv.MessagingDestinationName(topicName)) | ||
|
Comment on lines
+107
to
108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Subscribe the sink to the configured event topic. The collector now publishes all namespaces to 📍 Affects 1 file
🤖 Prompt for AI Agents |
||
|
|
||
| // Make sure topic is provisioned | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package kafkaingest | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| cloudevents "github.com/cloudevents/sdk-go/v2/event" | ||
| "github.com/confluentinc/confluent-kafka-go/v2/kafka" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| "go.opentelemetry.io/otel/trace/noop" | ||
|
|
||
| "github.com/openmeterio/openmeter/openmeter/testutils" | ||
| pkgkafka "github.com/openmeterio/openmeter/pkg/kafka" | ||
| ) | ||
|
|
||
| type recordingSerializer struct { | ||
| topics []string | ||
| err error | ||
| } | ||
|
|
||
| func (s *recordingSerializer) SerializeKey(topic string, _ string, _ cloudevents.Event) ([]byte, error) { | ||
| s.topics = append(s.topics, topic) | ||
|
|
||
| return nil, s.err | ||
| } | ||
|
|
||
| func (s *recordingSerializer) SerializeValue(_ string, _ cloudevents.Event) ([]byte, error) { | ||
| return nil, nil | ||
|
Comment on lines
+18
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Exercise both serializer calls in the fixed-topic test.
As per path instructions, tests must be comprehensive and cover the changes. Suggested test adjustment type recordingSerializer struct {
topics []string
+ valueTopics []string
err error
}
func (s *recordingSerializer) SerializeKey(topic string, _ string, _ cloudevents.Event) ([]byte, error) {
s.topics = append(s.topics, topic)
- return nil, s.err
+ return nil, nil
}
-func (s *recordingSerializer) SerializeValue(_ string, _ cloudevents.Event) ([]byte, error) {
- return nil, nil
+func (s *recordingSerializer) SerializeValue(topic string, _ cloudevents.Event) ([]byte, error) {
+ s.valueTopics = append(s.valueTopics, topic)
+ return nil, s.err
}
+ assert.Equal(t, []string{"om_default_events", "om_default_events"}, serializer.valueTopics)Also applies to: 59-87 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
|
|
||
| func (s *recordingSerializer) GetFormat() string { | ||
| return "" | ||
| } | ||
|
|
||
| func (s *recordingSerializer) GetKeySchemaId() int { | ||
| return 0 | ||
| } | ||
|
|
||
| func (s *recordingSerializer) GetValueSchemaId() int { | ||
| return 0 | ||
| } | ||
|
|
||
| type recordingTopicProvisioner struct { | ||
| topics []pkgkafka.TopicConfig | ||
| } | ||
|
|
||
| func (p *recordingTopicProvisioner) Provision(_ context.Context, topics ...pkgkafka.TopicConfig) error { | ||
| p.topics = append(p.topics, topics...) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (p *recordingTopicProvisioner) DeProvision(_ context.Context, _ ...string) error { | ||
| return nil | ||
| } | ||
|
|
||
| func TestCollectorUsesFixedTopic(t *testing.T) { | ||
| serializerError := errors.New("stop before producing") | ||
| serializer := &recordingSerializer{err: serializerError} | ||
| provisioner := &recordingTopicProvisioner{} | ||
|
|
||
| collector, err := NewCollector( | ||
| &kafka.Producer{}, | ||
| serializer, | ||
| "om_default_events", | ||
| provisioner, | ||
| 1, | ||
| testutils.NewDiscardLogger(t), | ||
| noop.NewTracerProvider().Tracer("test"), | ||
| ) | ||
| require.NoError(t, err) | ||
|
|
||
| ev := cloudevents.New() | ||
| ev.SetID("event-id") | ||
|
|
||
| for _, namespace := range []string{"default", "customer"} { | ||
| err = collector.Ingest(t.Context(), namespace, ev) | ||
| require.ErrorIs(t, err, serializerError) | ||
| } | ||
|
|
||
| assert.Equal(t, []string{"om_default_events", "om_default_events"}, serializer.topics) | ||
| assert.Equal(t, []pkgkafka.TopicConfig{ | ||
| {Name: "om_default_events", Partitions: 1}, | ||
| {Name: "om_default_events", Partitions: 1}, | ||
| }, provisioner.topics) | ||
| } | ||
|
|
||
| func TestNewCollectorRequiresTopic(t *testing.T) { | ||
| collector, err := NewCollector( | ||
| &kafka.Producer{}, | ||
| &recordingSerializer{}, | ||
| "", | ||
| &recordingTopicProvisioner{}, | ||
| 1, | ||
| testutils.NewDiscardLogger(t), | ||
| noop.NewTracerProvider().Tracer("test"), | ||
| ) | ||
|
|
||
| require.EqualError(t, err, "topic is required") | ||
| assert.Nil(t, collector) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
ingest.kafka.eventsTopicis set to a valid Kafka topic such aseventsorom_events,NewEventTopicreturns it unchanged while the sink subscribes only to topics matching its namespace-topic regexp. Events are accepted and queued in Kafka, but the sink never consumes them for storage or usage aggregation.Prompt To Fix With AI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regexp used for topic name matching can be set in the sink worker configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right—the sink's
NamespaceTopicRegexpis configurable, so an explicitingest.kafka.eventsTopicis supported as long as the sink configuration matches it. My original comment treated the default regexp as invariant, which was too strong. The remaining concern is only an operational misconfiguration risk: the ingest topic and sink regexp are independent, so a mismatch would leave events unconsumed. I don't consider that a blocking defect in this PR and would withdraw the finding.