Skip to content

Commit 9b8a420

Browse files
Ajout de l'intégration Kafka
1 parent 372fb8e commit 9b8a420

9 files changed

Lines changed: 172 additions & 2 deletions

File tree

.env.exemple

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ EMAIL_USERNAME=
55
EMAIL_PASSWORD=
66
FROM_EMAIL=
77

8+
# Kafka configuration
9+
# Set to True to enable Kafka integration
10+
USE_KAFKA=
11+
# Comma-separated list of Kafka bootstrap servers
12+
KAFKA_BOOTSTRAP_SERVERS=
13+
# Topic to consume email messages from
14+
KAFKA_CONSUMER_TOPIC=email_topic
15+
# Key for Kafka messages
16+
KAFKA_MESSAGE_KEY=
817

918
# Example values:
1019
# EMAIL_DOMAIN=example.com

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,7 @@ data/mdm/data/*
155155
#Postgres
156156
data/postgres/
157157

158-
.env
158+
.env
159+
160+
docker-compose-kafka.yaml
161+
docker-compose-rabbitmq.yaml

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,51 @@ curl -X POST "http://localhost:9876/api/send-email" \
367367
echo "=== Fin du diagnostic ==="
368368
```
369369

370+
## 🔁 Intégration Kafka
371+
372+
Le service peut consommer des messages Kafka pour envoyer automatiquement des emails si l'intégration Kafka est activée via les variables d'environnement (`USE_KAFKA`, `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_CONSUMER_TOPIC`, `KAFKA_MESSAGE_KEY`).
373+
374+
Format JSON attendu pour le message (payload) avec la clé Kafka `email_topic` (vous pouvez modifier la clé via `KAFKA_MESSAGE_KEY`) :
375+
376+
```json
377+
{
378+
"receiver_email": "user@example.com",
379+
"email_object": "Sujet",
380+
"message_text": "Contenu du message"
381+
}
382+
```
383+
384+
Points importants pour une intégration en production ou en environnement conteneurisé :
385+
386+
- Le conteneur qui exécute le service doit être sur le même réseau Docker que le broker Kafka afin d'utiliser l'adresse interne du broker (ex. `broker:9093`). Sans réseau partagé, la résolution de nom et la connexion échoueront.
387+
- Pour les clients externes (depuis la machine hôte), utilisez l'endpoint exposé du broker (ex. `localhost:9092`) si les ports sont mappés.
388+
- Assurez-vous que `KAFKA_BOOTSTRAP_SERVERS` pointe vers l'endpoint correct selon le contexte (interne au réseau Docker vs externe).
389+
- Vérifiez que le topic configuré (`KAFKA_CONSUMER_TOPIC`) correspond au topic sur lequel sont publiés les messages.
390+
391+
Exemple minimal (attacher le service au réseau Docker interne du broker) :
392+
```yaml
393+
version: '3.8'
394+
395+
networks:
396+
local-kafka:
397+
external: true
398+
399+
services:
400+
mail_service:
401+
image: kalagaserge/mail_service
402+
container_name: mail_service
403+
ports:
404+
- "9876:9876"
405+
environment:
406+
- USE_KAFKA=True
407+
- KAFKA_BOOTSTRAP_SERVERS=broker:9093
408+
- KAFKA_CONSUMER_TOPIC=email_topic
409+
- KAFKA_MESSAGE_KEY=email_topic
410+
networks:
411+
- local-kafka
412+
restart: unless-stopped
413+
```
414+
370415
## 🚨 Sécurité et bonnes pratiques
371416
372417
### Recommandations de sécurité

app.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
from fastapi import FastAPI
2-
import uvicorn
32
from api import router as api_router
3+
from kafka_config import consume_messages
4+
from contextlib import asynccontextmanager
5+
6+
7+
@asynccontextmanager
8+
async def LifeSpan(app: FastAPI):
9+
print("-----------------APP STARTED----------------------")
10+
import asyncio
11+
12+
loop = asyncio.get_event_loop()
13+
loop.run_in_executor(None, consume_messages)
14+
yield
15+
print("-----------------APP ENDED----------------------")
416

517

618
app = FastAPI(
719
title="Mail Service",
820
version="1.0.0",
921
description="Service for sending emails",
1022
summary="This API provides endpoints for sending emails.",
23+
lifespan=LifeSpan,
1124
)
1225

1326
app.include_router(api_router, prefix="/api", tags=["Mail Service"])

docker-compose.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ services:
99
- .:/app
1010
env_file:
1111
- .env
12+
networks:
13+
- local-kafka
14+
15+
networks:
16+
local-kafka:
17+
name: local-kafka
18+
external: true

kafka_config.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from kafka import KafkaConsumer
2+
from utils import SETTINGS, send_email
3+
from datetime import datetime
4+
import json
5+
import time
6+
7+
8+
def make_consumer(topic, bootstrap_servers, group_id="mail_service-group"):
9+
"""Create and return a Kafka consumer with exponential backoff on connection failure."""
10+
backoff = 1
11+
while True:
12+
try:
13+
c = KafkaConsumer(
14+
topic,
15+
bootstrap_servers=bootstrap_servers,
16+
auto_offset_reset="earliest",
17+
enable_auto_commit=True,
18+
group_id=group_id,
19+
)
20+
print("Connected to Kafka")
21+
return c
22+
except Exception as e:
23+
print(f"Kafka connection failed: {e}. retrying in {backoff}s")
24+
time.sleep(backoff)
25+
backoff = min(backoff * 2, 30)
26+
27+
28+
def consume_messages():
29+
"""Consume messages from a specified Kafka topic."""
30+
if not SETTINGS.USE_KAFKA:
31+
print(
32+
"Kafka integration is disabled. Setting USE_KAFKA to True to enable it and restart the service."
33+
)
34+
return
35+
36+
print("Waiting for the producer to start...")
37+
38+
consumer = make_consumer(
39+
topic=SETTINGS.KAFKA_CONSUMER_TOPIC,
40+
bootstrap_servers=SETTINGS.KAFKA_BOOTSTRAP_SERVERS.split(","),
41+
)
42+
43+
print("Consumer is ready and listening for messages...")
44+
45+
try:
46+
while True:
47+
for message in consumer:
48+
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
49+
key = message.key.decode("utf-8") if message.key else "None"
50+
data = message.value.decode("utf-8")
51+
52+
if key == "email_topic":
53+
# Get email details from the message
54+
email_data = json.loads(data)
55+
receiver_email = email_data.get("receiver_email")
56+
email_object = email_data.get("email_object")
57+
message_text = email_data.get("message_text")
58+
59+
if not all([receiver_email, email_object, message_text]):
60+
print(f"[{ts}] Incomplete email data received: {email_data}")
61+
print("The correct format is:")
62+
print(
63+
'{"receiver_email": "email@example.com", "email_object": "Subject", "message_text": "Body"} or {"receiver_email": ["email@example.com"], "email_object": "Subject", "message_text": "Body"}'
64+
)
65+
print("Skipping this message.")
66+
continue
67+
68+
print(
69+
f"[{ts}] Sending email to {receiver_email} with subject '{email_object}'"
70+
)
71+
# Send the email
72+
send_email(
73+
receiver_email=receiver_email,
74+
message_text=message_text,
75+
email_object=email_object,
76+
)
77+
78+
except KeyboardInterrupt:
79+
pass
80+
finally:
81+
consumer.close()
82+
time.sleep(3)

rabbitmq.py

Whitespace-only changes.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ urllib3==2.5.0
3939
uvicorn==0.37.0
4040
watchfiles==1.1.0
4141
websockets==15.0.1
42+
kafka-python==2.2.15

utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@
88

99

1010
class Settings(BaseSettings):
11+
# App configurations
12+
PYTHONUNBUFFERED: int = 1
13+
14+
# Email configurations
1115
EMAIL_DOMAIN: str = "gmail.com"
1216
EMAIL_HOST: str
1317
EMAIL_PORT: int = 587
1418
EMAIL_USERNAME: str
1519
EMAIL_PASSWORD: str
1620
FROM_EMAIL: str | None = None
1721

22+
# Kafka configurations
23+
USE_KAFKA: bool = False
24+
KAFKA_BOOTSTRAP_SERVERS: str = None
25+
KAFKA_CONSUMER_TOPIC: str = None
26+
KAFKA_MESSAGE_KEY: str | None = None
27+
1828
class Config:
1929
env_file = ".env"
2030

0 commit comments

Comments
 (0)