21 KiB
AMQ Adapter Developer Guide
Overview
The AMQ Adapter library enables FastAPI-based Business Services to communicate with the CleverMicro platform via RabbitMQ. This guide demonstrates how to build your own service using the AMQ Adapter, following the pattern established by the OTDemo application.
Table of Contents
- Architecture Overview
- Prerequisites
- Project Structure
- Implementation Steps
- Configuration
- Creating Your Adapter
- Frontend Integration
- Backend Processing
- Testing and Deployment
- Advanced Features
Architecture Overview
The AMQ Adapter follows a layered architecture:
┌─────────────────────────────────────────────────────────────┐
│ Business Service │
├─────────────────────────────────────────────────────────────┤
│ FastAPI Endpoints │ Your Custom Adapter │ Backend │
│ │ │ Processing │
└─────────────────────┼───────────────────────┼───────────────┘
│ │
┌─────────────────────┼───────────────────────┼───────────────┐
│ AMQ Adapter Library │ │
├─────────────────────────────────────────────────────────────┤
│ CleverThisServiceAdapter │ AMQService │ Router │
│ (Base Class) │ │ │
└─────────────────────┼───────────────────────┼───────────────┘
│ │
┌─────────────────────┼───────────────────────┼───────────────┐
│ RabbitMQ │ │
│ Exchanges │ Queues │ Routing Keys │ │
└─────────────────────────────────────────────────────────────┘
Key Components
- Your Custom Adapter: Extends
CleverThisServiceAdapterto handle service-specific logic - AMQService: Manages RabbitMQ connections and message routing
- Router: Routes incoming AMQP messages to appropriate endpoints
- Configuration: Defines service settings, routing, and RabbitMQ connection details
Prerequisites
Before building your service, ensure you have:
- Python 3.8+ installed
- RabbitMQ server running (or access to CleverMicro platform)
- FastAPI knowledge
- AMQ Adapter library installed in your project
Dependencies
Add the following to your requirements.txt:
fastapi>=0.111.0
uvicorn[standard]>=0.29.0
python-multipart>=0.0.20
aio-pika>=9.0.0
pydantic>=2.0.0
# Add your AMQ Adapter library dependency
Project Structure
Follow this recommended structure for your Business Service:
your-business-service/
├── main.py # FastAPI application entry point
├── requirements.txt # Python dependencies
├── config/
│ ├── frontend.properties # Frontend service configuration
│ └── backend.properties # Backend service configuration
├── adapter/
│ └── your_adapter.py # Your custom AMQ adapter
├── backend/
│ └── backend_processor.py # Backend message processing
└── tests/
└── test_your_service.py
Implementation Steps
Step 1: Create Your Configuration Files
Create configuration files for both frontend and backend services.
config/frontend.properties:
# ====================================================================
# CleverMicro AMQ Adapter settings
# ====================================================================
[CleverMicro-AMQ]
cm.amq-adapter.service-name=your-service
cm.amq-adapter.generator-id=1
cm.amq-adapter.route-mapping=your-service:your-exchange:your-queue-1:api.cm.dev.cleverthis.localhost.test.python.frontend.#:5:0
cm.amq-adapter.require-authenticated-user=false
cm.amq-adapter.callbacks-enabled=True
# CleverMicro AMQ Adapter dispatcher settings
cm.dispatch.use-dlq=true
cm.dispatch.amq-host=localhost
cm.dispatch.amq-port=5672
cm.dispatch.amq-port-tls=5671
cm.dispatch.download-dir=/tmp/downloaded_files
# Backpressure settings
cm.backpressure.threshold=1
cm.backpressure.time-window=150
cm.backpressure.cpu-overload-duration=300
cm.backpressure.cpu-idle-duration=300
config/backend.properties:
# ====================================================================
# CleverMicro AMQ Adapter settings
# ====================================================================
[CleverMicro-AMQ]
cm.amq-adapter.service-name=your-service-backend
cm.amq-adapter.generator-id=1
cm.amq-adapter.route-mapping=your-service-backend:your-exchange:your-queue-2:your-service-backend.#:5:0
cm.amq-adapter.require-authenticated-user=false
cm.amq-adapter.callbacks-enabled=False
# CleverMicro AMQ Adapter dispatcher settings
cm.dispatch.use-dlq=true
cm.dispatch.amq-host=localhost
cm.dispatch.amq-port=5672
cm.dispatch.amq-port-tls=5671
cm.dispatch.download-dir=/tmp/downloaded_files
# Backpressure settings
cm.backpressure.threshold=1
cm.backpressure.time-window=150
cm.backpressure.cpu-overload-duration=300
cm.backpressure.cpu-idle-duration=300
Step 2: Create Your Custom Adapter
Create your custom adapter by extending CleverThisServiceAdapter:
"""
Your Business Service AMQP Adapter for handling API requests via AMQP.
This adapter is specific to your business service and handles service-specific logic.
"""
from threading import Thread
from typing import Any, List
from fastapi.routing import APIRoute
from amqp.adapter.cleverthis_service_adapter import CleverThisServiceAdapter
from amqp.config.amq_configuration import AMQConfiguration
from amqp.service.amq_service import AMQService
class YourServiceAmqpAdapter(CleverThisServiceAdapter):
"""
Your Business Service AMQP Adapter. Implements service-specific logic that overrides
common logic defined by the AMQ Adapter library.
Key responsibilities:
1. Handle authentication and user management
2. Process incoming AMQP messages
3. Route messages to appropriate endpoints
4. Manage service-specific business logic
"""
def __init__(self, amq_configuration: AMQConfiguration, routes: List[APIRoute]):
super().__init__(amq_configuration, routes=routes, session=None)
self.amq_service: AMQService = AMQService(amq_configuration, self)
self.thread = Thread(target=self.amq_service.run)
self.thread.start()
async def get_current_user(self, token: str) -> str:
"""
Override the default get_current_user method to handle authentication.
In this example, we simply return the token, but you should implement
proper JWT token validation and user extraction.
:param token: JWT token from the AMQP message
:return: User identifier or user object
"""
# TODO: Implement proper JWT token validation
# Example: return await your_auth_service.validate_token(token)
return token
def get_endpoint(self, endpoint: Any) -> Any:
"""
Override to provide custom endpoint selection logic.
For example, you might want to use JSON wrapper endpoints
or implement custom routing logic.
:param endpoint: The original endpoint callable
:return: The endpoint callable to use (original or modified)
"""
return endpoint
Step 3: Create Your FastAPI Application
Create your main FastAPI application with endpoints:
"""
Your Business Service FastAPI Application
"""
import os
from fastapi import FastAPI, File, Form, UploadFile
from fastapi.routing import APIRoute
from amqp.config.amq_configuration import AMQConfiguration
from adapter.your_adapter import YourServiceAmqpAdapter
# Create FastAPI application
app = FastAPI(
title="Your Business Service",
description="A FastAPI application using AMQ Adapter for CleverMicro communication",
version="1.0.0"
)
# Initialize your AMQ adapter
# This should be done after all routes are defined
your_adapter = None
@app.post("/api/v1/items")
async def create_item(
name: str = Form(...),
description: str = Form(...),
file: UploadFile = File(...)
):
"""
Create a new item with file upload.
This endpoint will be available via both REST and AMQP.
"""
# Your business logic here
result = {
"message": "Item created successfully",
"name": name,
"description": description,
"filename": file.filename
}
# Example: Send a command to backend via AMQP
if your_adapter:
your_adapter.amq_service.command(
"your-service-backend",
"process_item",
item_name=name,
item_description=description
)
return result
@app.get("/api/v1/items")
async def get_items():
"""
Retrieve all items.
This endpoint will be available via both REST and AMQP.
"""
# Your business logic here
items = [
{"id": "1", "name": "Item 1", "description": "Description 1"},
{"id": "2", "name": "Item 2", "description": "Description 2"}
]
return {"items": items}
@app.get("/health")
async def health_check():
"""
Health check endpoint.
"""
return {"status": "healthy", "service": "your-business-service"}
# Initialize the AMQ adapter after all routes are defined
def initialize_amq_adapter():
global your_adapter
config = AMQConfiguration("config/frontend.properties")
your_adapter = YourServiceAmqpAdapter(config, app.routes)
# Initialize adapter when the application starts
@app.on_event("startup")
async def startup_event():
initialize_amq_adapter()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Step 4: Create Backend Processing
Create a backend processor for handling AMQP messages:
"""
Backend processor for your business service.
Handles AMQP messages and performs background processing.
"""
import asyncio
from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import AMQRoute, DataMessage
from adapter.your_adapter import YourServiceAmqpAdapter
def process_item_static(item_name: str, item_description: str) -> str:
"""
Static function to process an item.
"""
print(f"STATIC Processing item: {item_name} - {item_description}")
return f"STATIC PROCESSED: {item_name}"
class BackendProcessor:
"""
Backend processor class for handling business logic.
"""
def process_item(self, item_name: str, item_description: str) -> str:
"""
Process an item using instance method.
"""
print(f"CLASS Processing item: {item_name} - {item_description}")
return f"CLASS PROCESSED: {item_name}"
async def processing_loop():
"""
Main processing loop for handling AMQP messages.
"""
# Wait for AMQ adapter to initialize
while not otadapter.amq_service.future.done():
await asyncio.sleep(0.1)
# Find the route for backend processing
route: AMQRoute = otadapter.amq_service.router.find_route_by_service("your-service-backend")
generator = otadapter.amq_service.data_message_generator(route)
print(f"Data message generator created: {generator}")
# Start the generator
message = await generator.asend(None)
# Process messages
while True:
print(f"Received message: {message}")
if message is None:
break
data_message: DataMessage = message
# Extract message attributes
print(f"Message attributes: {data_message.method()} {data_message.domain()} {data_message.path()}")
# Process based on message path
if data_message.path() == "process_item_static":
item_name = data_message._base64body.decode("utf-8")
process_item_static(item_name, "Static processing")
elif data_message.path() == "process_item":
item_name = data_message._base64body.decode("utf-8")
processor = BackendProcessor()
processor.process_item(item_name, "Class processing")
# Get next message
message = await generator.asend(True)
if __name__ == "__main__":
# Initialize backend adapter
otadapter = YourServiceAmqpAdapter(AMQConfiguration("config/backend.properties"), [])
# Run the processing loop in the AMQ service event loop
asyncio.run_coroutine_threadsafe(processing_loop(), otadapter.amq_service.loop)
# Keep the program running
otadapter.thread.join()
Configuration
Configuration Parameters
The configuration file contains several key sections:
AMQ Adapter Settings (cm.amq-adapter.*)
service-name: Unique identifier for your servicegenerator-id: Unique ID for message generation (usually 1)route-mapping: RabbitMQ routing configurationrequire-authenticated-user: Whether authentication is requiredcallbacks-enabled: Whether to enable callback functionality
Dispatch Settings (cm.dispatch.*)
amq-host: RabbitMQ host addressamq-port: RabbitMQ port (5672 for non-TLS, 5671 for TLS)use-dlq: Whether to use Dead Letter Queuedownload-dir: Directory for downloaded files
Backpressure Settings (cm.backpressure.*)
threshold: Backpressure threshold valuetime-window: Time window for backpressure calculationcpu-overload-duration: Duration for CPU overload detectioncpu-idle-duration: Duration for CPU idle detection
Route Mapping Format
The route mapping follows this format:
service-name:exchange:queue:routing-key:prefetch:durable
Example:
your-service:your-exchange:your-queue-1:api.cm.dev.cleverthis.localhost.test.python.frontend.#:5:0
Frontend Integration
Key Integration Points
- Adapter Initialization: Initialize your adapter after defining all FastAPI routes
- Message Handling: The adapter automatically routes AMQP messages to your endpoints
- Command Sending: Use
amq_service.command()to send commands to backend services
Example Integration
# In your FastAPI application
@app.post("/api/v1/process")
async def process_data(data: dict):
# Your business logic
result = {"status": "processing", "data": data}
# Send command to backend
your_adapter.amq_service.command(
"your-service-backend",
"process_data",
**data
)
return result
Backend Processing
Message Processing Pattern
- Initialize Adapter: Create adapter with backend configuration
- Wait for Connection: Wait for RabbitMQ connection to be established
- Find Route: Locate the route for your backend service
- Create Generator: Create message generator for the route
- Process Messages: Continuously process incoming messages
Example Processing
async def process_messages():
# Wait for connection
while not adapter.amq_service.future.done():
await asyncio.sleep(0.1)
# Get route and generator
route = adapter.amq_service.router.find_route_by_service("your-service-backend")
generator = adapter.amq_service.data_message_generator(route)
# Process messages
message = await generator.asend(None)
while message:
# Process the message
await handle_message(message)
message = await generator.asend(True)
Testing and Deployment
Local Testing
- Start RabbitMQ: Ensure RabbitMQ is running locally
- Run Frontend: Start your FastAPI application
- Run Backend: Start your backend processor
- Test Endpoints: Use tools like curl or Postman to test REST endpoints
- Monitor AMQP: Check RabbitMQ management interface for message flow
Docker Deployment
Create a Dockerfile for your service:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Docker Compose
Create a docker-compose.yml for local development:
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
your-service-frontend:
build: .
ports:
- "8000:8000"
environment:
- RABBITMQ_HOST=rabbitmq
depends_on:
- rabbitmq
your-service-backend:
build: .
command: ["python", "backend/backend_processor.py"]
environment:
- RABBITMQ_HOST=rabbitmq
depends_on:
- rabbitmq
Advanced Features
Authentication
Implement proper JWT token validation in your adapter:
async def get_current_user(self, token: str) -> User:
"""
Validate JWT token and extract user information.
"""
try:
# Decode and validate JWT token
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
user_id = payload.get("sub")
# Fetch user from database
user = await get_user_by_id(user_id)
return user
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
Error Handling
Implement comprehensive error handling:
async def handle_message(self, message: DataMessage):
"""
Handle incoming messages with proper error handling.
"""
try:
# Process message
result = await self.process_message(message)
return result
except ValidationError as e:
logging.error(f"Validation error: {e}")
# Handle validation errors
except ProcessingError as e:
logging.error(f"Processing error: {e}")
# Handle processing errors
except Exception as e:
logging.error(f"Unexpected error: {e}")
# Handle unexpected errors
Monitoring and Logging
Implement proper logging and monitoring:
import logging
from opentelemetry import trace, metrics
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configure tracing
tracer = trace.get_tracer(__name__)
# Configure metrics
meter = metrics.get_meter(__name__)
message_counter = meter.create_counter("messages_processed_total")
async def process_message(self, message: DataMessage):
"""
Process message with tracing and metrics.
"""
with tracer.start_as_current_span("process_message"):
logger.info(f"Processing message: {message.id()}")
# Your processing logic here
message_counter.add(1, {"service": "your-service"})
logger.info(f"Message processed successfully: {message.id()}")
Troubleshooting
Common Issues
- Connection Errors: Ensure RabbitMQ is running and accessible
- Route Not Found: Check route mapping configuration
- Message Not Processed: Verify endpoint method signatures match expected parameters
- Authentication Failures: Check JWT token validation logic
Debugging Tips
- Enable Debug Logging: Set logging level to DEBUG
- Check RabbitMQ Management: Use RabbitMQ management interface to monitor queues
- Trace Message Flow: Use OpenTelemetry tracing to follow message flow
- Validate Configuration: Ensure all configuration parameters are correct
Conclusion
This guide provides a comprehensive overview of building a Business Service using the AMQ Adapter library. By following the patterns established in the OTDemo application, you can create robust, scalable services that integrate seamlessly with the CleverMicro platform.
For more detailed information, refer to the AMQ Adapter library documentation and the OTDemo source code examples.