Skip to content

Commit 557c740

Browse files
[MODNOTES-295] Address comments
1 parent 1c855b4 commit 557c740

15 files changed

Lines changed: 282 additions & 205 deletions

File tree

descriptors/ModuleDescriptor-template.json

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,40 @@
322322
"name": "KAFKA_PORT",
323323
"value": "9092"
324324
},
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+
},
325355
{
326356
"name": "ENV",
327-
"value": "folio"
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"
328359
}
329360
]
330361
}

docker/.env

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,5 @@ KAFKA_PORT=9093
2929
KAFKA_EXTERNAL_PORT=9092
3030
# Kafka UI (web) port -> http://localhost:8090
3131
KAFKA_UI_PORT=8090
32-
# Topics pre-created by kafka-topic-init (comma-separated tenants) and their partition count
33-
KAFKA_INIT_TENANTS=diku,test
34-
KAFKA_TOPIC_PARTITIONS=1
3532

3633

docker/README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ Configuration is managed via the `.env` file in this directory.
3939
| `KAFKA_PORT` | Broker port (in-cluster) | `9093` |
4040
| `KAFKA_EXTERNAL_PORT` | Broker port from host/IDE | `9092` |
4141
| `KAFKA_UI_PORT` | Kafka UI web port | `8090` |
42-
| `KAFKA_INIT_TENANTS` | Tenants to pre-create topics | `diku,test` |
43-
| `KAFKA_TOPIC_PARTITIONS` | Partitions per topic | `1` |
4442

4543
## 🚀 Services
4644

@@ -66,7 +64,7 @@ Configuration is managed via the `.env` file in this directory.
6664
- **Image**: `apache/kafka-native` (KRaft mode, no Zookeeper)
6765
- **Access (host/IDE)**: `localhost:9092` (configurable via `KAFKA_EXTERNAL_PORT`)
6866
- **Access (in-cluster)**: `kafka:9093`
69-
- **Topics**: `{ENV}.{tenant}.notes.note` — e.g. `folio.diku.notes.note`. Pre-created for the tenants in `KAFKA_INIT_TENANTS`; the broker also auto-creates the topic on first publish.
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.
7068

7169
### Kafka UI
7270
- **Purpose**: Web UI to browse topics and inspect published event messages

docker/app-docker-compose.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ services:
3030
- postgres
3131
- wiremock
3232
- kafka
33-
- kafka-topic-init
3433
deploy:
3534
replicas: ${MODULE_REPLICAS:-1}
3635
restart_policy:

docker/infra-docker-compose.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,6 @@ services:
7878
depends_on:
7979
- kafka
8080

81-
kafka-topic-init:
82-
image: apache/kafka:latest
83-
depends_on:
84-
- kafka
85-
environment:
86-
KAFKA_HOST: kafka
87-
KAFKA_PORT: 9093
88-
ENV: ${ENV}
89-
TENANTS: ${KAFKA_INIT_TENANTS}
90-
KAFKA_TOPIC_PARTITIONS: ${KAFKA_TOPIC_PARTITIONS}
91-
volumes:
92-
- ./kafka-init.sh:/usr/bin/kafka-init.sh
93-
entrypoint: ["bash", "/usr/bin/kafka-init.sh"]
94-
networks:
95-
- mod-notes-local
9681

9782
networks:
9883
mod-notes-local:

docker/kafka-init.sh

Lines changed: 0 additions & 34 deletions
This file was deleted.

pom.xml

Lines changed: 6 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>
Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,47 @@
11
package org.folio.notes.integration.kafka;
22

3+
import java.nio.charset.StandardCharsets;
4+
import java.util.ArrayList;
5+
import java.util.List;
36
import java.util.UUID;
47
import lombok.extern.slf4j.Slf4j;
8+
import org.apache.kafka.clients.producer.ProducerRecord;
9+
import org.apache.kafka.common.header.Header;
10+
import org.apache.kafka.common.header.internals.RecordHeader;
511
import org.folio.notes.domain.dto.Note;
612
import org.folio.notes.domain.event.DomainEvent;
713
import org.folio.spring.FolioExecutionContext;
8-
import org.springframework.beans.factory.annotation.Value;
14+
import org.folio.spring.tools.kafka.FolioKafkaProperties;
15+
import org.folio.spring.tools.kafka.KafkaUtils;
916
import org.springframework.kafka.core.KafkaTemplate;
1017
import org.springframework.stereotype.Component;
1118

