Skip to content

Commit 2687b05

Browse files
[MODNOTES-295] Kafka producer foundation + Note create events (#373)
1 parent 005b203 commit 2687b05

21 files changed

Lines changed: 791 additions & 4 deletions

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ quite do yet.
3939

4040
### Compiling
4141

42-
Compile with
42+
Compile with
4343
```shell
4444
mvn clean install
4545
```
@@ -71,6 +71,10 @@ requires and provides, the permissions, and the additional module metadata.
7171

7272
Use `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` to configure the PostgreSQL database.
7373

74+
Use `KAFKA_HOST` (default `localhost`) and `KAFKA_PORT` (default `9092`) to configure the Kafka broker the module
75+
publishes domain events to. `ENV` (default `folio`) is the environment prefix used when building the tenant-scoped topic
76+
name `{ENV}.{tenant}.notes.note`.
77+
7478
`NOTES_TYPES_DEFAULTS_LIMIT` defaults to 25.
7579

7680
`MAX_RECORDS_COUNT` defaults to 1000.

descriptors/ModuleDescriptor-template.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,49 @@
313313
{
314314
"name": "NOTES_TYPES_DEFAULTS_LIMIT",
315315
"value": "25"
316+
},
317+
{
318+
"name": "KAFKA_HOST",
319+
"value": "kafka"
320+
},
321+
{
322+
"name": "KAFKA_PORT",
323+
"value": "9092"
324+
},
325+
{
326+
"name": "KAFKA_SECURITY_PROTOCOL",
327+
"value": "PLAINTEXT",
328+
"description": "Kafka security protocol used to communicate with brokers (SSL or PLAINTEXT)"
329+
},
330+
{
331+
"name": "KAFKA_SSL_KEYSTORE_LOCATION",
332+
"description": "The location of the Kafka key store file. This is optional for client and can be used for two-way authentication for client."
333+
},
334+
{
335+
"name": "KAFKA_SSL_KEYSTORE_PASSWORD",
336+
"description": "The store password for the Kafka key store file. This is optional for client and only needed if 'ssl.keystore.location' is configured."
337+
},
338+
{
339+
"name": "KAFKA_SSL_TRUSTSTORE_LOCATION",
340+
"description": "The location of the Kafka trust store file."
341+
},
342+
{
343+
"name": "KAFKA_SSL_TRUSTSTORE_PASSWORD",
344+
"description": "The password for the Kafka trust store file. If a password is not set, trust store file configured will still be used, but integrity checking is disabled."
345+
},
346+
{
347+
"name": "KAFKA_NOTE_TOPIC_PARTITIONS",
348+
"value": "1",
349+
"description": "Number of partitions for the `notes.note` topic."
350+
},
351+
{
352+
"name": "KAFKA_NOTE_TOPIC_REPLICATION_FACTOR",
353+
"description": "Replication factor for the `notes.note` topic. When not set, the broker default is used."
354+
},
355+
{
356+
"name": "ENV",
357+
"value": "folio",
358+
"description": "The logical name of the deployment, must be unique across all environments using the same shared Kafka/Elasticsearch clusters, `a-z (any case)`, `0-9`, `-`, `_` symbols only allowed"
316359
}
317360
]
318361
}

docker/.env

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,12 @@ PGADMIN_DEFAULT_PASSWORD=admin
2222
WIREMOCK_PORT=9130
2323
OKAPI_URL=http://wiremock:8080
2424

25+
# Kafka configuration
26+
ENV=folio
27+
KAFKA_HOST=kafka
28+
KAFKA_PORT=9093
29+
KAFKA_EXTERNAL_PORT=9092
30+
# Kafka UI (web) port -> http://localhost:8090
31+
KAFKA_UI_PORT=8090
32+
33+

docker/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ Configuration is managed via the `.env` file in this directory.
3434
| `PGADMIN_PORT` | PgAdmin port | `5050` |
3535
| `WIREMOCK_PORT` | WireMock (Okapi mock) port | `9130` |
3636
| `OKAPI_URL` | Okapi URL for the module | `http://wiremock:8080` |
37+
| `ENV` | Kafka topic prefix | `folio` |
38+
| `KAFKA_HOST` | Broker host (in-cluster) | `kafka` |
39+
| `KAFKA_PORT` | Broker port (in-cluster) | `9093` |
40+
| `KAFKA_EXTERNAL_PORT` | Broker port from host/IDE | `9092` |
41+
| `KAFKA_UI_PORT` | Kafka UI web port | `8090` |
3742

3843
## 🚀 Services
3944

@@ -54,6 +59,17 @@ Configuration is managed via the `.env` file in this directory.
5459
- **Access**: http://localhost:9130 (configurable via `WIREMOCK_PORT`)
5560
- **Mappings**: Located in `src/test/resources/mappings`
5661

