-
Notifications
You must be signed in to change notification settings - Fork 3
[MODNOTES-295] Kafka producer foundation + Note create events #373
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: master
Are you sure you want to change the base?
Changes from all commits
e665254
2249419
b6aa626
829a2a0
66c4079
c7293b8
555c47c
1c855b4
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 |
|---|---|---|
|
|
@@ -45,6 +45,55 @@ services: | |
| networks: | ||
| - mod-notes-local | ||
|
|
||
| kafka: | ||
| image: apache/kafka-native | ||
| networks: | ||
| - mod-notes-local | ||
| ports: | ||
| - ${KAFKA_EXTERNAL_PORT}:${KAFKA_EXTERNAL_PORT} | ||
| - ${KAFKA_PORT}:9093 | ||
| environment: | ||
| KAFKA_LISTENERS: CONTROLLER://localhost:9091,HOST://0.0.0.0:${KAFKA_EXTERNAL_PORT},DOCKER://0.0.0.0:9093 | ||
| KAFKA_ADVERTISED_LISTENERS: HOST://localhost:${KAFKA_EXTERNAL_PORT},DOCKER://kafka:9093 | ||
| KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,DOCKER:PLAINTEXT,HOST:PLAINTEXT | ||
| KAFKA_NODE_ID: 1 | ||
| KAFKA_PROCESS_ROLES: broker,controller | ||
| KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9091 | ||
| KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER | ||
| KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER | ||
| KAFKA_LOG_DIRS: /tmp/kraft-combined-logs | ||
| KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | ||
| KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' | ||
|
|
||
| kafka-ui: | ||
| image: provectuslabs/kafka-ui:latest | ||
| networks: | ||
| - mod-notes-local | ||
| ports: | ||
| - ${KAFKA_UI_PORT}:8080 | ||
| environment: | ||
| DYNAMIC_CONFIG_ENABLED: 'true' | ||
| KAFKA_CLUSTERS_0_NAME: local | ||
| KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9093 | ||
| depends_on: | ||
| - kafka | ||
|
|
||
| kafka-topic-init: | ||
| image: apache/kafka:latest | ||
| depends_on: | ||
| - kafka | ||
| environment: | ||
| KAFKA_HOST: kafka | ||
| KAFKA_PORT: 9093 | ||
| ENV: ${ENV} | ||
| TENANTS: ${KAFKA_INIT_TENANTS} | ||
| KAFKA_TOPIC_PARTITIONS: ${KAFKA_TOPIC_PARTITIONS} | ||
| volumes: | ||
| - ./kafka-init.sh:/usr/bin/kafka-init.sh | ||
| entrypoint: ["bash", "/usr/bin/kafka-init.sh"] | ||
| networks: | ||
| - mod-notes-local | ||
|
Comment on lines
+81
to
+95
Collaborator
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. Don't think this is needed as mod-notes doesn't consume any topic, so no external topics should be created. Topic for produce will be created during enabling tenant in the module. |
||
|
|
||
| networks: | ||
| mod-notes-local: | ||
| driver: bridge | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #!/bin/bash | ||
| set -e | ||
|
|
||
| # Pre-creates the Note domain-event topics for mod-notes so they are visible in Kafka UI straight away | ||
| KAFKA_BROKER="${KAFKA_HOST}:${KAFKA_PORT}" | ||
| echo "Waiting for Kafka broker at $KAFKA_BROKER..." | ||
|
|
||
| until /opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server "$KAFKA_BROKER" > /dev/null 2>&1; do | ||
| echo "Kafka broker not ready yet, waiting..." | ||
| sleep 2 | ||
| done | ||
|
|
||
| echo "Kafka broker is ready!" | ||
|
|
||
| KAFKA_TOPICS_CMD="/opt/kafka/bin/kafka-topics.sh" | ||
|
|
||
| # Comma-separated list of tenants (from the TENANTS env var), default "diku,test". | ||
| IFS=',' read -ra TENANT_LIST <<< "${TENANTS:-diku,test}" | ||
|
|
||
| for TENANT in "${TENANT_LIST[@]}"; do | ||
| TENANT_TRIMMED=$(echo "$TENANT" | xargs) | ||
| TOPIC="${ENV}.${TENANT_TRIMMED}.notes.note" | ||
| $KAFKA_TOPICS_CMD \ | ||
| --create \ | ||
| --bootstrap-server "$KAFKA_BROKER" \ | ||
| --replication-factor 1 \ | ||
| --partitions "${KAFKA_TOPIC_PARTITIONS}" \ | ||
| --topic "$TOPIC" \ | ||
| --if-not-exists | ||
| echo "Created topic: $TOPIC" | ||
| done | ||
|
|
||
| echo "Kafka topics created successfully." | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package org.folio.notes.config; | ||
|
|
||
| import java.util.UUID; | ||
| import org.folio.notes.domain.dto.Note; | ||
| import org.folio.notes.domain.event.DomainEvent; | ||
| import org.springframework.boot.kafka.autoconfigure.KafkaProperties; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.kafka.core.DefaultKafkaProducerFactory; | ||
| import org.springframework.kafka.core.KafkaTemplate; | ||
| import org.springframework.kafka.core.ProducerFactory; | ||
|
|
||
| /** | ||
| * Kafka producer configuration for Note domain events. | ||
| * | ||
| * <p>Defines the {@link ProducerFactory} and {@link KafkaTemplate} used to publish {@link DomainEvent} envelopes | ||
| * keyed by the Note id. Serializers are driven by the {@code spring.kafka.producer} configuration | ||
| * ({@code UUIDSerializer} for the key, {@code JacksonJsonSerializer} for the value).</p> | ||
| */ | ||
| @Configuration | ||
| public class KafkaConfiguration { | ||
|
|
||
| @Bean | ||
| public ProducerFactory<UUID, DomainEvent<Note>> noteEventProducerFactory(KafkaProperties kafkaProperties) { | ||
| return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties()); | ||
| } | ||
|
|
||
| @Bean | ||
| public KafkaTemplate<UUID, DomainEvent<Note>> noteEventKafkaTemplate( | ||
| ProducerFactory<UUID, DomainEvent<Note>> noteEventProducerFactory) { | ||
| return new KafkaTemplate<>(noteEventProducerFactory); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package org.folio.notes.domain.event; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import java.util.UUID; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| /** | ||
| * Envelope for a mod-notes domain event. | ||
| * | ||
| * @param <T> the payload type carried in the {@code old} and {@code new} fields (a full entity snapshot). | ||
| */ | ||
| @Data | ||
| @NoArgsConstructor | ||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| public class DomainEvent<T> { | ||
|
|
||
| private UUID id; | ||
| @JsonProperty("old") | ||
| private T oldEntity; | ||
| @JsonProperty("new") | ||
| private T newEntity; | ||
| private DomainEventType type; | ||
| private String tenant; | ||
| private String ts; | ||
|
|
||
| @JsonCreator | ||
| public DomainEvent(@JsonProperty("id") UUID id, | ||
| @JsonProperty("old") T oldEntity, | ||
| @JsonProperty("new") T newEntity, | ||
| @JsonProperty("type") DomainEventType type, | ||
| @JsonProperty("tenant") String tenant, | ||
| @JsonProperty("ts") String ts) { | ||
| this.id = id; | ||
| this.oldEntity = oldEntity; | ||
| this.newEntity = newEntity; | ||
| this.type = type; | ||
| this.tenant = tenant; | ||
| this.ts = ts; | ||
| } | ||
|
|
||
| public static <T> DomainEvent<T> createEvent(UUID id, T newEntity, String tenant) { | ||
| return new DomainEvent<>(id, null, newEntity, DomainEventType.CREATE, tenant, currentTs()); | ||
| } | ||
|
|
||
| public static <T> DomainEvent<T> updateEvent(UUID id, T oldEntity, T newEntity, String tenant) { | ||
| return new DomainEvent<>(id, oldEntity, newEntity, DomainEventType.UPDATE, tenant, currentTs()); | ||
| } | ||
|
|
||
| public static <T> DomainEvent<T> deleteEvent(UUID id, T oldEntity, String tenant) { | ||
| return new DomainEvent<>(id, oldEntity, null, DomainEventType.DELETE, tenant, currentTs()); | ||
| } | ||
|
|
||
| private static String currentTs() { | ||
| return String.valueOf(System.currentTimeMillis()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package org.folio.notes.domain.event; | ||
|
|
||
| public enum DomainEventType { | ||
| CREATE, UPDATE, DELETE | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package org.folio.notes.integration.kafka; | ||
|
|
||
| import java.util.UUID; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.folio.notes.domain.dto.Note; | ||
| import org.folio.notes.domain.event.DomainEvent; | ||
| import org.folio.spring.FolioExecutionContext; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.kafka.core.KafkaTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| /** | ||
| * Publishes Note {@link DomainEvent}s to the tenant-scoped Kafka topic. | ||
| * | ||
| * <p>The topic name follows the FOLIO convention {@code {env}.{tenant}.notes.note} (for example | ||
| * {@code folio.diku.notes.note}). The tenant is resolved at publish time via {@link FolioExecutionContext} | ||
| * and {@code env} defaults to {@code folio}.</p> | ||
| */ | ||
| @Slf4j | ||
| @Component | ||
| public class NoteEventProducer { | ||
|
|
||
| private final KafkaTemplate<UUID, DomainEvent<Note>> kafkaTemplate; | ||
| private final FolioExecutionContext context; | ||
| private final String env; | ||
| private final String noteTopic; | ||
|
|
||
| public NoteEventProducer(KafkaTemplate<UUID, DomainEvent<Note>> kafkaTemplate, | ||
| FolioExecutionContext context, | ||
| @Value("${folio.environment:folio}") String env, | ||
| @Value("${folio.kafka.topics.note:notes.note}") String noteTopic) { | ||
| this.kafkaTemplate = kafkaTemplate; | ||
| this.context = context; | ||
| this.env = env; | ||
| this.noteTopic = noteTopic; | ||
| } | ||
|
|
||
| /** | ||
| * Publishes a Note domain event to the tenant-scoped topic. | ||
| * | ||
| * @param id the Note id used as the Kafka record key | ||
| * @param event the domain event envelope to publish | ||
| */ | ||
| public void publish(UUID id, DomainEvent<Note> event) { | ||
| var topic = topicName(); | ||
| try { | ||
| log.debug("publish:: sending Note event [topic: {}, id: {}, type: {}]", topic, id, event.getType()); | ||
| kafkaTemplate.send(topic, id, event); | ||
| log.info("publish:: Note event sent [topic: {}, id: {}, type: {}]", topic, id, event.getType()); | ||
| } catch (Exception e) { | ||
| log.error("publish:: failed to send Note event [topic: {}, id: {}, type: {}]", topic, id, event.getType(), e); | ||
| } | ||
| } | ||
|
Comment on lines
+44
to
+53
Collaborator
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. Kafka message must have headers. |
||
|
|
||
| private String topicName() { | ||
| return String.format("%s.%s.%s", env, context.getTenantId(), noteTopic); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.