1219
/**
1320
* Publishes Note {@link DomainEvent}s to the tenant-scoped Kafka topic.
1421
*
1522
* <p>The topic name follows the FOLIO convention {@code {env}.{tenant}.notes.note} (for example
16-
* {@code folio.diku.notes.note}). The tenant is resolved at publish time via {@link FolioExecutionContext}
17-
* and {@code env} defaults to {@code folio}.</p>
23+
* {@code folio.diku.notes.note}), resolved via {@link KafkaUtils#getTenantTopicName(String, String)} the same way the
24+
* topic is created on tenant enable. The tenant is resolved at publish time via {@link FolioExecutionContext}.</p>
25+
*
26+
* <p>Every message carries headers ({@code X-Okapi-Tenant}, {@code eventType}, {@code domain}) so consumers can
27+
* filter by tenant, event type or Note domain without deserializing the JSON payload.</p>
1828
*/
1929
@Slf4j
2030
@Component
2131
public class NoteEventProducer {
2232

33+
private static final String EVENT_TYPE_HEADER = "domain-event-type";
34+
2335
private final KafkaTemplate<UUID, DomainEvent<Note>> kafkaTemplate;
2436
private final FolioExecutionContext context;
25-
private final String env;
2637
private final String noteTopic;
2738

2839
public NoteEventProducer(KafkaTemplate<UUID, DomainEvent<Note>> kafkaTemplate,
2940
FolioExecutionContext context,
30-
@Value("${folio.environment:folio}") String env,
31-
@Value("${folio.kafka.topics.note:notes.note}") String noteTopic) {
41+
FolioKafkaProperties kafkaProperties) {
3242
this.kafkaTemplate = kafkaTemplate;
3343
this.context = context;
34-
this.env = env;
35-
this.noteTopic = noteTopic;
44+
this.noteTopic = kafkaProperties.getTopics().getFirst().getName();
3645
}
3746

3847
/**
@@ -42,17 +51,26 @@ public NoteEventProducer(KafkaTemplate<UUID, DomainEvent<Note>> kafkaTemplate,
4251
* @param event the domain event envelope to publish
4352
*/
4453
public void publish(UUID id, DomainEvent<Note> event) {
45-
var topic = topicName();
54+
var topic = KafkaUtils.getTenantTopicName(noteTopic, context.getTenantId());
4655
try {
4756
log.debug("publish:: sending Note event [topic: {}, id: {}, type: {}]", topic, id, event.getType());
48-
kafkaTemplate.send(topic, id, event);
57+
var producerRecord = new ProducerRecord<>(topic, null, id, event, buildHeaders(event));
58+
kafkaTemplate.send(producerRecord);
4959
log.info("publish:: Note event sent [topic: {}, id: {}, type: {}]", topic, id, event.getType());
5060
} catch (Exception e) {
5161
log.error("publish:: failed to send Note event [topic: {}, id: {}, type: {}]", topic, id, event.getType(), e);
5262
}
5363
}
5464

55-
private String topicName() {
56-
return String.format("%s.%s.%s", env, context.getTenantId(), noteTopic);
65+
private List<Header> buildHeaders(DomainEvent<Note> event) {
66+
var headers = new ArrayList<Header>();
67+
headers.add(header(EVENT_TYPE_HEADER, event.getType().name()));
68+
context.getAllHeaders().forEach((key, value) -> headers.add(header(key, value.iterator().next())));
69+
return headers;
5770
}
71+
72+
private Header header(String key, String value) {
73+
return new RecordHeader(key, value == null ? null : value.getBytes(StandardCharsets.UTF_8));
74+
}
75+
5876
}
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
package org.folio.notes.service.impl;
22

3+
import lombok.extern.slf4j.Slf4j;
34
import org.folio.notes.service.NoteTypesService;
45
import org.folio.spring.FolioExecutionContext;
56
import org.folio.spring.liquibase.FolioSpringLiquibase;
67
import org.folio.spring.service.TenantService;
8+
import org.folio.spring.tools.kafka.KafkaAdminService;
9+
import org.folio.tenant.domain.dto.TenantAttributes;
710
import org.springframework.context.annotation.Primary;
811
import org.springframework.jdbc.core.JdbcTemplate;
912
import org.springframework.stereotype.Service;
1013

1114
@Service
1215
@Primary
16+
@Slf4j
1317
public class NoteTenantService extends TenantService {
1418

1519
private final NoteTypesService noteTypesService;
20+
private final KafkaAdminService kafkaAdminService;
1621

1722
public NoteTenantService(JdbcTemplate jdbcTemplate,
1823
FolioExecutionContext context,
1924
FolioSpringLiquibase folioSpringLiquibase,
20-
NoteTypesService noteTypesService) {
25+
NoteTypesService noteTypesService,
26+
KafkaAdminService kafkaAdminService) {
2127
super(jdbcTemplate, context, folioSpringLiquibase);
2228
this.noteTypesService = noteTypesService;
29+
this.kafkaAdminService = kafkaAdminService;
2330
}
2431

2532
@Override
2633
public void loadReferenceData() {
2734
noteTypesService.populateDefaultType();
2835
}
36+
37+
@Override
38+
protected void afterTenantUpdate(TenantAttributes tenantAttributes) {
39+
super.afterTenantUpdate(tenantAttributes);
40+
kafkaAdminService.createTopics(context.getTenantId());
41+
}
2942
}

src/main/java/org/folio/notes/service/impl/NotesServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ public Note createNote(Note note) {
133133
NoteEntity entity = saveNote(note, dto -> initNewEntity(notesMapper.toEntity(dto)));
134134
log.info("createNote:: created note by title: {}, domain: {}, type: {}",
135135
note.getTitle(), note.getDomain(), note.getType());
136-
Note newSnapshot = notesMapper.toDto(entity);
137-
domainEventPublisherService.publishNoteCreatedEvent(newSnapshot);
138-
return newSnapshot;
136+
Note createdNote = notesMapper.toDto(entity);
137+
domainEventPublisherService.publishNoteCreatedEvent(createdNote);
138+
return createdNote;
139139
}
140140

141141
@Transactional

0 commit comments

Comments
 (0)