62+
### Kafka
63+
- **Purpose**: Message broker the module publishes Note domain events to
64+
- **Image**: `apache/kafka-native` (KRaft mode, no Zookeeper)
65+
- **Access (host/IDE)**: `localhost:9092` (configurable via `KAFKA_EXTERNAL_PORT`)
66+
- **Access (in-cluster)**: `kafka:9093`
67+
- **Topics**: `{ENV}.{tenant}.notes.note` — e.g. `folio.diku.notes.note`. The module creates the topic when a tenant is enabled (via the `/_/tenant` API); the broker also auto-creates it on first publish.
68+
69+
### Kafka UI
70+
- **Purpose**: Web UI to browse topics and inspect published event messages
71+
- **Access**: http://localhost:8090 (configurable via `KAFKA_UI_PORT`)
72+
5773
## 📖 Usage
5874

5975
> **Note**: All commands in this guide assume you are in the `docker/` directory. If you're at the project root, run `cd docker` first.

docker/app-docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ services:
1414
- "${MODULE_PORT}:8081"
1515
- "${DEBUG_PORT}:5005"
1616
environment:
17+
ENV: ${ENV}
18+
KAFKA_HOST: ${KAFKA_HOST}
19+
KAFKA_PORT: ${KAFKA_PORT}
1720
DB_HOST: ${DB_HOST}
1821
DB_PORT: ${DB_PORT}
1922
DB_DATABASE: ${DB_DATABASE}
@@ -26,6 +29,7 @@ services:
2629
depends_on:
2730
- postgres
2831
- wiremock
32+
- kafka
2933
deploy:
3034
replicas: ${MODULE_REPLICAS:-1}
3135
restart_policy:

docker/infra-docker-compose.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,40 @@ services:
4545
networks:
4646
- mod-notes-local
4747

48+
kafka:
49+
image: apache/kafka-native
50+
networks:
51+
- mod-notes-local
52+
ports:
53+
- ${KAFKA_EXTERNAL_PORT}:${KAFKA_EXTERNAL_PORT}
54+
- ${KAFKA_PORT}:9093
55+
environment:
56+
KAFKA_LISTENERS: CONTROLLER://localhost:9091,HOST://0.0.0.0:${KAFKA_EXTERNAL_PORT},DOCKER://0.0.0.0:9093
57+
KAFKA_ADVERTISED_LISTENERS: HOST://localhost:${KAFKA_EXTERNAL_PORT},DOCKER://kafka:9093
58+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,DOCKER:PLAINTEXT,HOST:PLAINTEXT
59+
KAFKA_NODE_ID: 1
60+
KAFKA_PROCESS_ROLES: broker,controller
61+
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9091
62+
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
63+
KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER
64+
KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
65+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
66+
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
67+
68+
kafka-ui:
69+
image: provectuslabs/kafka-ui:latest
70+
networks:
71+
- mod-notes-local
72+
ports:
73+
- ${KAFKA_UI_PORT}:8080
74+
environment:
75+
DYNAMIC_CONFIG_ENABLED: 'true'
76+
KAFKA_CLUSTERS_0_NAME: local
77+
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9093
78+
depends_on:
79+
- kafka
80+
81+
4882
networks:
4983
mod-notes-local:
5084
driver: bridge

pom.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
2828

2929
<folio-spring-support.version>10.0.0</folio-spring-support.version>
30+
<folio-service-tools.version>6.0.0</folio-service-tools.version>
3031
<mapstruct.version>1.6.3</mapstruct.version>
3132
<jsoup.version>1.22.2</jsoup.version>
3233
<lombok.version>1.18.44</lombok.version>
@@ -64,6 +65,11 @@
6465
<artifactId>folio-spring-cql</artifactId>
6566
<version>${folio-spring-support.version}</version>
6667
</dependency>
68+
<dependency>
69+
<groupId>org.folio</groupId>
70+
<artifactId>folio-service-tools-spring-dev</artifactId>
71+
<version>${folio-service-tools.version}</version>
72+
</dependency>
6773
<dependency>
6874
<groupId>org.springframework.boot</groupId>
6975
<artifactId>spring-boot-starter-cache</artifactId>
@@ -78,6 +84,16 @@
7884
<groupId>org.springframework.boot</groupId>
7985
<artifactId>spring-boot-starter-validation</artifactId>
8086
</dependency>
87+
<dependency>
88+
<groupId>org.springframework.boot</groupId>
89+
<artifactId>spring-boot-starter-kafka</artifactId>
90+
<exclusions>
91+
<exclusion>
92+
<groupId>org.springframework.boot</groupId>
93+
<artifactId>spring-boot-starter-logging</artifactId>
94+
</exclusion>
95+
</exclusions>
96+
</dependency>
8197
<dependency>
8298
<groupId>org.springframework.boot</groupId>
8399
<artifactId>spring-boot-starter-actuator</artifactId>
@@ -150,6 +166,17 @@
150166
<version>${folio-spring-support.version}</version>
151167
<scope>test</scope>
152168
</dependency>
169+
<dependency>
170+
<groupId>org.springframework.boot</groupId>
171+
<artifactId>spring-boot-starter-kafka-test</artifactId>
172+
<scope>test</scope>
173+
<exclusions>
174+
<exclusion>
175+
<groupId>org.springframework.boot</groupId>
176+
<artifactId>spring-boot-starter-logging</artifactId>
177+
</exclusion>
178+
</exclusions>
179+
</dependency>
153180
<dependency>
154181
<groupId>org.springframework.boot</groupId>
155182
<artifactId>spring-boot-starter-webmvc-test</artifactId>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.folio.notes.config;
2+
3+
import java.util.UUID;
4+
import org.folio.notes.domain.dto.Note;
5+
import org.folio.notes.domain.event.DomainEvent;
6+
import org.springframework.boot.kafka.autoconfigure.KafkaProperties;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
10+
import org.springframework.kafka.core.KafkaTemplate;
11+
import org.springframework.kafka.core.ProducerFactory;
12+
13+
/**
14+
* Kafka producer configuration for Note domain events.
15+
*
16+
* <p>Defines the {@link ProducerFactory} and {@link KafkaTemplate} used to publish {@link DomainEvent} envelopes
17+
* keyed by the Note id. Serializers are driven by the {@code spring.kafka.producer} configuration
18+
* ({@code UUIDSerializer} for the key, {@code JacksonJsonSerializer} for the value).</p>
19+
*/
20+
@Configuration
21+
public class KafkaConfiguration {
22+
23+
@Bean
24+
public ProducerFactory<UUID, DomainEvent<Note>> noteEventProducerFactory(KafkaProperties kafkaProperties) {
25+
return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
26+
}
27+
28+
@Bean
29+
public KafkaTemplate<UUID, DomainEvent<Note>> noteEventKafkaTemplate(
30+
ProducerFactory<UUID, DomainEvent<Note>> noteEventProducerFactory) {
31+
return new KafkaTemplate<>(noteEventProducerFactory);
32+
}
33+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.folio.notes.domain.event;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import java.util.UUID;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
/**
11+
* Envelope for a mod-notes domain event.
12+
*
13+
* @param <T> the payload type carried in the {@code old} and {@code new} fields (a full entity snapshot).
14+
*/
15+
@Data
16+
@NoArgsConstructor
17+
@JsonInclude(JsonInclude.Include.NON_NULL)
18+
public class DomainEvent<T> {
19+
20+
private UUID id;
21+
@JsonProperty("old")
22+
private T oldEntity;
23+
@JsonProperty("new")
24+
private T newEntity;
25+
private DomainEventType type;
26+
private String tenant;
27+
private String ts;
28+
29+
@JsonCreator
30+
public DomainEvent(@JsonProperty("id") UUID id,
31+
@JsonProperty("old") T oldEntity,
32+
@JsonProperty("new") T newEntity,
33+
@JsonProperty("type") DomainEventType type,
34+
@JsonProperty("tenant") String tenant,
35+
@JsonProperty("ts") String ts) {
36+
this.id = id;
37+
this.oldEntity = oldEntity;
38+
this.newEntity = newEntity;
39+
this.type = type;
40+
this.tenant = tenant;
41+
this.ts = ts;
42+
}
43+
44+
public static <T> DomainEvent<T> createEvent(UUID id, T newEntity, String tenant) {
45+
return new DomainEvent<>(id, null, newEntity, DomainEventType.CREATE, tenant, currentTs());
46+
}
47+
48+
public static <T> DomainEvent<T> updateEvent(UUID id, T oldEntity, T newEntity, String tenant) {
49+
return new DomainEvent<>(id, oldEntity, newEntity, DomainEventType.UPDATE, tenant, currentTs());
50+
}
51+
52+
public static <T> DomainEvent<T> deleteEvent(UUID id, T oldEntity, String tenant) {
53+
return new DomainEvent<>(id, oldEntity, null, DomainEventType.DELETE, tenant, currentTs());
54+
}
55+
56+
private static String currentTs() {
57+
return String.valueOf(System.currentTimeMillis());
58+
}
59+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.folio.notes.domain.event;
2+
3+
public enum DomainEventType {
4+
CREATE, UPDATE, DELETE
5+
}

0 commit comments

Comments
 (0)