Refactor all main.py
This commit is contained in:
281
HEALTH_CHECKS.md
Normal file
281
HEALTH_CHECKS.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# Unified Health Check System
|
||||||
|
|
||||||
|
This document describes the unified health check system implemented across all microservices in the bakery-ia platform.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The unified health check system provides standardized health monitoring endpoints across all services, with comprehensive database verification, Kubernetes integration, and detailed health reporting.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Standardized Endpoints**: All services now provide the same health check endpoints
|
||||||
|
- **Database Verification**: Comprehensive database health checks including table existence verification
|
||||||
|
- **Kubernetes Integration**: Proper separation of liveness and readiness probes
|
||||||
|
- **Detailed Reporting**: Rich health status information for debugging and monitoring
|
||||||
|
- **App State Integration**: Health checks automatically detect service ready state
|
||||||
|
|
||||||
|
## Health Check Endpoints
|
||||||
|
|
||||||
|
### `/health` - Basic Health Check
|
||||||
|
- **Purpose**: Basic service health status
|
||||||
|
- **Use Case**: General health monitoring, API gateways
|
||||||
|
- **Response**: Service name, version, status, and timestamp
|
||||||
|
- **Status Codes**: 200 (healthy/starting)
|
||||||
|
|
||||||
|
### `/health/ready` - Kubernetes Readiness Probe
|
||||||
|
- **Purpose**: Indicates if service is ready to receive traffic
|
||||||
|
- **Use Case**: Kubernetes readiness probe, load balancer health checks
|
||||||
|
- **Checks**: Application state, database connectivity, table verification, custom checks
|
||||||
|
- **Status Codes**: 200 (ready), 503 (not ready)
|
||||||
|
|
||||||
|
### `/health/live` - Kubernetes Liveness Probe
|
||||||
|
- **Purpose**: Indicates if service is alive and should not be restarted
|
||||||
|
- **Use Case**: Kubernetes liveness probe
|
||||||
|
- **Response**: Simple alive status
|
||||||
|
- **Status Codes**: 200 (alive)
|
||||||
|
|
||||||
|
### `/health/database` - Detailed Database Health
|
||||||
|
- **Purpose**: Comprehensive database health information for debugging
|
||||||
|
- **Use Case**: Database monitoring, troubleshooting
|
||||||
|
- **Checks**: Connectivity, table existence, connection pool status, response times
|
||||||
|
- **Status Codes**: 200 (healthy), 503 (unhealthy)
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Services Updated
|
||||||
|
|
||||||
|
The following services have been updated to use the unified health check system:
|
||||||
|
|
||||||
|
1. **Training Service** (`training-service`)
|
||||||
|
- Full implementation with database manager integration
|
||||||
|
- Table verification for ML training tables
|
||||||
|
- Expected tables: `model_training_logs`, `trained_models`, `model_performance_metrics`, `training_job_queue`, `model_artifacts`
|
||||||
|
|
||||||
|
2. **Orders Service** (`orders-service`)
|
||||||
|
- Legacy database integration with custom health checks
|
||||||
|
- Expected tables: `customers`, `customer_contacts`, `customer_orders`, `order_items`, `order_status_history`, `procurement_plans`, `procurement_requirements`
|
||||||
|
|
||||||
|
3. **Inventory Service** (`inventory-service`)
|
||||||
|
- Full database manager integration
|
||||||
|
- Food safety and inventory table verification
|
||||||
|
- Expected tables: `ingredients`, `stock`, `stock_movements`, `product_transformations`, `stock_alerts`, `food_safety_compliance`, `temperature_logs`, `food_safety_alerts`
|
||||||
|
|
||||||
|
### Code Integration
|
||||||
|
|
||||||
|
#### Basic Setup
|
||||||
|
```python
|
||||||
|
from shared.monitoring.health_checks import setup_fastapi_health_checks
|
||||||
|
|
||||||
|
# Setup unified health checks
|
||||||
|
health_manager = setup_fastapi_health_checks(
|
||||||
|
app=app,
|
||||||
|
service_name="my-service",
|
||||||
|
version="1.0.0",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=['table1', 'table2'],
|
||||||
|
custom_checks={"custom_check": custom_check_function}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With Custom Checks
|
||||||
|
```python
|
||||||
|
async def custom_health_check():
|
||||||
|
"""Custom health check function"""
|
||||||
|
return await some_service_check()
|
||||||
|
|
||||||
|
health_manager = setup_fastapi_health_checks(
|
||||||
|
app=app,
|
||||||
|
service_name="my-service",
|
||||||
|
version="1.0.0",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=['table1', 'table2'],
|
||||||
|
custom_checks={"external_service": custom_health_check}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Service Ready State
|
||||||
|
```python
|
||||||
|
# In your lifespan function
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Startup logic
|
||||||
|
await initialize_service()
|
||||||
|
|
||||||
|
# Mark service as ready
|
||||||
|
app.state.ready = True
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Shutdown logic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kubernetes Configuration
|
||||||
|
|
||||||
|
### Updated Probe Configuration
|
||||||
|
|
||||||
|
The microservice template and specific service configurations have been updated to use the new endpoints:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/live
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 3
|
||||||
|
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/ready
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
timeoutSeconds: 3
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Changes from Previous Configuration
|
||||||
|
|
||||||
|
1. **Liveness Probe**: Now uses `/health/live` instead of `/health`
|
||||||
|
2. **Readiness Probe**: Now uses `/health/ready` instead of `/health`
|
||||||
|
3. **Improved Timing**: Adjusted timeouts and failure thresholds for better reliability
|
||||||
|
4. **Separate Concerns**: Liveness and readiness are now properly separated
|
||||||
|
|
||||||
|
## Health Check Response Examples
|
||||||
|
|
||||||
|
### Basic Health Check Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "training-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"timestamp": "2025-01-27T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Readiness Check Response (Ready)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ready",
|
||||||
|
"checks": {
|
||||||
|
"application": true,
|
||||||
|
"database_connectivity": true,
|
||||||
|
"database_tables": true
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"status": "healthy",
|
||||||
|
"tables_verified": ["model_training_logs", "trained_models"],
|
||||||
|
"missing_tables": [],
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Health Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"connectivity": true,
|
||||||
|
"tables_exist": true,
|
||||||
|
"tables_verified": ["model_training_logs", "trained_models"],
|
||||||
|
"missing_tables": [],
|
||||||
|
"errors": [],
|
||||||
|
"connection_info": {
|
||||||
|
"service_name": "training-service",
|
||||||
|
"database_type": "postgresql",
|
||||||
|
"pool_size": 20,
|
||||||
|
"current_checked_out": 2
|
||||||
|
},
|
||||||
|
"response_time_ms": 15.23
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
```bash
|
||||||
|
# Test all endpoints for a running service
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/health/ready
|
||||||
|
curl http://localhost:8000/health/live
|
||||||
|
curl http://localhost:8000/health/database
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
Use the provided test script:
|
||||||
|
```bash
|
||||||
|
python test_unified_health_checks.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### For Existing Services
|
||||||
|
|
||||||
|
1. **Add Health Check Import**:
|
||||||
|
```python
|
||||||
|
from shared.monitoring.health_checks import setup_fastapi_health_checks
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add Database Manager Import** (if using shared database):
|
||||||
|
```python
|
||||||
|
from app.core.database import database_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Setup Health Checks** (after app creation, before router inclusion):
|
||||||
|
```python
|
||||||
|
health_manager = setup_fastapi_health_checks(
|
||||||
|
app=app,
|
||||||
|
service_name="your-service-name",
|
||||||
|
version=settings.VERSION,
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=["table1", "table2"]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Remove Old Health Endpoints**:
|
||||||
|
Remove any existing `@app.get("/health")` endpoints
|
||||||
|
|
||||||
|
5. **Add Ready State Management**:
|
||||||
|
```python
|
||||||
|
# In lifespan function after successful startup
|
||||||
|
app.state.ready = True
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Update Kubernetes Configuration**:
|
||||||
|
Update deployment YAML to use new probe endpoints
|
||||||
|
|
||||||
|
### For Services Using Legacy Database
|
||||||
|
|
||||||
|
If your service doesn't use the shared database manager:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def legacy_database_check():
|
||||||
|
"""Custom health check for legacy database"""
|
||||||
|
return await your_db_health_check()
|
||||||
|
|
||||||
|
health_manager = setup_fastapi_health_checks(
|
||||||
|
app=app,
|
||||||
|
service_name="your-service",
|
||||||
|
version=settings.VERSION,
|
||||||
|
database_manager=None,
|
||||||
|
expected_tables=None,
|
||||||
|
custom_checks={"legacy_database": legacy_database_check}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Consistency**: All services now provide the same health check interface
|
||||||
|
2. **Better Kubernetes Integration**: Proper separation of liveness and readiness concerns
|
||||||
|
3. **Enhanced Debugging**: Detailed health information for troubleshooting
|
||||||
|
4. **Database Verification**: Comprehensive database health checks including table verification
|
||||||
|
5. **Monitoring Ready**: Rich health status information for monitoring systems
|
||||||
|
6. **Maintainability**: Centralized health check logic reduces code duplication
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Metrics Integration**: Add Prometheus metrics for health check performance
|
||||||
|
2. **Circuit Breaker**: Implement circuit breaker pattern for external service checks
|
||||||
|
3. **Health Check Dependencies**: Add dependency health checks between services
|
||||||
|
4. **Performance Thresholds**: Add configurable performance thresholds for health checks
|
||||||
|
5. **Health Check Scheduling**: Add scheduled background health checks
|
||||||
@@ -15,3 +15,5 @@ pytz==2023.3
|
|||||||
python-logstash==0.4.8
|
python-logstash==0.4.8
|
||||||
structlog==23.2.0
|
structlog==23.2.0
|
||||||
websockets==12.0
|
websockets==12.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
asyncpg==0.29.0
|
||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -123,12 +123,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ spec:
|
|||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -65,12 +65,12 @@ spec:
|
|||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 15
|
||||||
timeoutSeconds: 1
|
timeoutSeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ spec:
|
|||||||
cpu: "2000m"
|
cpu: "2000m"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/live
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 60
|
initialDelaySeconds: 60
|
||||||
timeoutSeconds: 30
|
timeoutSeconds: 30
|
||||||
@@ -73,12 +73,12 @@ spec:
|
|||||||
failureThreshold: 5
|
failureThreshold: 5
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8000
|
port: 8000
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 30
|
||||||
timeoutSeconds: 10
|
timeoutSeconds: 15
|
||||||
periodSeconds: 15
|
periodSeconds: 15
|
||||||
failureThreshold: 3
|
failureThreshold: 5
|
||||||
volumes:
|
volumes:
|
||||||
- name: training-state
|
- name: training-state
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
|
|||||||
@@ -1,180 +1,120 @@
|
|||||||
"""
|
"""
|
||||||
Authentication Service Main Application - Fixed middleware issue
|
Authentication Service Main Application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
from fastapi import FastAPI
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import engine, create_tables
|
from app.core.database import database_manager
|
||||||
from app.api import auth, users, onboarding
|
from app.api import auth, users, onboarding
|
||||||
from app.services.messaging import setup_messaging, cleanup_messaging
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
||||||
from shared.monitoring import setup_logging, HealthChecker
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
|
||||||
|
|
||||||
# Setup logging first
|
|
||||||
setup_logging("auth-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Global variables for lifespan access
|
class AuthService(StandardFastAPIService):
|
||||||
metrics_collector = None
|
"""Authentication Service with standardized setup"""
|
||||||
health_checker = None
|
|
||||||
|
|
||||||
# Create FastAPI app FIRST
|
def __init__(self):
|
||||||
app = FastAPI(
|
# Define expected database tables for health checks
|
||||||
title="Authentication Service",
|
auth_expected_tables = [
|
||||||
|
'users', 'refresh_tokens', 'user_onboarding_progress',
|
||||||
|
'user_onboarding_summary', 'login_attempts'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Define custom metrics for auth service
|
||||||
|
auth_custom_metrics = {
|
||||||
|
"registration_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total user registrations by status",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"login_success_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total successful user logins"
|
||||||
|
},
|
||||||
|
"login_failure_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total failed user logins by reason",
|
||||||
|
"labels": ["reason"]
|
||||||
|
},
|
||||||
|
"token_refresh_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total token refreshes by status",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"token_verify_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total token verifications by status",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"logout_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total user logouts by status",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"registration_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Registration request duration"
|
||||||
|
},
|
||||||
|
"login_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Login request duration"
|
||||||
|
},
|
||||||
|
"token_refresh_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Token refresh duration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
service_name="auth-service",
|
||||||
|
app_name="Authentication Service",
|
||||||
description="Handles user authentication and authorization for bakery forecasting platform",
|
description="Handles user authentication and authorization for bakery forecasting platform",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=auth_expected_tables,
|
||||||
|
enable_messaging=True,
|
||||||
|
custom_metrics=auth_custom_metrics
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _setup_messaging(self):
|
||||||
|
"""Setup messaging for auth service"""
|
||||||
|
await setup_messaging()
|
||||||
|
self.logger.info("Messaging setup complete")
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging for auth service"""
|
||||||
|
await cleanup_messaging()
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for auth service"""
|
||||||
|
self.logger.info("Authentication Service shutdown complete")
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return auth-specific features"""
|
||||||
|
return [
|
||||||
|
"user_authentication",
|
||||||
|
"token_management",
|
||||||
|
"user_onboarding",
|
||||||
|
"role_based_access",
|
||||||
|
"messaging_integration"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = AuthService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app(
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc"
|
redoc_url="/redoc"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup metrics BEFORE any middleware and BEFORE lifespan
|
# Setup standard endpoints
|
||||||
# This must happen before the app starts
|
service.setup_standard_endpoints()
|
||||||
metrics_collector = setup_metrics_early(app, "auth-service")
|
|
||||||
|
|
||||||
@asynccontextmanager
|
# Include routers with specific configurations
|
||||||
async def lifespan(app: FastAPI):
|
service.add_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
||||||
"""Application lifespan events - NO MIDDLEWARE ADDED HERE"""
|
service.add_router(users.router, prefix="/api/v1/users", tags=["users"])
|
||||||
global health_checker
|
service.add_router(onboarding.router, prefix="/api/v1/users", tags=["onboarding"])
|
||||||
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Authentication Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create database tables
|
|
||||||
await create_tables()
|
|
||||||
logger.info("Database tables created")
|
|
||||||
|
|
||||||
# Setup messaging
|
|
||||||
await setup_messaging()
|
|
||||||
logger.info("Messaging setup complete")
|
|
||||||
|
|
||||||
# Register custom metrics (metrics_collector already exists)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"registration_total",
|
|
||||||
"Total user registrations by status",
|
|
||||||
labels=["status"] # Add this line
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"login_success_total",
|
|
||||||
"Total successful user logins"
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"login_failure_total",
|
|
||||||
"Total failed user logins by reason",
|
|
||||||
labels=["reason"] # Add this line, based on auth.py usage
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"token_refresh_total",
|
|
||||||
"Total token refreshes by status",
|
|
||||||
labels=["status"] # Add this line
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"token_verify_total",
|
|
||||||
"Total token verifications by status",
|
|
||||||
labels=["status"] # Add this line
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter(
|
|
||||||
"logout_total",
|
|
||||||
"Total user logouts by status",
|
|
||||||
labels=["status"] # Add this line
|
|
||||||
)
|
|
||||||
metrics_collector.register_counter("errors_total", "Total errors", labels=["type"]) # Add this line
|
|
||||||
metrics_collector.register_histogram("registration_duration_seconds", "Registration request duration")
|
|
||||||
metrics_collector.register_histogram("login_duration_seconds", "Login request duration")
|
|
||||||
metrics_collector.register_histogram("token_refresh_duration_seconds", "Token refresh duration")
|
|
||||||
|
|
||||||
# Setup health checker
|
|
||||||
health_checker = HealthChecker("auth-service")
|
|
||||||
|
|
||||||
# Add database health check
|
|
||||||
async def check_database():
|
|
||||||
try:
|
|
||||||
from app.core.database import get_db
|
|
||||||
from sqlalchemy import text
|
|
||||||
async for db in get_db():
|
|
||||||
await db.execute(text("SELECT 1"))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return f"Database error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("database", check_database, timeout=5.0, critical=True)
|
|
||||||
|
|
||||||
# Add messaging health check
|
|
||||||
def check_messaging():
|
|
||||||
try:
|
|
||||||
# Add your messaging health check logic here
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return f"Messaging error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("messaging", check_messaging, timeout=3.0, critical=False)
|
|
||||||
|
|
||||||
# Store health checker in app state
|
|
||||||
app.state.health_checker = health_checker
|
|
||||||
|
|
||||||
logger.info("Authentication Service started successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to start Authentication Service: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Authentication Service...")
|
|
||||||
try:
|
|
||||||
await cleanup_messaging()
|
|
||||||
await engine.dispose()
|
|
||||||
logger.info("Authentication Service shutdown complete")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error during shutdown: {e}")
|
|
||||||
|
|
||||||
# Set lifespan AFTER metrics setup
|
|
||||||
app.router.lifespan_context = lifespan
|
|
||||||
|
|
||||||
# CORS middleware (added after metrics setup)
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # Configure properly for production
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["authentication"])
|
|
||||||
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
|
||||||
app.include_router(onboarding.router, prefix="/api/v1/users", tags=["onboarding"])
|
|
||||||
|
|
||||||
# Health check endpoint with comprehensive checks
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Comprehensive health check endpoint"""
|
|
||||||
if health_checker:
|
|
||||||
return await health_checker.check_health()
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"service": "auth-service",
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Global exception handler with metrics"""
|
|
||||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
||||||
|
|
||||||
# Record error metric if available
|
|
||||||
if metrics_collector:
|
|
||||||
metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={"detail": "Internal server error"}
|
|
||||||
)
|
|
||||||
314
services/external/app/main.py
vendored
314
services/external/app/main.py
vendored
@@ -3,184 +3,158 @@
|
|||||||
External Service Main Application
|
External Service Main Application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
from fastapi import FastAPI
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db, close_db
|
from app.core.database import database_manager
|
||||||
from shared.monitoring import setup_logging, HealthChecker
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
from shared.service_base import StandardFastAPIService
|
||||||
|
|
||||||
# Setup logging first
|
|
||||||
setup_logging("external-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Global variables for lifespan access
|
|
||||||
metrics_collector = None
|
|
||||||
health_checker = None
|
|
||||||
|
|
||||||
# Create FastAPI app FIRST
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bakery External Data Service",
|
|
||||||
description="External data collection service for weather, traffic, and events data",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup metrics BEFORE any middleware and BEFORE lifespan
|
|
||||||
metrics_collector = setup_metrics_early(app, "external-service")
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Application lifespan events"""
|
|
||||||
global health_checker
|
|
||||||
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting External Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize database
|
|
||||||
await init_db()
|
|
||||||
logger.info("Database initialized")
|
|
||||||
|
|
||||||
# Register custom metrics
|
|
||||||
metrics_collector.register_counter("weather_api_calls_total", "Total weather API calls")
|
|
||||||
metrics_collector.register_counter("weather_api_success_total", "Successful weather API calls")
|
|
||||||
metrics_collector.register_counter("weather_api_failures_total", "Failed weather API calls")
|
|
||||||
|
|
||||||
metrics_collector.register_counter("traffic_api_calls_total", "Total traffic API calls")
|
|
||||||
metrics_collector.register_counter("traffic_api_success_total", "Successful traffic API calls")
|
|
||||||
metrics_collector.register_counter("traffic_api_failures_total", "Failed traffic API calls")
|
|
||||||
|
|
||||||
metrics_collector.register_counter("data_collection_jobs_total", "Data collection jobs")
|
|
||||||
metrics_collector.register_counter("data_records_stored_total", "Data records stored")
|
|
||||||
metrics_collector.register_counter("data_quality_issues_total", "Data quality issues detected")
|
|
||||||
|
|
||||||
metrics_collector.register_histogram("weather_api_duration_seconds", "Weather API call duration")
|
|
||||||
metrics_collector.register_histogram("traffic_api_duration_seconds", "Traffic API call duration")
|
|
||||||
metrics_collector.register_histogram("data_collection_duration_seconds", "Data collection job duration")
|
|
||||||
metrics_collector.register_histogram("data_processing_duration_seconds", "Data processing duration")
|
|
||||||
|
|
||||||
# Setup health checker
|
|
||||||
health_checker = HealthChecker("external-service")
|
|
||||||
|
|
||||||
# Add database health check
|
|
||||||
async def check_database():
|
|
||||||
try:
|
|
||||||
from app.core.database import get_db
|
|
||||||
from sqlalchemy import text
|
|
||||||
async for db in get_db():
|
|
||||||
await db.execute(text("SELECT 1"))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return f"Database error: {e}"
|
|
||||||
|
|
||||||
# Add external API health checks
|
|
||||||
async def check_weather_api():
|
|
||||||
try:
|
|
||||||
# Simple connectivity check
|
|
||||||
if settings.AEMET_API_KEY:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return "AEMET API key not configured"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Weather API error: {e}"
|
|
||||||
|
|
||||||
async def check_traffic_api():
|
|
||||||
try:
|
|
||||||
# Simple connectivity check
|
|
||||||
if settings.MADRID_OPENDATA_API_KEY:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return "Madrid Open Data API key not configured"
|
|
||||||
except Exception as e:
|
|
||||||
return f"Traffic API error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("database", check_database, timeout=5.0, critical=True)
|
|
||||||
health_checker.add_check("weather_api", check_weather_api, timeout=10.0, critical=False)
|
|
||||||
health_checker.add_check("traffic_api", check_traffic_api, timeout=10.0, critical=False)
|
|
||||||
|
|
||||||
# Store health checker in app state
|
|
||||||
app.state.health_checker = health_checker
|
|
||||||
|
|
||||||
logger.info("External Service started successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to start External Service: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down External Service...")
|
|
||||||
await close_db()
|
|
||||||
|
|
||||||
# Set lifespan AFTER metrics setup
|
|
||||||
app.router.lifespan_context = lifespan
|
|
||||||
|
|
||||||
# CORS middleware (added after metrics setup)
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
from app.api.weather import router as weather_router
|
from app.api.weather import router as weather_router
|
||||||
from app.api.traffic import router as traffic_router
|
from app.api.traffic import router as traffic_router
|
||||||
app.include_router(weather_router, prefix="/api/v1", tags=["weather"])
|
|
||||||
app.include_router(traffic_router, prefix="/api/v1", tags=["traffic"])
|
|
||||||
|
|
||||||
# Health check endpoint
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Comprehensive health check endpoint"""
|
|
||||||
if health_checker:
|
|
||||||
return await health_checker.check_health()
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"service": "external-service",
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Root endpoint
|
class ExternalService(StandardFastAPIService):
|
||||||
@app.get("/")
|
"""External Data Service with standardized setup"""
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
def __init__(self):
|
||||||
return {
|
# Define expected database tables for health checks
|
||||||
"service": "External Data Service",
|
external_expected_tables = [
|
||||||
"version": "1.0.0",
|
'weather_data', 'weather_forecasts', 'traffic_data',
|
||||||
"status": "running",
|
'traffic_measurement_points', 'traffic_background_jobs'
|
||||||
"endpoints": {
|
]
|
||||||
"health": "/health",
|
|
||||||
"docs": "/docs",
|
# Define custom API checks
|
||||||
"weather": "/api/v1/weather",
|
async def check_weather_api():
|
||||||
"traffic": "/api/v1/traffic",
|
"""Check weather API configuration"""
|
||||||
"jobs": "/api/v1/jobs"
|
try:
|
||||||
|
return bool(settings.AEMET_API_KEY)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Weather API check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_traffic_api():
|
||||||
|
"""Check traffic API configuration"""
|
||||||
|
try:
|
||||||
|
return bool(settings.MADRID_OPENDATA_API_KEY)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Traffic API check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Define custom metrics for external service
|
||||||
|
external_custom_metrics = {
|
||||||
|
"weather_api_calls_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total weather API calls"
|
||||||
},
|
},
|
||||||
"data_sources": {
|
"weather_api_success_total": {
|
||||||
"weather": "AEMET (Spanish Weather Service)",
|
"type": "counter",
|
||||||
"traffic": "Madrid Open Data Portal",
|
"description": "Successful weather API calls"
|
||||||
"coverage": "Madrid, Spain"
|
},
|
||||||
|
"weather_api_failures_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Failed weather API calls"
|
||||||
|
},
|
||||||
|
"traffic_api_calls_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total traffic API calls"
|
||||||
|
},
|
||||||
|
"traffic_api_success_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Successful traffic API calls"
|
||||||
|
},
|
||||||
|
"traffic_api_failures_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Failed traffic API calls"
|
||||||
|
},
|
||||||
|
"data_collection_jobs_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Data collection jobs"
|
||||||
|
},
|
||||||
|
"data_records_stored_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Data records stored"
|
||||||
|
},
|
||||||
|
"data_quality_issues_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Data quality issues detected"
|
||||||
|
},
|
||||||
|
"weather_api_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Weather API call duration"
|
||||||
|
},
|
||||||
|
"traffic_api_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Traffic API call duration"
|
||||||
|
},
|
||||||
|
"data_collection_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Data collection job duration"
|
||||||
|
},
|
||||||
|
"data_processing_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Data processing duration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Exception handlers
|
super().__init__(
|
||||||
@app.exception_handler(Exception)
|
service_name="external-service",
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
app_name="Bakery External Data Service",
|
||||||
"""Global exception handler with metrics"""
|
description="External data collection service for weather, traffic, and events data",
|
||||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
version="1.0.0",
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
# Record error metric if available
|
cors_origins=settings.CORS_ORIGINS,
|
||||||
if metrics_collector:
|
api_prefix="/api/v1",
|
||||||
metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
|
database_manager=database_manager,
|
||||||
|
expected_tables=external_expected_tables,
|
||||||
return JSONResponse(
|
custom_health_checks={
|
||||||
status_code=500,
|
"weather_api": check_weather_api,
|
||||||
content={"detail": "Internal server error"}
|
"traffic_api": check_traffic_api
|
||||||
|
},
|
||||||
|
custom_metrics=external_custom_metrics,
|
||||||
|
enable_messaging=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _setup_messaging(self):
|
||||||
|
"""Setup messaging for external service"""
|
||||||
|
await setup_messaging()
|
||||||
|
self.logger.info("External service messaging initialized")
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging for external service"""
|
||||||
|
await cleanup_messaging()
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for external service"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for external service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return external-specific features"""
|
||||||
|
return [
|
||||||
|
"weather_data_collection",
|
||||||
|
"traffic_data_collection",
|
||||||
|
"aemet_integration",
|
||||||
|
"madrid_opendata_integration",
|
||||||
|
"data_quality_monitoring",
|
||||||
|
"scheduled_collection_jobs",
|
||||||
|
"external_api_monitoring"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = ExternalService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app()
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(weather_router, tags=["weather"])
|
||||||
|
service.add_router(traffic_router, tags=["traffic"])
|
||||||
@@ -7,67 +7,74 @@ Database configuration for forecasting service
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
from sqlalchemy.pool import NullPool
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from shared.database.base import Base
|
from shared.database.base import Base, DatabaseManager
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
# Create async engine
|
# Create async engine
|
||||||
engine = create_async_engine(
|
async_engine = create_async_engine(
|
||||||
settings.DATABASE_URL,
|
settings.DATABASE_URL,
|
||||||
poolclass=NullPool,
|
|
||||||
echo=settings.DEBUG,
|
echo=settings.DEBUG,
|
||||||
future=True
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
pool_recycle=3600
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create session factory
|
# Create async session factory
|
||||||
AsyncSessionLocal = async_sessionmaker(
|
AsyncSessionLocal = async_sessionmaker(
|
||||||
engine,
|
bind=async_engine,
|
||||||
class_=AsyncSession,
|
class_=AsyncSession,
|
||||||
expire_on_commit=False,
|
expire_on_commit=False
|
||||||
autoflush=False,
|
|
||||||
autocommit=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class DatabaseManager:
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
"""Database management operations"""
|
|
||||||
|
|
||||||
async def create_tables(self):
|
|
||||||
"""Create database tables"""
|
|
||||||
async with engine.begin() as conn:
|
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
|
||||||
logger.info("Forecasting database tables created successfully")
|
|
||||||
|
|
||||||
async def get_session(self) -> AsyncSession:
|
|
||||||
"""Get database session"""
|
"""Get database session"""
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
await session.commit()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(f"Database session error: {e}")
|
logger.error("Database session error", error=str(e))
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
# Global database manager instance
|
async def init_database():
|
||||||
database_manager = DatabaseManager()
|
"""Initialize database tables"""
|
||||||
|
try:
|
||||||
|
async with async_engine.begin() as conn:
|
||||||
|
# Import all models to ensure they are registered
|
||||||
|
from app.models.forecast import ForecastBatch, Forecast
|
||||||
|
from app.models.prediction import PredictionBatch, Prediction
|
||||||
|
|
||||||
async def get_db() -> AsyncSession:
|
# Create all tables
|
||||||
"""Database dependency"""
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
async for session in database_manager.get_session():
|
|
||||||
yield session
|
logger.info("Forecasting database initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to initialize forecasting database", error=str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
async def get_db_health() -> bool:
|
async def get_db_health() -> bool:
|
||||||
"""Check database health"""
|
"""Check database health"""
|
||||||
try:
|
try:
|
||||||
async with AsyncSessionLocal() as session:
|
async with async_engine.begin() as conn:
|
||||||
await session.execute(text("SELECT 1"))
|
await conn.execute(text("SELECT 1"))
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Database health check failed: {e}")
|
logger.error("Database health check failed", error=str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Database manager instance for service_base compatibility
|
||||||
|
database_manager = DatabaseManager(
|
||||||
|
database_url=settings.DATABASE_URL,
|
||||||
|
service_name="forecasting-service",
|
||||||
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
echo=settings.DEBUG
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,145 +6,142 @@ Forecasting Service Main Application
|
|||||||
Demand prediction and forecasting service for bakery operations
|
Demand prediction and forecasting service for bakery operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
from fastapi import FastAPI
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import database_manager, get_db_health
|
from app.core.database import database_manager
|
||||||
from app.api import forecasts, predictions
|
from app.api import forecasts, predictions
|
||||||
|
|
||||||
|
|
||||||
from app.services.messaging import setup_messaging, cleanup_messaging
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
||||||
from app.services.forecasting_alert_service import ForecastingAlertService
|
from app.services.forecasting_alert_service import ForecastingAlertService
|
||||||
from shared.monitoring.logging import setup_logging
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import MetricsCollector
|
|
||||||
|
|
||||||
# Setup structured logging
|
|
||||||
setup_logging("forecasting-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Initialize metrics collector
|
|
||||||
metrics_collector = MetricsCollector("forecasting-service")
|
|
||||||
|
|
||||||
# Initialize alert service
|
|
||||||
alert_service = None
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Application lifespan manager for startup and shutdown events"""
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Forecasting Service", version="1.0.0")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize database
|
|
||||||
logger.info("Initializing database connection")
|
|
||||||
await database_manager.create_tables()
|
|
||||||
logger.info("Database initialized successfully")
|
|
||||||
|
|
||||||
# Initialize messaging
|
|
||||||
logger.info("Setting up messaging")
|
|
||||||
await setup_messaging()
|
|
||||||
logger.info("Messaging initialized")
|
|
||||||
|
|
||||||
# Initialize forecasting alert service
|
|
||||||
logger.info("Setting up forecasting alert service")
|
|
||||||
global alert_service
|
|
||||||
alert_service = ForecastingAlertService(settings)
|
|
||||||
await alert_service.start()
|
|
||||||
logger.info("Forecasting alert service initialized")
|
|
||||||
|
|
||||||
# Register custom metrics
|
|
||||||
metrics_collector.register_counter("forecasts_generated_total", "Total forecasts generated")
|
|
||||||
metrics_collector.register_counter("predictions_served_total", "Total predictions served")
|
|
||||||
metrics_collector.register_counter("prediction_errors_total", "Total prediction errors") # ← MISSING REGISTRATION!
|
|
||||||
metrics_collector.register_histogram("forecast_processing_time_seconds", "Time to process forecast request")
|
|
||||||
metrics_collector.register_histogram("prediction_processing_time_seconds", "Time to process prediction request") # ← ADD MISSING METRIC!
|
|
||||||
metrics_collector.register_gauge("active_models_count", "Number of active models")
|
|
||||||
metrics_collector.register_counter("model_cache_hits_total", "Total model cache hits") # ← ADD USEFUL METRIC!
|
|
||||||
metrics_collector.register_counter("model_cache_misses_total", "Total model cache misses") # ← ADD USEFUL METRIC!
|
|
||||||
|
|
||||||
# Start metrics server
|
|
||||||
metrics_collector.start_metrics_server(8080)
|
|
||||||
|
|
||||||
logger.info("Forecasting Service started successfully")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to start Forecasting Service", error=str(e))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Forecasting Service")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Cleanup alert service
|
|
||||||
if alert_service:
|
|
||||||
await alert_service.stop()
|
|
||||||
logger.info("Alert service cleanup completed")
|
|
||||||
|
|
||||||
await cleanup_messaging()
|
|
||||||
logger.info("Messaging cleanup completed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error during messaging cleanup", error=str(e))
|
|
||||||
|
|
||||||
# Create FastAPI app with lifespan
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bakery Forecasting Service",
|
|
||||||
description="AI-powered demand prediction and forecasting service for bakery operations",
|
|
||||||
version="1.0.0",
|
|
||||||
docs_url="/docs",
|
|
||||||
redoc_url="/redoc",
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
# CORS middleware
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS_LIST,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include API routers
|
|
||||||
app.include_router(forecasts.router, prefix="/api/v1", tags=["forecasts"])
|
|
||||||
|
|
||||||
app.include_router(predictions.router, prefix="/api/v1", tags=["predictions"])
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
class ForecastingService(StandardFastAPIService):
|
||||||
async def health_check():
|
"""Forecasting Service with standardized setup"""
|
||||||
"""Health check endpoint"""
|
|
||||||
db_health = await get_db_health()
|
|
||||||
alert_health = await alert_service.health_check() if alert_service else {"status": "not_initialized"}
|
|
||||||
|
|
||||||
overall_health = db_health and alert_health.get("status") == "healthy"
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
forecasting_expected_tables = [
|
||||||
|
'forecasts', 'prediction_batches', 'model_performance_metrics', 'prediction_cache'
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
self.alert_service = None
|
||||||
"status": "healthy" if overall_health else "unhealthy",
|
|
||||||
"service": "forecasting-service",
|
# Create custom checks for alert service
|
||||||
"version": "1.0.0",
|
async def alert_service_check():
|
||||||
"database": "connected" if db_health else "disconnected",
|
"""Custom health check for forecasting alert service"""
|
||||||
"alert_service": alert_health,
|
return await self.alert_service.health_check() if self.alert_service else False
|
||||||
"timestamp": structlog.get_logger().info("Health check requested")
|
|
||||||
|
# Define custom metrics for forecasting service
|
||||||
|
forecasting_custom_metrics = {
|
||||||
|
"forecasts_generated_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total forecasts generated"
|
||||||
|
},
|
||||||
|
"predictions_served_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total predictions served"
|
||||||
|
},
|
||||||
|
"prediction_errors_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total prediction errors"
|
||||||
|
},
|
||||||
|
"forecast_processing_time_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Time to process forecast request"
|
||||||
|
},
|
||||||
|
"prediction_processing_time_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Time to process prediction request"
|
||||||
|
},
|
||||||
|
"model_cache_hits_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total model cache hits"
|
||||||
|
},
|
||||||
|
"model_cache_misses_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total model cache misses"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/metrics")
|
super().__init__(
|
||||||
async def get_metrics():
|
service_name="forecasting-service",
|
||||||
"""Metrics endpoint for Prometheus"""
|
app_name="Bakery Forecasting Service",
|
||||||
return metrics_collector.get_metrics()
|
description="AI-powered demand prediction and forecasting service for bakery operations",
|
||||||
|
version="1.0.0",
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
cors_origins=settings.CORS_ORIGINS_LIST,
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=forecasting_expected_tables,
|
||||||
|
custom_health_checks={"alert_service": alert_service_check},
|
||||||
|
enable_messaging=True,
|
||||||
|
custom_metrics=forecasting_custom_metrics
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/alert-metrics")
|
async def _setup_messaging(self):
|
||||||
|
"""Setup messaging for forecasting service"""
|
||||||
|
await setup_messaging()
|
||||||
|
self.logger.info("Messaging initialized")
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging for forecasting service"""
|
||||||
|
await cleanup_messaging()
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for forecasting service"""
|
||||||
|
# Initialize forecasting alert service
|
||||||
|
self.alert_service = ForecastingAlertService(settings)
|
||||||
|
await self.alert_service.start()
|
||||||
|
self.logger.info("Forecasting alert service initialized")
|
||||||
|
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for forecasting service"""
|
||||||
|
# Cleanup alert service
|
||||||
|
if self.alert_service:
|
||||||
|
await self.alert_service.stop()
|
||||||
|
self.logger.info("Alert service cleanup completed")
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return forecasting-specific features"""
|
||||||
|
return [
|
||||||
|
"demand_prediction",
|
||||||
|
"ai_forecasting",
|
||||||
|
"model_performance_tracking",
|
||||||
|
"prediction_caching",
|
||||||
|
"alert_notifications",
|
||||||
|
"messaging_integration"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_endpoints(self):
|
||||||
|
"""Setup custom endpoints for forecasting service"""
|
||||||
|
@self.app.get("/alert-metrics")
|
||||||
async def get_alert_metrics():
|
async def get_alert_metrics():
|
||||||
"""Alert service metrics endpoint"""
|
"""Alert service metrics endpoint"""
|
||||||
if alert_service:
|
if self.alert_service:
|
||||||
return alert_service.get_metrics()
|
return self.alert_service.get_metrics()
|
||||||
return {"error": "Alert service not initialized"}
|
return {"error": "Alert service not initialized"}
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = ForecastingService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app(
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom endpoints
|
||||||
|
service.setup_custom_endpoints()
|
||||||
|
|
||||||
|
# Include API routers
|
||||||
|
service.add_router(forecasts.router, tags=["forecasts"])
|
||||||
|
service.add_router(predictions.router, tags=["predictions"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -4,191 +4,62 @@ Inventory Service FastAPI Application
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from fastapi import FastAPI
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
# Import core modules
|
# Import core modules
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db, close_db, health_check as db_health_check
|
from app.core.database import database_manager
|
||||||
from app.api import ingredients, stock, classification, transformations
|
from app.api import ingredients, stock, classification, transformations
|
||||||
from app.services.inventory_alert_service import InventoryAlertService
|
from app.services.inventory_alert_service import InventoryAlertService
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
from shared.service_base import StandardFastAPIService
|
||||||
# Auth decorators are used in endpoints, no global setup needed
|
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
# Import enhanced routers
|
||||||
|
from app.api.dashboard import router as dashboard_router
|
||||||
|
from app.api.food_safety import router as food_safety_router
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
class InventoryService(StandardFastAPIService):
|
||||||
async def lifespan(app: FastAPI):
|
"""Inventory Service with standardized setup"""
|
||||||
"""Application lifespan management"""
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Inventory Service", version=settings.VERSION)
|
|
||||||
|
|
||||||
try:
|
def __init__(self):
|
||||||
# Initialize database
|
# Define expected database tables for health checks
|
||||||
await init_db()
|
inventory_expected_tables = [
|
||||||
logger.info("Database initialized successfully")
|
'ingredients', 'stock', 'stock_movements', 'product_transformations',
|
||||||
|
'stock_alerts', 'food_safety_compliance', 'temperature_logs', 'food_safety_alerts'
|
||||||
|
]
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
service_name="inventory-service",
|
||||||
|
app_name=settings.APP_NAME,
|
||||||
|
description=settings.DESCRIPTION,
|
||||||
|
version=settings.VERSION,
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
cors_origins=settings.CORS_ORIGINS,
|
||||||
|
api_prefix=settings.API_V1_STR,
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=inventory_expected_tables
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for inventory service"""
|
||||||
# Initialize alert service
|
# Initialize alert service
|
||||||
alert_service = InventoryAlertService(settings)
|
alert_service = InventoryAlertService(settings)
|
||||||
await alert_service.start()
|
await alert_service.start()
|
||||||
logger.info("Inventory alert service started")
|
self.logger.info("Inventory alert service started")
|
||||||
|
|
||||||
# Store alert service in app state
|
# Store alert service in app state
|
||||||
app.state.alert_service = alert_service
|
app.state.alert_service = alert_service
|
||||||
|
|
||||||
# Setup metrics is already done early - no need to do it here
|
async def on_shutdown(self, app: FastAPI):
|
||||||
logger.info("Metrics setup completed")
|
"""Custom shutdown logic for inventory service"""
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Startup failed", error=str(e))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Inventory Service")
|
|
||||||
try:
|
|
||||||
# Stop alert service
|
# Stop alert service
|
||||||
if hasattr(app.state, 'alert_service'):
|
if hasattr(app.state, 'alert_service'):
|
||||||
await app.state.alert_service.stop()
|
await app.state.alert_service.stop()
|
||||||
logger.info("Alert service stopped")
|
self.logger.info("Alert service stopped")
|
||||||
|
|
||||||
await close_db()
|
def get_service_features(self):
|
||||||
logger.info("Database connections closed")
|
"""Return inventory-specific features"""
|
||||||
except Exception as e:
|
return [
|
||||||
logger.error("Shutdown error", error=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI application
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.APP_NAME,
|
|
||||||
description=settings.DESCRIPTION,
|
|
||||||
version=settings.VERSION,
|
|
||||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
|
||||||
docs_url=f"{settings.API_V1_STR}/docs",
|
|
||||||
redoc_url=f"{settings.API_V1_STR}/redoc",
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup metrics BEFORE any middleware and BEFORE lifespan
|
|
||||||
metrics_collector = setup_metrics_early(app, "inventory-service")
|
|
||||||
|
|
||||||
|
|
||||||
# CORS middleware
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Auth is handled via decorators in individual endpoints
|
|
||||||
|
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
@app.exception_handler(ValueError)
|
|
||||||
async def value_error_handler(request: Request, exc: ValueError):
|
|
||||||
"""Handle validation errors"""
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=400,
|
|
||||||
content={
|
|
||||||
"error": "Validation Error",
|
|
||||||
"detail": str(exc),
|
|
||||||
"type": "value_error"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def general_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Handle general exceptions"""
|
|
||||||
logger.error(
|
|
||||||
"Unhandled exception",
|
|
||||||
error=str(exc),
|
|
||||||
path=request.url.path,
|
|
||||||
method=request.method
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={
|
|
||||||
"error": "Internal Server Error",
|
|
||||||
"detail": "An unexpected error occurred",
|
|
||||||
"type": "internal_error"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(ingredients.router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(stock.router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(transformations.router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(classification.router, prefix=settings.API_V1_STR)
|
|
||||||
|
|
||||||
# Include enhanced routers
|
|
||||||
from app.api.dashboard import router as dashboard_router
|
|
||||||
from app.api.food_safety import router as food_safety_router
|
|
||||||
|
|
||||||
app.include_router(dashboard_router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(food_safety_router, prefix=settings.API_V1_STR)
|
|
||||||
|
|
||||||
|
|
||||||
# Root endpoint
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint with service information"""
|
|
||||||
return {
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"description": settings.DESCRIPTION,
|
|
||||||
"status": "running",
|
|
||||||
"docs_url": f"{settings.API_V1_STR}/docs",
|
|
||||||
"health_url": "/health"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Comprehensive health check endpoint"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"timestamp": structlog.get_logger().info("Health check requested")
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Health check failed", error=str(e))
|
|
||||||
return {
|
|
||||||
"status": "unhealthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"database": "unknown",
|
|
||||||
"alert_service": {"status": "unknown"},
|
|
||||||
"error": str(e),
|
|
||||||
"timestamp": structlog.get_logger().info("Health check failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Service info endpoint
|
|
||||||
@app.get(f"{settings.API_V1_STR}/info")
|
|
||||||
async def service_info():
|
|
||||||
"""Service information endpoint"""
|
|
||||||
return {
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"description": settings.DESCRIPTION,
|
|
||||||
"api_version": "v1",
|
|
||||||
"environment": settings.ENVIRONMENT,
|
|
||||||
"features": [
|
|
||||||
"ingredient_management",
|
"ingredient_management",
|
||||||
"stock_tracking",
|
"stock_tracking",
|
||||||
"expiration_alerts",
|
"expiration_alerts",
|
||||||
@@ -203,7 +74,24 @@ async def service_info():
|
|||||||
"real_time_alerts",
|
"real_time_alerts",
|
||||||
"regulatory_reporting"
|
"regulatory_reporting"
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = InventoryService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app()
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Include routers using the service helper
|
||||||
|
service.add_router(ingredients.router)
|
||||||
|
service.add_router(stock.router)
|
||||||
|
service.add_router(transformations.router)
|
||||||
|
service.add_router(classification.router)
|
||||||
|
service.add_router(dashboard_router)
|
||||||
|
service.add_router(food_safety_router)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -6,14 +6,9 @@ Notification Service Main Application
|
|||||||
Handles email, WhatsApp notifications and SSE for real-time alerts/recommendations
|
Handles email, WhatsApp notifications and SSE for real-time alerts/recommendations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
from fastapi import FastAPI
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db
|
from app.core.database import database_manager
|
||||||
from app.api.notifications import router as notification_router
|
from app.api.notifications import router as notification_router
|
||||||
from app.api.sse_routes import router as sse_router
|
from app.api.sse_routes import router as sse_router
|
||||||
from app.services.messaging import setup_messaging, cleanup_messaging
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
||||||
@@ -21,227 +16,208 @@ from app.services.sse_service import SSEService
|
|||||||
from app.services.notification_orchestrator import NotificationOrchestrator
|
from app.services.notification_orchestrator import NotificationOrchestrator
|
||||||
from app.services.email_service import EmailService
|
from app.services.email_service import EmailService
|
||||||
from app.services.whatsapp_service import WhatsAppService
|
from app.services.whatsapp_service import WhatsAppService
|
||||||
from shared.monitoring import setup_logging, HealthChecker
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
|
||||||
|
|
||||||
# Setup logging first
|
|
||||||
setup_logging("notification-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Global variables for lifespan access
|
class NotificationService(StandardFastAPIService):
|
||||||
metrics_collector = None
|
"""Notification Service with standardized setup"""
|
||||||
health_checker = None
|
|
||||||
|
|
||||||
# Create FastAPI app FIRST
|
def __init__(self):
|
||||||
app = FastAPI(
|
# Define expected database tables for health checks
|
||||||
title="Bakery Notification Service",
|
notification_expected_tables = [
|
||||||
|
'notifications', 'notification_templates', 'notification_preferences',
|
||||||
|
'notification_logs', 'email_templates', 'whatsapp_templates'
|
||||||
|
]
|
||||||
|
|
||||||
|
self.sse_service = None
|
||||||
|
self.orchestrator = None
|
||||||
|
self.email_service = None
|
||||||
|
self.whatsapp_service = None
|
||||||
|
|
||||||
|
# Define custom metrics for notification service
|
||||||
|
notification_custom_metrics = {
|
||||||
|
"notifications_sent_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total notifications sent",
|
||||||
|
"labels": ["type", "status", "channel"]
|
||||||
|
},
|
||||||
|
"emails_sent_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total emails sent",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"whatsapp_sent_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total WhatsApp messages sent",
|
||||||
|
"labels": ["status"]
|
||||||
|
},
|
||||||
|
"sse_events_sent_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total SSE events sent",
|
||||||
|
"labels": ["tenant", "event_type"]
|
||||||
|
},
|
||||||
|
"notification_processing_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Time spent processing notifications"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define custom health checks for notification service components
|
||||||
|
async def check_email_service():
|
||||||
|
"""Check email service health"""
|
||||||
|
try:
|
||||||
|
return await self.email_service.health_check() if self.email_service else False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Email service health check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_whatsapp_service():
|
||||||
|
"""Check WhatsApp service health"""
|
||||||
|
try:
|
||||||
|
return await self.whatsapp_service.health_check() if self.whatsapp_service else False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("WhatsApp service health check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_sse_service():
|
||||||
|
"""Check SSE service health"""
|
||||||
|
try:
|
||||||
|
if self.sse_service:
|
||||||
|
metrics = self.sse_service.get_metrics()
|
||||||
|
return bool(metrics.get("redis_connected", False))
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("SSE service health check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def check_messaging():
|
||||||
|
"""Check messaging service health"""
|
||||||
|
try:
|
||||||
|
from app.services.messaging import notification_publisher
|
||||||
|
return bool(notification_publisher and notification_publisher.connected)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Messaging health check failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
service_name="notification-service",
|
||||||
|
app_name="Bakery Notification Service",
|
||||||
description="Email, WhatsApp and SSE notification service for bakery alerts and recommendations",
|
description="Email, WhatsApp and SSE notification service for bakery alerts and recommendations",
|
||||||
version="2.0.0",
|
version="2.0.0",
|
||||||
docs_url="/docs",
|
log_level=settings.LOG_LEVEL,
|
||||||
redoc_url="/redoc"
|
cors_origins=getattr(settings, 'CORS_ORIGINS', ["*"]),
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=notification_expected_tables,
|
||||||
|
custom_health_checks={
|
||||||
|
"email_service": check_email_service,
|
||||||
|
"whatsapp_service": check_whatsapp_service,
|
||||||
|
"sse_service": check_sse_service,
|
||||||
|
"messaging": check_messaging
|
||||||
|
},
|
||||||
|
enable_messaging=True,
|
||||||
|
custom_metrics=notification_custom_metrics
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup metrics BEFORE any middleware and BEFORE lifespan
|
async def _setup_messaging(self):
|
||||||
metrics_collector = setup_metrics_early(app, "notification-service")
|
"""Setup messaging for notification service"""
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Application lifespan events - NO MIDDLEWARE ADDED HERE"""
|
|
||||||
global health_checker
|
|
||||||
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Notification Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize database
|
|
||||||
await init_db()
|
|
||||||
logger.info("Database initialized")
|
|
||||||
|
|
||||||
# Setup messaging
|
|
||||||
await setup_messaging()
|
await setup_messaging()
|
||||||
logger.info("Messaging initialized")
|
self.logger.info("Messaging initialized")
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging for notification service"""
|
||||||
|
await cleanup_messaging()
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for notification service"""
|
||||||
# Initialize services
|
# Initialize services
|
||||||
email_service = EmailService()
|
self.email_service = EmailService()
|
||||||
whatsapp_service = WhatsAppService()
|
self.whatsapp_service = WhatsAppService()
|
||||||
|
|
||||||
# Initialize SSE service
|
# Initialize SSE service
|
||||||
sse_service = SSEService(settings.REDIS_URL)
|
self.sse_service = SSEService(settings.REDIS_URL)
|
||||||
await sse_service.initialize()
|
await self.sse_service.initialize()
|
||||||
logger.info("SSE service initialized")
|
self.logger.info("SSE service initialized")
|
||||||
|
|
||||||
# Create orchestrator
|
# Create orchestrator
|
||||||
orchestrator = NotificationOrchestrator(
|
self.orchestrator = NotificationOrchestrator(
|
||||||
email_service=email_service,
|
email_service=self.email_service,
|
||||||
whatsapp_service=whatsapp_service,
|
whatsapp_service=self.whatsapp_service,
|
||||||
sse_service=sse_service
|
sse_service=self.sse_service
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store services in app state
|
# Store services in app state
|
||||||
app.state.orchestrator = orchestrator
|
app.state.orchestrator = self.orchestrator
|
||||||
app.state.sse_service = sse_service
|
app.state.sse_service = self.sse_service
|
||||||
app.state.email_service = email_service
|
app.state.email_service = self.email_service
|
||||||
app.state.whatsapp_service = whatsapp_service
|
app.state.whatsapp_service = self.whatsapp_service
|
||||||
|
|
||||||
# Register custom metrics (metrics_collector already exists)
|
async def on_shutdown(self, app: FastAPI):
|
||||||
metrics_collector.register_counter("notifications_sent_total", "Total notifications sent", labels=["type", "status", "channel"])
|
"""Custom shutdown logic for notification service"""
|
||||||
metrics_collector.register_counter("emails_sent_total", "Total emails sent", labels=["status"])
|
|
||||||
metrics_collector.register_counter("whatsapp_sent_total", "Total WhatsApp messages sent", labels=["status"])
|
|
||||||
metrics_collector.register_counter("sse_events_sent_total", "Total SSE events sent", labels=["tenant", "event_type"])
|
|
||||||
metrics_collector.register_histogram("notification_processing_duration_seconds", "Time spent processing notifications")
|
|
||||||
metrics_collector.register_gauge("notification_queue_size", "Current notification queue size")
|
|
||||||
metrics_collector.register_gauge("sse_active_connections", "Number of active SSE connections")
|
|
||||||
|
|
||||||
# Setup health checker
|
|
||||||
health_checker = HealthChecker("notification-service")
|
|
||||||
|
|
||||||
# Add database health check
|
|
||||||
async def check_database():
|
|
||||||
try:
|
|
||||||
from app.core.database import get_db
|
|
||||||
from sqlalchemy import text
|
|
||||||
async for db in get_db():
|
|
||||||
await db.execute(text("SELECT 1"))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return f"Database error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("database", check_database, timeout=5.0, critical=True)
|
|
||||||
|
|
||||||
# Add email service health check
|
|
||||||
async def check_email_service():
|
|
||||||
try:
|
|
||||||
from app.services.email_service import EmailService
|
|
||||||
email_service = EmailService()
|
|
||||||
return await email_service.health_check()
|
|
||||||
except Exception as e:
|
|
||||||
return f"Email service error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("email_service", check_email_service, timeout=10.0, critical=True)
|
|
||||||
|
|
||||||
# Add WhatsApp service health check
|
|
||||||
async def check_whatsapp_service():
|
|
||||||
try:
|
|
||||||
return await whatsapp_service.health_check()
|
|
||||||
except Exception as e:
|
|
||||||
return f"WhatsApp service error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("whatsapp_service", check_whatsapp_service, timeout=10.0, critical=False)
|
|
||||||
|
|
||||||
# Add SSE service health check
|
|
||||||
async def check_sse_service():
|
|
||||||
try:
|
|
||||||
metrics = sse_service.get_metrics()
|
|
||||||
return "healthy" if metrics["redis_connected"] else "Redis connection failed"
|
|
||||||
except Exception as e:
|
|
||||||
return f"SSE service error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("sse_service", check_sse_service, timeout=5.0, critical=True)
|
|
||||||
|
|
||||||
# Add messaging health check
|
|
||||||
def check_messaging():
|
|
||||||
try:
|
|
||||||
# Check if messaging is properly initialized
|
|
||||||
from app.services.messaging import notification_publisher
|
|
||||||
return notification_publisher.connected if notification_publisher else False
|
|
||||||
except Exception as e:
|
|
||||||
return f"Messaging error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("messaging", check_messaging, timeout=3.0, critical=False)
|
|
||||||
|
|
||||||
# Store health checker in app state
|
|
||||||
app.state.health_checker = health_checker
|
|
||||||
|
|
||||||
logger.info("Notification Service with SSE support started successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to start Notification Service: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Notification Service...")
|
|
||||||
try:
|
|
||||||
# Shutdown SSE service
|
# Shutdown SSE service
|
||||||
if hasattr(app.state, 'sse_service'):
|
if self.sse_service:
|
||||||
await app.state.sse_service.shutdown()
|
await self.sse_service.shutdown()
|
||||||
logger.info("SSE service shutdown completed")
|
self.logger.info("SSE service shutdown completed")
|
||||||
|
|
||||||
await cleanup_messaging()
|
def get_service_features(self):
|
||||||
logger.info("Messaging cleanup completed")
|
"""Return notification-specific features"""
|
||||||
except Exception as e:
|
return [
|
||||||
logger.error(f"Error during shutdown: {e}")
|
"email_notifications",
|
||||||
|
"whatsapp_notifications",
|
||||||
|
"sse_real_time_updates",
|
||||||
|
"notification_templates",
|
||||||
|
"notification_orchestration",
|
||||||
|
"messaging_integration",
|
||||||
|
"multi_channel_support"
|
||||||
|
]
|
||||||
|
|
||||||
# Set lifespan AFTER metrics setup
|
def setup_custom_endpoints(self):
|
||||||
app.router.lifespan_context = lifespan
|
"""Setup custom endpoints for notification service"""
|
||||||
|
# SSE metrics endpoint
|
||||||
# CORS middleware (added after metrics setup)
|
@self.app.get("/sse-metrics")
|
||||||
app.add_middleware(
|
async def sse_metrics():
|
||||||
CORSMiddleware,
|
"""Get SSE service metrics"""
|
||||||
allow_origins=getattr(settings, 'CORS_ORIGINS', ["*"]),
|
if self.sse_service:
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(notification_router, prefix="/api/v1", tags=["notifications"])
|
|
||||||
app.include_router(sse_router, prefix="/api/v1", tags=["sse"])
|
|
||||||
|
|
||||||
# Health check endpoint
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Comprehensive health check endpoint including SSE"""
|
|
||||||
if health_checker:
|
|
||||||
health_result = await health_checker.check_health()
|
|
||||||
|
|
||||||
# Add SSE metrics to health check
|
|
||||||
if hasattr(app.state, 'sse_service'):
|
|
||||||
try:
|
try:
|
||||||
sse_metrics = app.state.sse_service.get_metrics()
|
sse_metrics = self.sse_service.get_metrics()
|
||||||
# Convert metrics to JSON-serializable format
|
return {
|
||||||
health_result['sse_metrics'] = {
|
|
||||||
'active_tenants': sse_metrics.get('active_tenants', 0),
|
'active_tenants': sse_metrics.get('active_tenants', 0),
|
||||||
'total_connections': sse_metrics.get('total_connections', 0),
|
'total_connections': sse_metrics.get('total_connections', 0),
|
||||||
'active_listeners': sse_metrics.get('active_listeners', 0),
|
'active_listeners': sse_metrics.get('active_listeners', 0),
|
||||||
'redis_connected': bool(sse_metrics.get('redis_connected', False))
|
'redis_connected': bool(sse_metrics.get('redis_connected', False))
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
health_result['sse_error'] = str(e)
|
return {"error": str(e)}
|
||||||
|
return {"error": "SSE service not available"}
|
||||||
return health_result
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"service": "notification-service",
|
|
||||||
"status": "healthy",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"features": ["email", "whatsapp", "sse", "alerts", "recommendations"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Metrics endpoint
|
# Metrics endpoint
|
||||||
@app.get("/metrics")
|
@self.app.get("/metrics")
|
||||||
async def metrics():
|
async def metrics():
|
||||||
"""Prometheus metrics endpoint"""
|
"""Prometheus metrics endpoint"""
|
||||||
if metrics_collector:
|
if self.metrics_collector:
|
||||||
return metrics_collector.get_metrics()
|
return self.metrics_collector.get_metrics()
|
||||||
return {"metrics": "not_available"}
|
return {"metrics": "not_available"}
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Global exception handler with metrics"""
|
|
||||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
||||||
|
|
||||||
# Record error metric if available
|
# Create service instance
|
||||||
if metrics_collector:
|
service = NotificationService()
|
||||||
metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
|
|
||||||
|
|
||||||
return JSONResponse(
|
# Create FastAPI app with standardized setup
|
||||||
status_code=500,
|
app = service.create_app(
|
||||||
content={"detail": "Internal server error"}
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom endpoints
|
||||||
|
service.setup_custom_endpoints()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(notification_router, tags=["notifications"])
|
||||||
|
service.add_router(sse_router, tags=["sse"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -77,3 +77,15 @@ async def get_db_health() -> bool:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Database health check failed", error=str(e))
|
logger.error("Database health check failed", error=str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Database manager instance for service_base compatibility
|
||||||
|
from shared.database.base import DatabaseManager
|
||||||
|
|
||||||
|
database_manager = DatabaseManager(
|
||||||
|
database_url=settings.DATABASE_URL,
|
||||||
|
service_name="orders-service",
|
||||||
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
echo=settings.DEBUG
|
||||||
|
)
|
||||||
@@ -7,113 +7,74 @@ Customer orders and procurement planning service
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_database, get_db_health
|
from app.core.database import database_manager
|
||||||
from app.api.orders import router as orders_router
|
from app.api.orders import router as orders_router
|
||||||
from app.api.procurement import router as procurement_router
|
from app.api.procurement import router as procurement_router
|
||||||
from app.services.procurement_scheduler_service import ProcurementSchedulerService
|
from app.services.procurement_scheduler_service import ProcurementSchedulerService
|
||||||
|
from shared.service_base import StandardFastAPIService
|
||||||
# Configure logging
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
class OrdersService(StandardFastAPIService):
|
||||||
async def lifespan(app: FastAPI):
|
"""Orders Service with standardized setup"""
|
||||||
"""Manage application lifespan events"""
|
|
||||||
# Startup
|
|
||||||
try:
|
|
||||||
await init_database()
|
|
||||||
logger.info("Database initialized successfully")
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
orders_expected_tables = [
|
||||||
|
'customers', 'customer_contacts', 'customer_orders', 'order_items',
|
||||||
|
'order_status_history', 'procurement_plans', 'procurement_requirements'
|
||||||
|
]
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
service_name="orders-service",
|
||||||
|
app_name=settings.APP_NAME,
|
||||||
|
description=settings.DESCRIPTION,
|
||||||
|
version=settings.VERSION,
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=orders_expected_tables
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for orders service"""
|
||||||
# Initialize procurement scheduler service
|
# Initialize procurement scheduler service
|
||||||
scheduler_service = ProcurementSchedulerService(settings)
|
scheduler_service = ProcurementSchedulerService(settings)
|
||||||
await scheduler_service.start()
|
await scheduler_service.start()
|
||||||
logger.info("Procurement scheduler service started")
|
self.logger.info("Procurement scheduler service started")
|
||||||
|
|
||||||
# Store scheduler service in app state
|
# Store scheduler service in app state
|
||||||
app.state.scheduler_service = scheduler_service
|
app.state.scheduler_service = scheduler_service
|
||||||
|
|
||||||
logger.info("Orders service started successfully")
|
async def on_shutdown(self, app: FastAPI):
|
||||||
except Exception as e:
|
"""Custom shutdown logic for orders service"""
|
||||||
logger.error("Failed to initialize orders service", error=str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Orders service shutting down")
|
|
||||||
try:
|
|
||||||
# Stop scheduler service
|
# Stop scheduler service
|
||||||
if hasattr(app.state, 'scheduler_service'):
|
if hasattr(app.state, 'scheduler_service'):
|
||||||
await app.state.scheduler_service.stop()
|
await app.state.scheduler_service.stop()
|
||||||
logger.info("Scheduler service stopped")
|
self.logger.info("Scheduler service stopped")
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error stopping scheduler service", error=str(e))
|
def get_service_features(self):
|
||||||
|
"""Return orders-specific features"""
|
||||||
|
return [
|
||||||
|
"customer_management",
|
||||||
|
"order_processing",
|
||||||
|
"procurement_planning",
|
||||||
|
"order_tracking",
|
||||||
|
"automated_scheduling"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI application
|
# Create service instance
|
||||||
app = FastAPI(
|
service = OrdersService()
|
||||||
title=settings.APP_NAME,
|
|
||||||
description=settings.DESCRIPTION,
|
|
||||||
version=settings.VERSION,
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add CORS middleware
|
# Create FastAPI app with standardized setup
|
||||||
app.add_middleware(
|
app = service.create_app()
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # Configure based on environment
|
# Setup standard endpoints
|
||||||
allow_credentials=True,
|
service.setup_standard_endpoints()
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(orders_router, prefix="/api/v1")
|
service.add_router(orders_router)
|
||||||
app.include_router(procurement_router, prefix="/api/v1")
|
service.add_router(procurement_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
try:
|
|
||||||
db_healthy = await get_db_health()
|
|
||||||
|
|
||||||
health_status = {
|
|
||||||
"status": "healthy" if db_healthy else "unhealthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"database": "connected" if db_healthy else "disconnected"
|
|
||||||
}
|
|
||||||
|
|
||||||
if not db_healthy:
|
|
||||||
health_status["status"] = "unhealthy"
|
|
||||||
|
|
||||||
return health_status
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Health check failed", error=str(e))
|
|
||||||
return {
|
|
||||||
"status": "unhealthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
|
||||||
return {
|
|
||||||
"service": settings.APP_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"description": settings.DESCRIPTION,
|
|
||||||
"status": "running"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/test/procurement-scheduler")
|
@app.post("/test/procurement-scheduler")
|
||||||
|
|||||||
@@ -3,77 +3,94 @@ POS Integration Service
|
|||||||
Handles integration with external POS systems (Square, Toast, Lightspeed)
|
Handles integration with external POS systems (Square, Toast, Lightspeed)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import structlog
|
|
||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from fastapi import FastAPI, Request
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.api import pos_config, webhooks, sync
|
from app.api import pos_config, webhooks, sync
|
||||||
from app.core.database import init_db, close_db
|
from app.core.database import database_manager
|
||||||
from shared.monitoring.health import router as health_router
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.logging import setup_logging
|
|
||||||
|
|
||||||
|
|
||||||
# Setup logging
|
class POSService(StandardFastAPIService):
|
||||||
setup_logging(service_name="pos-service")
|
"""POS Integration Service with standardized setup"""
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
pos_expected_tables = [
|
||||||
|
'pos_configurations', 'pos_transactions', 'pos_transaction_items',
|
||||||
|
'pos_webhook_logs', 'pos_sync_logs'
|
||||||
|
]
|
||||||
|
|
||||||
@asynccontextmanager
|
# Define custom metrics for POS service
|
||||||
async def lifespan(app: FastAPI):
|
pos_custom_metrics = {
|
||||||
"""Lifecycle management for FastAPI app"""
|
"pos_webhooks_received_total": {
|
||||||
logger.info("Starting POS Integration Service")
|
"type": "counter",
|
||||||
|
"description": "Total POS webhooks received",
|
||||||
|
"labels": ["provider", "event_type"]
|
||||||
|
},
|
||||||
|
"pos_sync_jobs_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total POS sync jobs",
|
||||||
|
"labels": ["provider", "status"]
|
||||||
|
},
|
||||||
|
"pos_transactions_synced_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total transactions synced",
|
||||||
|
"labels": ["provider"]
|
||||||
|
},
|
||||||
|
"pos_webhook_processing_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Time spent processing webhooks"
|
||||||
|
},
|
||||||
|
"pos_sync_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Time spent syncing data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Startup
|
super().__init__(
|
||||||
try:
|
service_name="pos-service",
|
||||||
# Initialize database connection
|
app_name="POS Integration Service",
|
||||||
logger.info("Initializing database connection")
|
|
||||||
await init_db()
|
|
||||||
|
|
||||||
# Add any startup logic here
|
|
||||||
logger.info("POS Integration Service started successfully")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to start POS Integration Service", error=str(e))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down POS Integration Service")
|
|
||||||
await close_db()
|
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI app
|
|
||||||
app = FastAPI(
|
|
||||||
title="POS Integration Service",
|
|
||||||
description="Handles integration with external POS systems",
|
description="Handles integration with external POS systems",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs" if settings.ENVIRONMENT != "production" else None,
|
cors_origins=settings.CORS_ORIGINS,
|
||||||
redoc_url="/redoc" if settings.ENVIRONMENT != "production" else None,
|
api_prefix="/api/v1",
|
||||||
lifespan=lifespan
|
database_manager=database_manager,
|
||||||
|
expected_tables=pos_expected_tables,
|
||||||
|
custom_metrics=pos_custom_metrics
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add CORS middleware
|
async def on_startup(self, app: FastAPI):
|
||||||
app.add_middleware(
|
"""Custom startup logic for POS service"""
|
||||||
CORSMiddleware,
|
# Custom startup completed
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
self.logger.info("POS Integration Service started successfully")
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for POS service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return POS-specific features"""
|
||||||
|
return [
|
||||||
|
"pos_integration",
|
||||||
|
"square_support",
|
||||||
|
"toast_support",
|
||||||
|
"lightspeed_support",
|
||||||
|
"webhook_handling",
|
||||||
|
"transaction_sync",
|
||||||
|
"real_time_updates"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_middleware(self):
|
||||||
|
"""Setup custom middleware for POS service"""
|
||||||
# Middleware for request logging and timing
|
# Middleware for request logging and timing
|
||||||
@app.middleware("http")
|
@self.app.middleware("http")
|
||||||
async def log_requests(request: Request, call_next):
|
async def log_requests(request: Request, call_next):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# Log request
|
# Log request
|
||||||
logger.info(
|
self.logger.info(
|
||||||
"Incoming request",
|
"Incoming request",
|
||||||
method=request.method,
|
method=request.method,
|
||||||
url=str(request.url),
|
url=str(request.url),
|
||||||
@@ -84,7 +101,7 @@ async def log_requests(request: Request, call_next):
|
|||||||
|
|
||||||
# Log response
|
# Log response
|
||||||
process_time = time.time() - start_time
|
process_time = time.time() - start_time
|
||||||
logger.info(
|
self.logger.info(
|
||||||
"Request completed",
|
"Request completed",
|
||||||
method=request.method,
|
method=request.method,
|
||||||
url=str(request.url),
|
url=str(request.url),
|
||||||
@@ -95,31 +112,9 @@ async def log_requests(request: Request, call_next):
|
|||||||
response.headers["X-Process-Time"] = str(process_time)
|
response.headers["X-Process-Time"] = str(process_time)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def setup_custom_endpoints(self):
|
||||||
# Global exception handler
|
"""Setup custom endpoints for POS service"""
|
||||||
@app.exception_handler(Exception)
|
@self.app.get("/")
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
|
||||||
logger.error(
|
|
||||||
"Unhandled exception",
|
|
||||||
error=str(exc),
|
|
||||||
method=request.method,
|
|
||||||
url=str(request.url)
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={"detail": "Internal server error"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(health_router, prefix="/health", tags=["health"])
|
|
||||||
app.include_router(pos_config.router, prefix="/api/v1", tags=["pos-config"])
|
|
||||||
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
|
|
||||||
app.include_router(sync.router, prefix="/api/v1", tags=["sync"])
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint"""
|
"""Root endpoint"""
|
||||||
return {
|
return {
|
||||||
@@ -130,6 +125,30 @@ async def root():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = POSService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app(
|
||||||
|
docs_url="/docs" if settings.ENVIRONMENT != "production" else None,
|
||||||
|
redoc_url="/redoc" if settings.ENVIRONMENT != "production" else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom middleware
|
||||||
|
service.setup_custom_middleware()
|
||||||
|
|
||||||
|
# Setup custom endpoints
|
||||||
|
service.setup_custom_endpoints()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(pos_config.router, tags=["pos-config"])
|
||||||
|
service.add_router(webhooks.router, tags=["webhooks"])
|
||||||
|
service.add_router(sync.router, tags=["sync"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
@@ -6,124 +6,86 @@ Production Service - FastAPI Application
|
|||||||
Production planning and batch management service
|
Production planning and batch management service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_database, get_db_health
|
from app.core.database import database_manager
|
||||||
from app.api.production import router as production_router
|
from app.api.production import router as production_router
|
||||||
from app.services.production_alert_service import ProductionAlertService
|
from app.services.production_alert_service import ProductionAlertService
|
||||||
|
from shared.service_base import StandardFastAPIService
|
||||||
# Configure logging
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
class ProductionService(StandardFastAPIService):
|
||||||
async def lifespan(app: FastAPI):
|
"""Production Service with standardized setup"""
|
||||||
"""Manage application lifespan events"""
|
|
||||||
# Startup
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
production_expected_tables = [
|
||||||
|
'production_batches', 'production_schedules', 'production_capacity',
|
||||||
|
'quality_check_templates', 'quality_checks', 'equipment'
|
||||||
|
]
|
||||||
|
|
||||||
|
self.alert_service = None
|
||||||
|
|
||||||
|
# Create custom checks for alert service
|
||||||
|
async def check_alert_service():
|
||||||
|
"""Check production alert service health"""
|
||||||
try:
|
try:
|
||||||
await init_database()
|
return bool(self.alert_service) if self.alert_service else False
|
||||||
logger.info("Database initialized")
|
|
||||||
|
|
||||||
# Initialize alert service
|
|
||||||
alert_service = ProductionAlertService(settings)
|
|
||||||
await alert_service.start()
|
|
||||||
logger.info("Production alert service started")
|
|
||||||
|
|
||||||
# Store alert service in app state
|
|
||||||
app.state.alert_service = alert_service
|
|
||||||
|
|
||||||
logger.info("Production service started successfully")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to initialize production service", error=str(e))
|
self.logger.error("Alert service health check failed", error=str(e))
|
||||||
raise
|
return False
|
||||||
|
|
||||||
yield
|
super().__init__(
|
||||||
|
service_name=settings.SERVICE_NAME,
|
||||||
# Shutdown
|
app_name=settings.APP_NAME,
|
||||||
logger.info("Production service shutting down")
|
|
||||||
try:
|
|
||||||
# Stop alert service
|
|
||||||
if hasattr(app.state, 'alert_service'):
|
|
||||||
await app.state.alert_service.stop()
|
|
||||||
logger.info("Alert service stopped")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error during shutdown", error=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI application
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.APP_NAME,
|
|
||||||
description=settings.DESCRIPTION,
|
description=settings.DESCRIPTION,
|
||||||
version=settings.VERSION,
|
version=settings.VERSION,
|
||||||
lifespan=lifespan
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=production_expected_tables,
|
||||||
|
custom_health_checks={"alert_service": check_alert_service}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add CORS middleware
|
async def on_startup(self, app: FastAPI):
|
||||||
app.add_middleware(
|
"""Custom startup logic for production service"""
|
||||||
CORSMiddleware,
|
# Initialize alert service
|
||||||
allow_origins=["*"], # Configure based on environment
|
self.alert_service = ProductionAlertService(settings)
|
||||||
allow_credentials=True,
|
await self.alert_service.start()
|
||||||
allow_methods=["*"],
|
self.logger.info("Production alert service started")
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
# Store alert service in app state
|
||||||
app.include_router(production_router, prefix="/api/v1")
|
app.state.alert_service = self.alert_service
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for production service"""
|
||||||
|
# Stop alert service
|
||||||
|
if self.alert_service:
|
||||||
|
await self.alert_service.stop()
|
||||||
|
self.logger.info("Alert service stopped")
|
||||||
|
|
||||||
@app.get("/health")
|
def get_service_features(self):
|
||||||
async def health_check():
|
"""Return production-specific features"""
|
||||||
"""Health check endpoint"""
|
return [
|
||||||
try:
|
"production_planning",
|
||||||
db_healthy = await get_db_health()
|
"batch_management",
|
||||||
|
"production_scheduling",
|
||||||
|
"quality_control",
|
||||||
|
"equipment_management",
|
||||||
|
"capacity_planning",
|
||||||
|
"alert_notifications"
|
||||||
|
]
|
||||||
|
|
||||||
health_status = {
|
def setup_custom_middleware(self):
|
||||||
"status": "healthy" if db_healthy else "unhealthy",
|
"""Setup custom middleware for production service"""
|
||||||
"service": settings.SERVICE_NAME,
|
@self.app.middleware("http")
|
||||||
"version": settings.VERSION,
|
|
||||||
"database": "connected" if db_healthy else "disconnected"
|
|
||||||
}
|
|
||||||
|
|
||||||
if not db_healthy:
|
|
||||||
health_status["status"] = "unhealthy"
|
|
||||||
|
|
||||||
return health_status
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Health check failed", error=str(e))
|
|
||||||
return {
|
|
||||||
"status": "unhealthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
|
||||||
return {
|
|
||||||
"service": settings.APP_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"description": settings.DESCRIPTION,
|
|
||||||
"status": "running"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def logging_middleware(request: Request, call_next):
|
async def logging_middleware(request: Request, call_next):
|
||||||
"""Add request logging middleware"""
|
"""Add request logging middleware"""
|
||||||
import time
|
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
process_time = time.time() - start_time
|
process_time = time.time() - start_time
|
||||||
|
|
||||||
logger.info("HTTP request processed",
|
self.logger.info("HTTP request processed",
|
||||||
method=request.method,
|
method=request.method,
|
||||||
url=str(request.url),
|
url=str(request.url),
|
||||||
status_code=response.status_code,
|
status_code=response.status_code,
|
||||||
@@ -132,6 +94,22 @@ async def logging_middleware(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = ProductionService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app()
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom middleware
|
||||||
|
service.setup_custom_middleware()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(production_router)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
@@ -4,72 +4,67 @@ Recipe Service - FastAPI application
|
|||||||
Handles recipe management, production planning, and inventory consumption tracking
|
Handles recipe management, production planning, and inventory consumption tracking
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import time
|
import time
|
||||||
import logging
|
from fastapi import FastAPI, Request
|
||||||
from contextlib import asynccontextmanager
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
from .core.config import settings
|
from .core.config import settings
|
||||||
from .core.database import db_manager
|
from .core.database import db_manager
|
||||||
from .api import recipes
|
from .api import recipes
|
||||||
|
from shared.service_base import StandardFastAPIService
|
||||||
# Import models to register them with SQLAlchemy metadata
|
# Import models to register them with SQLAlchemy metadata
|
||||||
from .models import recipes as recipe_models
|
from .models import recipes as recipe_models
|
||||||
|
|
||||||
|
|
||||||
# Configure logging
|
class RecipesService(StandardFastAPIService):
|
||||||
logging.basicConfig(
|
"""Recipes Service with standardized setup"""
|
||||||
level=getattr(logging, settings.LOG_LEVEL.upper()),
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
recipes_expected_tables = [
|
||||||
|
'recipes', 'recipe_ingredients', 'production_batches',
|
||||||
|
'production_ingredient_consumption', 'production_schedules'
|
||||||
|
]
|
||||||
|
|
||||||
@asynccontextmanager
|
super().__init__(
|
||||||
async def lifespan(app: FastAPI):
|
service_name="recipes-service",
|
||||||
"""Application lifespan events"""
|
app_name="Recipe Management Service",
|
||||||
# Startup
|
|
||||||
logger.info(f"Starting {settings.SERVICE_NAME} service v{settings.SERVICE_VERSION}")
|
|
||||||
|
|
||||||
# Create database tables
|
|
||||||
try:
|
|
||||||
await db_manager.create_tables()
|
|
||||||
logger.info("Database tables created successfully")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to create database tables: {e}")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info(f"Shutting down {settings.SERVICE_NAME} service")
|
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI application
|
|
||||||
app = FastAPI(
|
|
||||||
title="Recipe Management Service",
|
|
||||||
description="Comprehensive recipe management, production planning, and inventory consumption tracking for bakery operations",
|
description="Comprehensive recipe management, production planning, and inventory consumption tracking for bakery operations",
|
||||||
version=settings.SERVICE_VERSION,
|
version=settings.SERVICE_VERSION,
|
||||||
lifespan=lifespan,
|
log_level=settings.LOG_LEVEL,
|
||||||
docs_url="/docs" if settings.DEBUG else None,
|
cors_origins=settings.ALLOWED_ORIGINS,
|
||||||
redoc_url="/redoc" if settings.DEBUG else None,
|
api_prefix=settings.API_V1_PREFIX,
|
||||||
|
database_manager=db_manager,
|
||||||
|
expected_tables=recipes_expected_tables
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add middleware
|
async def on_startup(self, app: FastAPI):
|
||||||
app.add_middleware(
|
"""Custom startup logic for recipes service"""
|
||||||
CORSMiddleware,
|
# Custom startup completed
|
||||||
allow_origins=settings.ALLOWED_ORIGINS,
|
pass
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for recipes service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return recipes-specific features"""
|
||||||
|
return [
|
||||||
|
"recipe_management",
|
||||||
|
"production_planning",
|
||||||
|
"inventory_consumption_tracking",
|
||||||
|
"batch_production",
|
||||||
|
"tenant_scoped_operations"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_middleware(self):
|
||||||
|
"""Setup custom middleware for recipes service"""
|
||||||
|
# Add GZip middleware
|
||||||
|
self.app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
|
|
||||||
# Request timing middleware
|
# Request timing middleware
|
||||||
@app.middleware("http")
|
@self.app.middleware("http")
|
||||||
async def add_process_time_header(request: Request, call_next):
|
async def add_process_time_header(request: Request, call_next):
|
||||||
"""Add processing time header to responses"""
|
"""Add processing time header to responses"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -79,47 +74,20 @@ async def add_process_time_header(request: Request, call_next):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Global exception handler
|
# Create service instance
|
||||||
@app.exception_handler(Exception)
|
service = RecipesService()
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Global exception handler"""
|
# Create FastAPI app with standardized setup
|
||||||
logger.error(f"Global exception on {request.url}: {exc}", exc_info=True)
|
app = service.create_app(
|
||||||
return JSONResponse(
|
docs_url="/docs" if settings.DEBUG else None,
|
||||||
status_code=500,
|
redoc_url="/redoc" if settings.DEBUG else None
|
||||||
content={
|
|
||||||
"detail": "Internal server error",
|
|
||||||
"error": str(exc) if settings.DEBUG else "An unexpected error occurred"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
# Health check endpoint
|
# Setup custom middleware
|
||||||
@app.get("/health")
|
service.setup_custom_middleware()
|
||||||
async def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
try:
|
|
||||||
# Test database connection
|
|
||||||
health_result = await db_manager.health_check()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.SERVICE_VERSION,
|
|
||||||
"environment": settings.ENVIRONMENT,
|
|
||||||
"database": health_result
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Health check failed: {e}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=503,
|
|
||||||
content={
|
|
||||||
"status": "unhealthy",
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.SERVICE_VERSION,
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Include API routers with tenant-scoped paths
|
# Include API routers with tenant-scoped paths
|
||||||
app.include_router(
|
app.include_router(
|
||||||
@@ -129,19 +97,6 @@ app.include_router(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
|
||||||
return {
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.SERVICE_VERSION,
|
|
||||||
"status": "running",
|
|
||||||
"docs_url": "/docs" if settings.DEBUG else None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ requests==2.31.0
|
|||||||
asyncio-mqtt==0.16.1
|
asyncio-mqtt==0.16.1
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
|
|
||||||
|
# Messaging
|
||||||
|
aio-pika==9.3.1
|
||||||
|
|
||||||
# Caching (optional)
|
# Caching (optional)
|
||||||
redis==5.0.1
|
redis==5.0.1
|
||||||
python-redis-cache==0.1.0
|
python-redis-cache==0.1.0
|
||||||
@@ -31,6 +34,7 @@ python-redis-cache==0.1.0
|
|||||||
# Monitoring and logging
|
# Monitoring and logging
|
||||||
structlog==23.2.0
|
structlog==23.2.0
|
||||||
python-json-logger==2.0.4
|
python-json-logger==2.0.4
|
||||||
|
prometheus-client==0.19.0
|
||||||
|
|
||||||
# Date/time handling
|
# Date/time handling
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
|||||||
@@ -3,125 +3,98 @@
|
|||||||
Sales Service Main Application
|
Sales Service Main Application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
from fastapi import FastAPI
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db, close_db
|
from app.core.database import database_manager
|
||||||
from shared.monitoring import setup_logging, HealthChecker
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
|
||||||
|
|
||||||
# Setup logging first
|
|
||||||
setup_logging("sales-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Global variables for lifespan access
|
|
||||||
metrics_collector = None
|
|
||||||
health_checker = None
|
|
||||||
|
|
||||||
# Create FastAPI app FIRST
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bakery Sales Service",
|
|
||||||
description="Sales data management service for bakery operations",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup metrics BEFORE any middleware and BEFORE lifespan
|
|
||||||
metrics_collector = setup_metrics_early(app, "sales-service")
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Application lifespan events"""
|
|
||||||
global health_checker
|
|
||||||
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Sales Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize database
|
|
||||||
await init_db()
|
|
||||||
logger.info("Database initialized")
|
|
||||||
|
|
||||||
# Register custom metrics
|
|
||||||
metrics_collector.register_counter("sales_records_created_total", "Total sales records created")
|
|
||||||
metrics_collector.register_counter("sales_records_updated_total", "Total sales records updated")
|
|
||||||
metrics_collector.register_counter("sales_queries_total", "Sales record queries")
|
|
||||||
metrics_collector.register_counter("product_queries_total", "Product catalog queries")
|
|
||||||
metrics_collector.register_counter("import_jobs_total", "Data import jobs")
|
|
||||||
metrics_collector.register_counter("export_jobs_total", "Data export jobs")
|
|
||||||
|
|
||||||
metrics_collector.register_histogram("sales_create_duration_seconds", "Sales record creation duration")
|
|
||||||
metrics_collector.register_histogram("sales_query_duration_seconds", "Sales query duration")
|
|
||||||
metrics_collector.register_histogram("import_processing_duration_seconds", "Import processing duration")
|
|
||||||
metrics_collector.register_histogram("export_generation_duration_seconds", "Export generation duration")
|
|
||||||
|
|
||||||
# Setup health checker
|
|
||||||
health_checker = HealthChecker("sales-service")
|
|
||||||
|
|
||||||
# Add database health check
|
|
||||||
async def check_database():
|
|
||||||
try:
|
|
||||||
from app.core.database import get_db
|
|
||||||
from sqlalchemy import text
|
|
||||||
async for db in get_db():
|
|
||||||
await db.execute(text("SELECT 1"))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
return f"Database error: {e}"
|
|
||||||
|
|
||||||
health_checker.add_check("database", check_database, timeout=5.0, critical=True)
|
|
||||||
|
|
||||||
# Store health checker in app state
|
|
||||||
app.state.health_checker = health_checker
|
|
||||||
|
|
||||||
logger.info("Sales Service started successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to start Sales Service: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Sales Service...")
|
|
||||||
await close_db()
|
|
||||||
|
|
||||||
# Set lifespan AFTER metrics setup
|
|
||||||
app.router.lifespan_context = lifespan
|
|
||||||
|
|
||||||
# CORS middleware (added after metrics setup)
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers - import router BEFORE sales router to avoid conflicts
|
# Include routers - import router BEFORE sales router to avoid conflicts
|
||||||
from app.api.sales import router as sales_router
|
from app.api.sales import router as sales_router
|
||||||
from app.api.import_data import router as import_router
|
from app.api.import_data import router as import_router
|
||||||
app.include_router(import_router, prefix="/api/v1", tags=["import"])
|
|
||||||
app.include_router(sales_router, prefix="/api/v1", tags=["sales"])
|
|
||||||
|
|
||||||
# Health check endpoint
|
|
||||||
@app.get("/health")
|
class SalesService(StandardFastAPIService):
|
||||||
async def health_check():
|
"""Sales Service with standardized setup"""
|
||||||
"""Comprehensive health check endpoint"""
|
|
||||||
if health_checker:
|
def __init__(self):
|
||||||
return await health_checker.check_health()
|
# Define expected database tables for health checks
|
||||||
else:
|
sales_expected_tables = ['sales_data', 'sales_import_jobs']
|
||||||
return {
|
|
||||||
"service": "sales-service",
|
super().__init__(
|
||||||
"status": "healthy",
|
service_name="sales-service",
|
||||||
"version": "1.0.0"
|
app_name="Bakery Sales Service",
|
||||||
|
description="Sales data management service for bakery operations",
|
||||||
|
version="1.0.0",
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
cors_origins=settings.CORS_ORIGINS,
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=sales_expected_tables
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for sales service"""
|
||||||
|
# Register custom metrics
|
||||||
|
self.register_custom_metrics({
|
||||||
|
"sales_records_created_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total sales records created"
|
||||||
|
},
|
||||||
|
"sales_records_updated_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total sales records updated"
|
||||||
|
},
|
||||||
|
"sales_queries_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Sales record queries"
|
||||||
|
},
|
||||||
|
"product_queries_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Product catalog queries"
|
||||||
|
},
|
||||||
|
"import_jobs_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Data import jobs"
|
||||||
|
},
|
||||||
|
"export_jobs_total": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Data export jobs"
|
||||||
|
},
|
||||||
|
"sales_create_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Sales record creation duration"
|
||||||
|
},
|
||||||
|
"sales_query_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Sales query duration"
|
||||||
|
},
|
||||||
|
"import_processing_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Import processing duration"
|
||||||
|
},
|
||||||
|
"export_generation_duration_seconds": {
|
||||||
|
"type": "histogram",
|
||||||
|
"description": "Export generation duration"
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
# Root endpoint
|
async def on_shutdown(self, app: FastAPI):
|
||||||
@app.get("/")
|
"""Custom shutdown logic for sales service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return sales-specific features"""
|
||||||
|
return [
|
||||||
|
"sales_data_management",
|
||||||
|
"product_catalog",
|
||||||
|
"data_import_export",
|
||||||
|
"sales_analytics",
|
||||||
|
"performance_tracking"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_endpoints(self):
|
||||||
|
"""Setup custom endpoints for sales service"""
|
||||||
|
@self.app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint"""
|
"""Root endpoint"""
|
||||||
return {
|
return {
|
||||||
@@ -136,17 +109,19 @@ async def root():
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Global exception handler with metrics"""
|
|
||||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
|
||||||
|
|
||||||
# Record error metric if available
|
# Create service instance
|
||||||
if metrics_collector:
|
service = SalesService()
|
||||||
metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
|
|
||||||
|
|
||||||
return JSONResponse(
|
# Create FastAPI app with standardized setup
|
||||||
status_code=500,
|
app = service.create_app()
|
||||||
content={"detail": "Internal server error"}
|
|
||||||
)
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom endpoints
|
||||||
|
service.setup_custom_endpoints()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(import_router, tags=["import"])
|
||||||
|
service.add_router(sales_router, tags=["sales"])
|
||||||
@@ -4,151 +4,51 @@ Supplier & Procurement Service FastAPI Application
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from contextlib import asynccontextmanager
|
from fastapi import FastAPI
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import structlog
|
|
||||||
|
|
||||||
# Import core modules
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db, close_db
|
from app.core.database import database_manager
|
||||||
from app.api import suppliers, purchase_orders, deliveries
|
from app.api import suppliers, purchase_orders, deliveries
|
||||||
from shared.monitoring.health import router as health_router
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import setup_metrics_early
|
|
||||||
# from shared.auth.decorators import setup_auth_middleware
|
|
||||||
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Application lifespan management"""
|
|
||||||
# Startup
|
|
||||||
logger.info("Starting Supplier Service", version=settings.VERSION)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize database
|
|
||||||
await init_db()
|
|
||||||
logger.info("Database initialized successfully")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Startup failed", error=str(e))
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Supplier Service")
|
|
||||||
try:
|
|
||||||
await close_db()
|
|
||||||
logger.info("Database connections closed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Shutdown error", error=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI application
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.APP_NAME,
|
|
||||||
description=settings.DESCRIPTION,
|
|
||||||
version=settings.VERSION,
|
|
||||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
|
||||||
docs_url=f"{settings.API_V1_STR}/docs",
|
|
||||||
redoc_url=f"{settings.API_V1_STR}/redoc",
|
|
||||||
lifespan=lifespan
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# CORS middleware
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup metrics
|
|
||||||
try:
|
|
||||||
setup_metrics_early(app, "suppliers-service")
|
|
||||||
logger.info("Metrics setup completed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Metrics setup failed", error=str(e))
|
|
||||||
|
|
||||||
# Setup authentication middleware (commented out - not implemented)
|
|
||||||
# setup_auth_middleware(app)
|
|
||||||
|
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
@app.exception_handler(ValueError)
|
|
||||||
async def value_error_handler(request: Request, exc: ValueError):
|
|
||||||
"""Handle validation errors"""
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=400,
|
|
||||||
content={
|
|
||||||
"error": "Validation Error",
|
|
||||||
"detail": str(exc),
|
|
||||||
"type": "value_error"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
|
||||||
async def general_exception_handler(request: Request, exc: Exception):
|
|
||||||
"""Handle general exceptions"""
|
|
||||||
logger.error(
|
|
||||||
"Unhandled exception",
|
|
||||||
error=str(exc),
|
|
||||||
path=request.url.path,
|
|
||||||
method=request.method
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={
|
|
||||||
"error": "Internal Server Error",
|
|
||||||
"detail": "An unexpected error occurred",
|
|
||||||
"type": "internal_error"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(health_router, prefix="/health", tags=["health"])
|
|
||||||
app.include_router(suppliers.router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(purchase_orders.router, prefix=settings.API_V1_STR)
|
|
||||||
app.include_router(deliveries.router, prefix=settings.API_V1_STR)
|
|
||||||
|
|
||||||
# Include enhanced performance tracking router
|
# Include enhanced performance tracking router
|
||||||
from app.api.performance import router as performance_router
|
from app.api.performance import router as performance_router
|
||||||
app.include_router(performance_router, prefix=settings.API_V1_STR)
|
|
||||||
|
|
||||||
|
|
||||||
# Root endpoint
|
class SuppliersService(StandardFastAPIService):
|
||||||
@app.get("/")
|
"""Suppliers Service with standardized setup"""
|
||||||
async def root():
|
|
||||||
"""Root endpoint with service information"""
|
|
||||||
return {
|
|
||||||
"service": settings.SERVICE_NAME,
|
|
||||||
"version": settings.VERSION,
|
|
||||||
"description": settings.DESCRIPTION,
|
|
||||||
"status": "running",
|
|
||||||
"docs_url": f"{settings.API_V1_STR}/docs",
|
|
||||||
"health_url": "/health"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
suppliers_expected_tables = [
|
||||||
|
'suppliers', 'supplier_price_lists', 'purchase_orders', 'purchase_order_items',
|
||||||
|
'deliveries', 'delivery_items', 'supplier_quality_reviews', 'supplier_invoices',
|
||||||
|
'supplier_performance_metrics', 'supplier_alerts', 'supplier_scorecards',
|
||||||
|
'supplier_benchmarks', 'alert_rules'
|
||||||
|
]
|
||||||
|
|
||||||
# Service info endpoint
|
super().__init__(
|
||||||
@app.get(f"{settings.API_V1_STR}/info")
|
service_name="suppliers-service",
|
||||||
async def service_info():
|
app_name=settings.APP_NAME,
|
||||||
"""Service information endpoint"""
|
description=settings.DESCRIPTION,
|
||||||
return {
|
version=settings.VERSION,
|
||||||
"service": settings.SERVICE_NAME,
|
cors_origins=settings.CORS_ORIGINS,
|
||||||
"version": settings.VERSION,
|
api_prefix=settings.API_V1_STR,
|
||||||
"description": settings.DESCRIPTION,
|
database_manager=database_manager,
|
||||||
"api_version": "v1",
|
expected_tables=suppliers_expected_tables
|
||||||
"environment": settings.ENVIRONMENT,
|
)
|
||||||
"features": [
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for suppliers service"""
|
||||||
|
# Custom startup completed
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for suppliers service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return suppliers-specific features"""
|
||||||
|
return [
|
||||||
"supplier_management",
|
"supplier_management",
|
||||||
"vendor_onboarding",
|
"vendor_onboarding",
|
||||||
"purchase_orders",
|
"purchase_orders",
|
||||||
@@ -168,7 +68,22 @@ async def service_info():
|
|||||||
"risk_assessment",
|
"risk_assessment",
|
||||||
"benchmarking"
|
"benchmarking"
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = SuppliersService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app()
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
service.add_router(suppliers.router)
|
||||||
|
service.add_router(purchase_orders.router)
|
||||||
|
service.add_router(deliveries.router)
|
||||||
|
service.add_router(performance_router)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,97 +1,83 @@
|
|||||||
# services/tenant/app/main.py
|
# services/tenant/app/main.py
|
||||||
"""
|
"""
|
||||||
Tenant Service FastAPI application - FIXED VERSION
|
Tenant Service FastAPI application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import database_manager
|
from app.core.database import database_manager
|
||||||
from app.api import tenants, subscriptions, webhooks
|
from app.api import tenants, subscriptions, webhooks
|
||||||
from shared.monitoring.logging import setup_logging
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import MetricsCollector
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
setup_logging("tenant-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Create FastAPI app
|
class TenantService(StandardFastAPIService):
|
||||||
app = FastAPI(
|
"""Tenant Service with standardized setup"""
|
||||||
title="Tenant Management Service",
|
|
||||||
|
def __init__(self):
|
||||||
|
# Define expected database tables for health checks
|
||||||
|
tenant_expected_tables = ['tenants', 'tenant_members', 'subscriptions']
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
service_name="tenant-service",
|
||||||
|
app_name="Tenant Management Service",
|
||||||
description="Multi-tenant bakery management service",
|
description="Multi-tenant bakery management service",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=tenant_expected_tables
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for tenant service"""
|
||||||
|
# Import models to ensure they're registered with SQLAlchemy
|
||||||
|
from app.models.tenants import Tenant, TenantMember, Subscription
|
||||||
|
self.logger.info("Tenant models imported successfully")
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for tenant service"""
|
||||||
|
# Database cleanup is handled by the base class
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return tenant-specific features"""
|
||||||
|
return [
|
||||||
|
"multi_tenant_management",
|
||||||
|
"subscription_management",
|
||||||
|
"tenant_isolation",
|
||||||
|
"webhook_notifications",
|
||||||
|
"member_management"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_endpoints(self):
|
||||||
|
"""Setup custom endpoints for tenant service"""
|
||||||
|
@self.app.get("/metrics")
|
||||||
|
async def metrics():
|
||||||
|
"""Prometheus metrics endpoint"""
|
||||||
|
if self.metrics_collector:
|
||||||
|
return self.metrics_collector.get_metrics()
|
||||||
|
return {"metrics": "not_available"}
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = TenantService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app(
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc"
|
redoc_url="/redoc"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize metrics
|
# Setup standard endpoints
|
||||||
metrics_collector = MetricsCollector("tenant_service")
|
service.setup_standard_endpoints()
|
||||||
app.state.metrics_collector = metrics_collector
|
|
||||||
|
|
||||||
# CORS middleware
|
# Setup custom endpoints
|
||||||
app.add_middleware(
|
service.setup_custom_endpoints()
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(tenants.router, prefix="/api/v1", tags=["tenants"])
|
service.add_router(tenants.router, tags=["tenants"])
|
||||||
app.include_router(subscriptions.router, prefix="/api/v1", tags=["subscriptions"])
|
service.add_router(subscriptions.router, tags=["subscriptions"])
|
||||||
app.include_router(webhooks.router, prefix="/api/v1", tags=["webhooks"])
|
service.add_router(webhooks.router, tags=["webhooks"])
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
"""Initialize service on startup"""
|
|
||||||
logger.info("Starting Tenant Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# ✅ FIX: Import models to ensure they're registered with SQLAlchemy
|
|
||||||
from app.models.tenants import Tenant, TenantMember, Subscription
|
|
||||||
logger.info("Tenant models imported successfully")
|
|
||||||
|
|
||||||
# ✅ FIX: Create database tables on startup
|
|
||||||
await database_manager.create_tables()
|
|
||||||
logger.info("Tenant database tables created successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to initialize tenant service: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.info("Tenant Service startup completed successfully")
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
|
||||||
"""Cleanup on shutdown"""
|
|
||||||
logger.info("Shutting down Tenant Service...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Close database connections properly
|
|
||||||
if hasattr(database_manager, 'engine') and database_manager.engine:
|
|
||||||
await database_manager.engine.dispose()
|
|
||||||
logger.info("Database connections closed")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error during shutdown: {e}")
|
|
||||||
|
|
||||||
logger.info("Tenant Service shutdown completed")
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "tenant-service",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.get("/metrics")
|
|
||||||
async def metrics():
|
|
||||||
"""Prometheus metrics endpoint"""
|
|
||||||
return metrics_collector.get_metrics()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
@@ -1,118 +1,80 @@
|
|||||||
# ================================================================
|
# ================================================================
|
||||||
# services/training/app/main.py - FIXED VERSION
|
# services/training/app/main.py
|
||||||
# ================================================================
|
# ================================================================
|
||||||
"""
|
"""
|
||||||
Training Service Main Application
|
Training Service Main Application
|
||||||
Enhanced with proper error handling, monitoring, and lifecycle management
|
ML training service for bakery demand forecasting
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import structlog
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import initialize_training_database, cleanup_training_database, get_db_health, get_comprehensive_db_health
|
from app.core.database import initialize_training_database, cleanup_training_database, database_manager
|
||||||
from app.api import training, models
|
from app.api import training, models
|
||||||
|
|
||||||
from app.api.websocket import websocket_router
|
from app.api.websocket import websocket_router
|
||||||
from app.services.messaging import setup_messaging, cleanup_messaging
|
from app.services.messaging import setup_messaging, cleanup_messaging
|
||||||
from shared.monitoring.logging import setup_logging
|
from shared.service_base import StandardFastAPIService
|
||||||
from shared.monitoring.metrics import MetricsCollector
|
|
||||||
# REMOVED: from shared.auth.decorators import require_auth
|
|
||||||
|
|
||||||
# Setup structured logging
|
|
||||||
setup_logging("training-service", settings.LOG_LEVEL)
|
|
||||||
logger = structlog.get_logger()
|
|
||||||
|
|
||||||
# Initialize metrics collector
|
class TrainingService(StandardFastAPIService):
|
||||||
metrics_collector = MetricsCollector("training-service")
|
"""Training Service with standardized setup"""
|
||||||
|
|
||||||
@asynccontextmanager
|
def __init__(self):
|
||||||
async def lifespan(app: FastAPI):
|
# Define expected database tables for health checks
|
||||||
"""
|
training_expected_tables = [
|
||||||
Application lifespan manager for startup and shutdown events
|
'model_training_logs', 'trained_models', 'model_performance_metrics',
|
||||||
"""
|
'training_job_queue', 'model_artifacts'
|
||||||
# Startup
|
]
|
||||||
logger.info("Starting Training Service", version="1.0.0")
|
|
||||||
|
|
||||||
try:
|
super().__init__(
|
||||||
# Initialize database
|
service_name="training-service",
|
||||||
logger.info("Initializing database connection")
|
app_name="Bakery Training Service",
|
||||||
await initialize_training_database()
|
|
||||||
logger.info("Database initialized successfully")
|
|
||||||
|
|
||||||
# Initialize messaging
|
|
||||||
logger.info("Setting up messaging")
|
|
||||||
await setup_messaging()
|
|
||||||
logger.info("Messaging setup completed")
|
|
||||||
|
|
||||||
# Start metrics server
|
|
||||||
logger.info("Starting metrics server")
|
|
||||||
metrics_collector.start_metrics_server(8080)
|
|
||||||
logger.info("Metrics server started on port 8080")
|
|
||||||
|
|
||||||
# Store metrics collector in app state
|
|
||||||
app.state.metrics_collector = metrics_collector
|
|
||||||
|
|
||||||
# Mark service as ready
|
|
||||||
app.state.ready = True
|
|
||||||
logger.info("Training Service startup completed successfully")
|
|
||||||
|
|
||||||
yield
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to start Training Service", error=str(e))
|
|
||||||
app.state.ready = False
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Shutdown
|
|
||||||
logger.info("Shutting down Training Service")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Stop metrics server
|
|
||||||
if hasattr(app.state, 'metrics_collector'):
|
|
||||||
await app.state.metrics_collector.shutdown()
|
|
||||||
|
|
||||||
# Cleanup messaging
|
|
||||||
await cleanup_messaging()
|
|
||||||
logger.info("Messaging cleanup completed")
|
|
||||||
|
|
||||||
# Close database connections
|
|
||||||
await cleanup_training_database()
|
|
||||||
logger.info("Database connections closed")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error during shutdown", error=str(e))
|
|
||||||
|
|
||||||
logger.info("Training Service shutdown completed")
|
|
||||||
|
|
||||||
# Create FastAPI application with lifespan
|
|
||||||
app = FastAPI(
|
|
||||||
title="Bakery Training Service",
|
|
||||||
description="ML training service for bakery demand forecasting",
|
description="ML training service for bakery demand forecasting",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
docs_url="/docs",
|
log_level=settings.LOG_LEVEL,
|
||||||
redoc_url="/redoc",
|
cors_origins=settings.CORS_ORIGINS_LIST,
|
||||||
lifespan=lifespan
|
api_prefix="/api/v1",
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=training_expected_tables,
|
||||||
|
enable_messaging=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add middleware
|
async def _setup_messaging(self):
|
||||||
app.add_middleware(
|
"""Setup messaging for training service"""
|
||||||
CORSMiddleware,
|
await setup_messaging()
|
||||||
allow_origins=settings.CORS_ORIGINS_LIST,
|
self.logger.info("Messaging setup completed")
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging for training service"""
|
||||||
|
await cleanup_messaging()
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Custom startup logic for training service"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Custom shutdown logic for training service"""
|
||||||
|
# Note: Database cleanup is handled by the base class
|
||||||
|
# but training service has custom cleanup function
|
||||||
|
await cleanup_training_database()
|
||||||
|
self.logger.info("Training database cleanup completed")
|
||||||
|
|
||||||
|
def get_service_features(self):
|
||||||
|
"""Return training-specific features"""
|
||||||
|
return [
|
||||||
|
"ml_model_training",
|
||||||
|
"demand_forecasting",
|
||||||
|
"model_performance_tracking",
|
||||||
|
"training_job_queue",
|
||||||
|
"model_artifacts_management",
|
||||||
|
"websocket_support",
|
||||||
|
"messaging_integration"
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup_custom_middleware(self):
|
||||||
|
"""Setup custom middleware for training service"""
|
||||||
# Request middleware for logging and metrics
|
# Request middleware for logging and metrics
|
||||||
@app.middleware("http")
|
@self.app.middleware("http")
|
||||||
async def process_request(request: Request, call_next):
|
async def process_request(request: Request, call_next):
|
||||||
"""Process requests with logging and metrics"""
|
"""Process requests with logging and metrics"""
|
||||||
start_time = asyncio.get_event_loop().time()
|
start_time = asyncio.get_event_loop().time()
|
||||||
@@ -121,7 +83,7 @@ async def process_request(request: Request, call_next):
|
|||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
duration = asyncio.get_event_loop().time() - start_time
|
duration = asyncio.get_event_loop().time() - start_time
|
||||||
|
|
||||||
logger.info(
|
self.logger.info(
|
||||||
"Request completed",
|
"Request completed",
|
||||||
method=request.method,
|
method=request.method,
|
||||||
path=request.url.path,
|
path=request.url.path,
|
||||||
@@ -129,152 +91,57 @@ async def process_request(request: Request, call_next):
|
|||||||
duration_ms=round(duration * 1000, 2)
|
duration_ms=round(duration * 1000, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update metrics
|
|
||||||
metrics_collector.record_request(
|
|
||||||
method=request.method,
|
|
||||||
endpoint=request.url.path,
|
|
||||||
status_code=response.status_code,
|
|
||||||
duration=duration
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
duration = asyncio.get_event_loop().time() - start_time
|
duration = asyncio.get_event_loop().time() - start_time
|
||||||
|
|
||||||
logger.error(
|
self.logger.error(
|
||||||
"Request failed",
|
"Request failed",
|
||||||
method=request.method,
|
method=request.method,
|
||||||
path=request.url.path,
|
path=request.url.path,
|
||||||
error=str(e),
|
error=str(e),
|
||||||
duration_ms=round(duration * 1000, 2)
|
duration_ms=round(duration * 1000, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics_collector.increment_counter("http_requests_failed_total")
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Exception handlers
|
def setup_custom_endpoints(self):
|
||||||
@app.exception_handler(Exception)
|
"""Setup custom endpoints for training service"""
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
@self.app.get("/metrics")
|
||||||
"""Global exception handler for unhandled errors"""
|
|
||||||
logger.error(
|
|
||||||
"Unhandled exception",
|
|
||||||
path=request.url.path,
|
|
||||||
method=request.method,
|
|
||||||
error=str(exc),
|
|
||||||
exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
metrics_collector.increment_counter("unhandled_exceptions_total")
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={
|
|
||||||
"detail": "Internal server error",
|
|
||||||
"error_id": structlog.get_logger().new().info("Error logged", error=str(exc))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include API routers
|
|
||||||
app.include_router(training.router, prefix="/api/v1", tags=["training"])
|
|
||||||
|
|
||||||
app.include_router(models.router, prefix="/api/v1", tags=["models"])
|
|
||||||
app.include_router(websocket_router, prefix="/api/v1/ws", tags=["websocket"])
|
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoints
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Basic health check endpoint"""
|
|
||||||
return {
|
|
||||||
"status": "healthy" if app.state.ready else "starting",
|
|
||||||
"service": "training-service",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"timestamp": structlog.get_logger().new().info("Health check")
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.get("/health/ready")
|
|
||||||
async def readiness_check():
|
|
||||||
"""Kubernetes readiness probe endpoint with comprehensive database checks"""
|
|
||||||
try:
|
|
||||||
# Get comprehensive database health including table verification
|
|
||||||
db_health = await get_comprehensive_db_health()
|
|
||||||
|
|
||||||
checks = {
|
|
||||||
"database_connectivity": db_health["connectivity"],
|
|
||||||
"database_tables": db_health["tables_exist"],
|
|
||||||
"application": getattr(app.state, 'ready', False)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include detailed database info for debugging
|
|
||||||
database_details = {
|
|
||||||
"status": db_health["status"],
|
|
||||||
"tables_verified": db_health["tables_verified"],
|
|
||||||
"missing_tables": db_health["missing_tables"],
|
|
||||||
"errors": db_health["errors"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Service is ready only if all checks pass
|
|
||||||
all_ready = all(checks.values()) and db_health["status"] == "healthy"
|
|
||||||
|
|
||||||
if all_ready:
|
|
||||||
return {
|
|
||||||
"status": "ready",
|
|
||||||
"checks": checks,
|
|
||||||
"database": database_details
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=503,
|
|
||||||
content={
|
|
||||||
"status": "not ready",
|
|
||||||
"checks": checks,
|
|
||||||
"database": database_details
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Readiness check failed", error=str(e))
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=503,
|
|
||||||
content={
|
|
||||||
"status": "not ready",
|
|
||||||
"error": f"Health check failed: {str(e)}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/health/database")
|
|
||||||
async def database_health_check():
|
|
||||||
"""Detailed database health endpoint for debugging"""
|
|
||||||
try:
|
|
||||||
db_health = await get_comprehensive_db_health()
|
|
||||||
status_code = 200 if db_health["status"] == "healthy" else 503
|
|
||||||
return JSONResponse(status_code=status_code, content=db_health)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Database health check failed", error=str(e))
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=503,
|
|
||||||
content={
|
|
||||||
"status": "unhealthy",
|
|
||||||
"error": f"Health check failed: {str(e)}"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/metrics")
|
|
||||||
async def get_metrics():
|
async def get_metrics():
|
||||||
"""Prometheus metrics endpoint"""
|
"""Prometheus metrics endpoint"""
|
||||||
if hasattr(app.state, 'metrics_collector'):
|
if self.metrics_collector:
|
||||||
return app.state.metrics_collector.get_metrics()
|
return self.metrics_collector.get_metrics()
|
||||||
return {"status": "metrics not available"}
|
return {"status": "metrics not available"}
|
||||||
|
|
||||||
@app.get("/health/live")
|
@self.app.get("/")
|
||||||
async def liveness_check():
|
|
||||||
return {"status": "alive"}
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
async def root():
|
||||||
return {"service": "training-service", "version": "1.0.0"}
|
return {"service": "training-service", "version": "1.0.0"}
|
||||||
|
|
||||||
|
|
||||||
|
# Create service instance
|
||||||
|
service = TrainingService()
|
||||||
|
|
||||||
|
# Create FastAPI app with standardized setup
|
||||||
|
app = service.create_app(
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup standard endpoints
|
||||||
|
service.setup_standard_endpoints()
|
||||||
|
|
||||||
|
# Setup custom middleware
|
||||||
|
service.setup_custom_middleware()
|
||||||
|
|
||||||
|
# Setup custom endpoints
|
||||||
|
service.setup_custom_endpoints()
|
||||||
|
|
||||||
|
# Include API routers
|
||||||
|
service.add_router(training.router, tags=["training"])
|
||||||
|
service.add_router(models.router, tags=["models"])
|
||||||
|
app.include_router(websocket_router, prefix="/api/v1/ws", tags=["websocket"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"app.main:app",
|
"app.main:app",
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ Shared monitoring package for microservices
|
|||||||
|
|
||||||
from .logging import setup_logging
|
from .logging import setup_logging
|
||||||
from .metrics import setup_metrics_early, get_metrics_collector, MetricsCollector
|
from .metrics import setup_metrics_early, get_metrics_collector, MetricsCollector
|
||||||
from .health import HealthChecker
|
from .health_checks import (
|
||||||
|
HealthCheckManager,
|
||||||
|
FastAPIHealthChecker,
|
||||||
|
create_health_manager,
|
||||||
|
setup_fastapi_health_checks
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'setup_logging',
|
'setup_logging',
|
||||||
'setup_metrics_early',
|
'setup_metrics_early',
|
||||||
'get_metrics_collector',
|
'get_metrics_collector',
|
||||||
'MetricsCollector',
|
'MetricsCollector',
|
||||||
'HealthChecker'
|
'HealthCheckManager',
|
||||||
|
'FastAPIHealthChecker',
|
||||||
|
'create_health_manager',
|
||||||
|
'setup_fastapi_health_checks'
|
||||||
]
|
]
|
||||||
370
shared/monitoring/health_checks.py
Normal file
370
shared/monitoring/health_checks.py
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
"""
|
||||||
|
Enhanced Health Check System for Microservices
|
||||||
|
|
||||||
|
Provides unified health check endpoints and database verification based on
|
||||||
|
the comprehensive implementation from the training service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Any, List, Optional, Callable
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import text, inspect
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import structlog
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from ..database.base import DatabaseManager
|
||||||
|
from ..database.exceptions import DatabaseError, HealthCheckError
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class HealthCheckManager:
|
||||||
|
"""
|
||||||
|
Unified health check manager for microservices
|
||||||
|
|
||||||
|
Provides standardized health check endpoints:
|
||||||
|
- /health - Basic service health
|
||||||
|
- /health/ready - Kubernetes readiness probe with comprehensive checks
|
||||||
|
- /health/live - Kubernetes liveness probe
|
||||||
|
- /health/database - Detailed database health information
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
service_name: str,
|
||||||
|
version: str = "1.0.0",
|
||||||
|
database_manager: Optional[DatabaseManager] = None,
|
||||||
|
expected_tables: Optional[List[str]] = None,
|
||||||
|
custom_checks: Optional[Dict[str, Callable]] = None
|
||||||
|
):
|
||||||
|
self.service_name = service_name
|
||||||
|
self.version = version
|
||||||
|
self.database_manager = database_manager
|
||||||
|
self.expected_tables = expected_tables or []
|
||||||
|
self.custom_checks = custom_checks or {}
|
||||||
|
self.ready_state = False
|
||||||
|
|
||||||
|
def set_ready(self, ready: bool = True):
|
||||||
|
"""Set service ready state"""
|
||||||
|
self.ready_state = ready
|
||||||
|
logger.info(f"Service ready state changed",
|
||||||
|
service=self.service_name, ready=ready)
|
||||||
|
|
||||||
|
async def basic_health_check(self, app_state=None) -> Dict[str, Any]:
|
||||||
|
"""Basic health check endpoint (/health)"""
|
||||||
|
# Check app state for ready status if available
|
||||||
|
ready = self.ready_state
|
||||||
|
if app_state and hasattr(app_state, 'ready'):
|
||||||
|
ready = app_state.ready
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "healthy" if ready else "starting",
|
||||||
|
"service": self.service_name,
|
||||||
|
"version": self.version,
|
||||||
|
"timestamp": datetime.datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
async def readiness_check(self, app_state=None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Kubernetes readiness probe endpoint (/health/ready)
|
||||||
|
|
||||||
|
Returns 200 if ready, 503 if not ready
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check app state for ready status if available
|
||||||
|
ready = self.ready_state
|
||||||
|
if app_state and hasattr(app_state, 'ready'):
|
||||||
|
ready = app_state.ready
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
"application": ready
|
||||||
|
}
|
||||||
|
|
||||||
|
database_details = {}
|
||||||
|
|
||||||
|
# Database connectivity and table verification
|
||||||
|
if self.database_manager:
|
||||||
|
db_health = await self._get_comprehensive_db_health()
|
||||||
|
checks["database_connectivity"] = db_health["connectivity"]
|
||||||
|
checks["database_tables"] = db_health["tables_exist"]
|
||||||
|
|
||||||
|
database_details = {
|
||||||
|
"status": db_health["status"],
|
||||||
|
"tables_verified": db_health["tables_verified"],
|
||||||
|
"missing_tables": db_health["missing_tables"],
|
||||||
|
"errors": db_health["errors"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute custom checks
|
||||||
|
for check_name, check_func in self.custom_checks.items():
|
||||||
|
try:
|
||||||
|
checks[check_name] = await check_func()
|
||||||
|
except Exception as e:
|
||||||
|
checks[check_name] = False
|
||||||
|
logger.error(f"Custom check '{check_name}' failed", error=str(e))
|
||||||
|
|
||||||
|
# Service is ready only if all checks pass
|
||||||
|
all_ready = all(checks.values())
|
||||||
|
if self.database_manager:
|
||||||
|
all_ready = all_ready and database_details.get("status") == "healthy"
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"status": "ready" if all_ready else "not ready",
|
||||||
|
"checks": checks
|
||||||
|
}
|
||||||
|
|
||||||
|
if database_details:
|
||||||
|
response_data["database"] = database_details
|
||||||
|
|
||||||
|
if all_ready:
|
||||||
|
return response_data
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=503, detail=response_data)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Readiness check failed", error=str(e))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail={
|
||||||
|
"status": "not ready",
|
||||||
|
"error": f"Health check failed: {str(e)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def liveness_check(self) -> Dict[str, Any]:
|
||||||
|
"""Kubernetes liveness probe endpoint (/health/live)"""
|
||||||
|
return {"status": "alive"}
|
||||||
|
|
||||||
|
async def database_health_check(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Detailed database health endpoint (/health/database)
|
||||||
|
|
||||||
|
Returns 200 if healthy, 503 if unhealthy
|
||||||
|
"""
|
||||||
|
if not self.database_manager:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail={"error": "Database health check not available"}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_health = await self._get_comprehensive_db_health()
|
||||||
|
status_code = 200 if db_health["status"] == "healthy" else 503
|
||||||
|
|
||||||
|
if status_code == 503:
|
||||||
|
raise HTTPException(status_code=503, detail=db_health)
|
||||||
|
return db_health
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Database health check failed", error=str(e))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail={
|
||||||
|
"status": "unhealthy",
|
||||||
|
"error": f"Health check failed: {str(e)}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _get_comprehensive_db_health(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Comprehensive database health check with table verification
|
||||||
|
Based on training service implementation
|
||||||
|
"""
|
||||||
|
health_status = {
|
||||||
|
"status": "healthy",
|
||||||
|
"connectivity": False,
|
||||||
|
"tables_exist": False,
|
||||||
|
"tables_verified": [],
|
||||||
|
"missing_tables": [],
|
||||||
|
"errors": [],
|
||||||
|
"connection_info": {},
|
||||||
|
"response_time_ms": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.database_manager:
|
||||||
|
health_status["status"] = "unhealthy"
|
||||||
|
health_status["errors"].append("Database manager not configured")
|
||||||
|
return health_status
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test basic connectivity with timing
|
||||||
|
start_time = time.time()
|
||||||
|
health_status["connectivity"] = await self.database_manager.test_connection()
|
||||||
|
response_time = (time.time() - start_time) * 1000
|
||||||
|
health_status["response_time_ms"] = round(response_time, 2)
|
||||||
|
|
||||||
|
if not health_status["connectivity"]:
|
||||||
|
health_status["status"] = "unhealthy"
|
||||||
|
health_status["errors"].append("Database connectivity failed")
|
||||||
|
return health_status
|
||||||
|
|
||||||
|
# Get connection pool information
|
||||||
|
health_status["connection_info"] = await self.database_manager.get_connection_info()
|
||||||
|
|
||||||
|
# Test table existence if expected tables are configured
|
||||||
|
if self.expected_tables:
|
||||||
|
tables_verified = await self._verify_tables_exist()
|
||||||
|
health_status["tables_exist"] = tables_verified
|
||||||
|
|
||||||
|
if tables_verified:
|
||||||
|
health_status["tables_verified"] = self.expected_tables.copy()
|
||||||
|
else:
|
||||||
|
health_status["status"] = "unhealthy"
|
||||||
|
health_status["errors"].append("Required tables missing or inaccessible")
|
||||||
|
|
||||||
|
# Identify which specific tables are missing
|
||||||
|
await self._identify_missing_tables(health_status)
|
||||||
|
else:
|
||||||
|
# If no expected tables configured, just mark as verified
|
||||||
|
health_status["tables_exist"] = True
|
||||||
|
|
||||||
|
logger.debug("Comprehensive database health check completed",
|
||||||
|
service=self.service_name,
|
||||||
|
status=health_status["status"],
|
||||||
|
connectivity=health_status["connectivity"],
|
||||||
|
tables_exist=health_status["tables_exist"])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
health_status["status"] = "unhealthy"
|
||||||
|
health_status["errors"].append(f"Health check failed: {str(e)}")
|
||||||
|
logger.error("Comprehensive database health check failed",
|
||||||
|
service=self.service_name, error=str(e))
|
||||||
|
|
||||||
|
return health_status
|
||||||
|
|
||||||
|
async def _verify_tables_exist(self) -> bool:
|
||||||
|
"""Verify that all expected tables exist and are accessible"""
|
||||||
|
try:
|
||||||
|
async with self.database_manager.get_session() as session:
|
||||||
|
for table_name in self.expected_tables:
|
||||||
|
try:
|
||||||
|
await session.execute(text(f"SELECT 1 FROM {table_name} LIMIT 1"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Table verification failed", error=str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _identify_missing_tables(self, health_status: Dict[str, Any]):
|
||||||
|
"""Identify which specific tables are missing"""
|
||||||
|
try:
|
||||||
|
async with self.database_manager.get_session() as session:
|
||||||
|
for table_name in self.expected_tables:
|
||||||
|
try:
|
||||||
|
await session.execute(text(f"SELECT 1 FROM {table_name} LIMIT 1"))
|
||||||
|
health_status["tables_verified"].append(table_name)
|
||||||
|
except Exception:
|
||||||
|
health_status["missing_tables"].append(table_name)
|
||||||
|
except Exception as e:
|
||||||
|
health_status["errors"].append(f"Error checking individual tables: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
class FastAPIHealthChecker:
|
||||||
|
"""
|
||||||
|
FastAPI integration for health checks
|
||||||
|
|
||||||
|
Provides router setup and endpoint registration
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, health_manager: HealthCheckManager):
|
||||||
|
self.health_manager = health_manager
|
||||||
|
|
||||||
|
def setup_health_routes(self, app):
|
||||||
|
"""Setup health check routes on FastAPI app"""
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Basic health check endpoint"""
|
||||||
|
return await self.health_manager.basic_health_check(app.state)
|
||||||
|
|
||||||
|
@app.get("/health/ready")
|
||||||
|
async def readiness_check():
|
||||||
|
"""Kubernetes readiness probe endpoint"""
|
||||||
|
try:
|
||||||
|
return await self.health_manager.readiness_check(app.state)
|
||||||
|
except HTTPException as e:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=e.status_code,
|
||||||
|
content=e.detail
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/health/live")
|
||||||
|
async def liveness_check():
|
||||||
|
"""Kubernetes liveness probe endpoint"""
|
||||||
|
return await self.health_manager.liveness_check()
|
||||||
|
|
||||||
|
@app.get("/health/database")
|
||||||
|
async def database_health_check():
|
||||||
|
"""Detailed database health endpoint"""
|
||||||
|
try:
|
||||||
|
return await self.health_manager.database_health_check()
|
||||||
|
except HTTPException as e:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=e.status_code,
|
||||||
|
content=e.detail
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience functions for easy integration
|
||||||
|
|
||||||
|
def create_health_manager(
|
||||||
|
service_name: str,
|
||||||
|
version: str = "1.0.0",
|
||||||
|
database_manager: Optional[DatabaseManager] = None,
|
||||||
|
expected_tables: Optional[List[str]] = None,
|
||||||
|
custom_checks: Optional[Dict[str, Callable]] = None
|
||||||
|
) -> HealthCheckManager:
|
||||||
|
"""Factory function to create a HealthCheckManager"""
|
||||||
|
return HealthCheckManager(
|
||||||
|
service_name=service_name,
|
||||||
|
version=version,
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=expected_tables,
|
||||||
|
custom_checks=custom_checks
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_fastapi_health_checks(
|
||||||
|
app,
|
||||||
|
service_name: str,
|
||||||
|
version: str = "1.0.0",
|
||||||
|
database_manager: Optional[DatabaseManager] = None,
|
||||||
|
expected_tables: Optional[List[str]] = None,
|
||||||
|
custom_checks: Optional[Dict[str, Callable]] = None
|
||||||
|
) -> HealthCheckManager:
|
||||||
|
"""
|
||||||
|
Convenience function to setup health checks on a FastAPI app
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: FastAPI application instance
|
||||||
|
service_name: Name of the service
|
||||||
|
version: Service version
|
||||||
|
database_manager: Database manager instance
|
||||||
|
expected_tables: List of tables that should exist
|
||||||
|
custom_checks: Dict of custom check functions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HealthCheckManager instance for further configuration
|
||||||
|
"""
|
||||||
|
health_manager = create_health_manager(
|
||||||
|
service_name=service_name,
|
||||||
|
version=version,
|
||||||
|
database_manager=database_manager,
|
||||||
|
expected_tables=expected_tables,
|
||||||
|
custom_checks=custom_checks
|
||||||
|
)
|
||||||
|
|
||||||
|
fastapi_checker = FastAPIHealthChecker(health_manager)
|
||||||
|
fastapi_checker.setup_health_routes(app)
|
||||||
|
|
||||||
|
return health_manager
|
||||||
|
|
||||||
|
|
||||||
429
shared/service_base.py
Normal file
429
shared/service_base.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
"""
|
||||||
|
Standardized FastAPI Service Base
|
||||||
|
|
||||||
|
Provides a unified approach for creating FastAPI microservices with common patterns:
|
||||||
|
- Logging setup
|
||||||
|
- Metrics initialization
|
||||||
|
- Health checks
|
||||||
|
- Database initialization
|
||||||
|
- CORS middleware
|
||||||
|
- Exception handlers
|
||||||
|
- Lifespan management
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import structlog
|
||||||
|
from typing import Optional, List, Dict, Callable, Any, TYPE_CHECKING
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
|
from shared.monitoring import setup_logging
|
||||||
|
from shared.monitoring.metrics import setup_metrics_early
|
||||||
|
from shared.monitoring.health_checks import setup_fastapi_health_checks
|
||||||
|
from shared.database.base import DatabaseManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFastAPIService:
|
||||||
|
"""
|
||||||
|
Base class for FastAPI microservices with standardized patterns
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
service_name: str,
|
||||||
|
app_name: str,
|
||||||
|
description: str,
|
||||||
|
version: str = "1.0.0",
|
||||||
|
log_level: str = "INFO",
|
||||||
|
cors_origins: Optional[List[str]] = None,
|
||||||
|
api_prefix: str = "/api/v1",
|
||||||
|
database_manager: Optional[DatabaseManager] = None,
|
||||||
|
expected_tables: Optional[List[str]] = None,
|
||||||
|
custom_health_checks: Optional[Dict[str, Callable[[], Any]]] = None,
|
||||||
|
enable_metrics: bool = True,
|
||||||
|
enable_health_checks: bool = True,
|
||||||
|
enable_cors: bool = True,
|
||||||
|
enable_exception_handlers: bool = True,
|
||||||
|
enable_messaging: bool = False,
|
||||||
|
custom_metrics: Optional[Dict[str, Dict[str, Any]]] = None,
|
||||||
|
alert_service_class: Optional[type] = None
|
||||||
|
):
|
||||||
|
self.service_name = service_name
|
||||||
|
self.app_name = app_name
|
||||||
|
self.description = description
|
||||||
|
self.version = version
|
||||||
|
self.log_level = log_level
|
||||||
|
self.cors_origins = cors_origins or ["*"]
|
||||||
|
self.api_prefix = api_prefix
|
||||||
|
self.database_manager = database_manager
|
||||||
|
self.expected_tables = expected_tables
|
||||||
|
self.custom_health_checks = custom_health_checks or {}
|
||||||
|
self.enable_metrics = enable_metrics
|
||||||
|
self.enable_health_checks = enable_health_checks
|
||||||
|
self.enable_cors = enable_cors
|
||||||
|
self.enable_exception_handlers = enable_exception_handlers
|
||||||
|
self.enable_messaging = enable_messaging
|
||||||
|
self.custom_metrics = custom_metrics or {}
|
||||||
|
self.alert_service_class = alert_service_class
|
||||||
|
|
||||||
|
# Initialize logging
|
||||||
|
setup_logging(service_name, log_level)
|
||||||
|
self.logger = structlog.get_logger()
|
||||||
|
|
||||||
|
# Will be set during app creation
|
||||||
|
self.app: Optional[FastAPI] = None
|
||||||
|
self.metrics_collector = None
|
||||||
|
self.health_manager = None
|
||||||
|
self.alert_service = None
|
||||||
|
|
||||||
|
def create_app(self, **fastapi_kwargs) -> FastAPI:
|
||||||
|
"""
|
||||||
|
Create and configure FastAPI application with standardized setup
|
||||||
|
"""
|
||||||
|
# Default FastAPI configuration
|
||||||
|
default_config = {
|
||||||
|
"title": self.app_name,
|
||||||
|
"description": self.description,
|
||||||
|
"version": self.version,
|
||||||
|
"openapi_url": f"{self.api_prefix}/openapi.json",
|
||||||
|
"docs_url": f"{self.api_prefix}/docs",
|
||||||
|
"redoc_url": f"{self.api_prefix}/redoc",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge with user-provided config
|
||||||
|
config = {**default_config, **fastapi_kwargs}
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
self.app = FastAPI(**config)
|
||||||
|
|
||||||
|
# Setup metrics BEFORE middleware and lifespan
|
||||||
|
if self.enable_metrics:
|
||||||
|
self.metrics_collector = setup_metrics_early(self.app, self.service_name)
|
||||||
|
|
||||||
|
# Setup lifespan
|
||||||
|
self.app.router.lifespan_context = self._create_lifespan()
|
||||||
|
|
||||||
|
# Setup middleware
|
||||||
|
if self.enable_cors:
|
||||||
|
self._setup_cors()
|
||||||
|
|
||||||
|
# Setup exception handlers
|
||||||
|
if self.enable_exception_handlers:
|
||||||
|
self._setup_exception_handlers()
|
||||||
|
|
||||||
|
# Setup health checks
|
||||||
|
if self.enable_health_checks:
|
||||||
|
self._setup_health_checks()
|
||||||
|
|
||||||
|
# Setup root endpoint
|
||||||
|
self._setup_root_endpoint()
|
||||||
|
|
||||||
|
return self.app
|
||||||
|
|
||||||
|
def _create_lifespan(self):
|
||||||
|
"""Create lifespan context manager"""
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# Startup
|
||||||
|
self.logger.info(f"Starting {self.service_name}", version=self.version)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize database if provided
|
||||||
|
if self.database_manager:
|
||||||
|
await self._initialize_database()
|
||||||
|
|
||||||
|
# Setup messaging if enabled
|
||||||
|
if self.enable_messaging:
|
||||||
|
await self._setup_messaging()
|
||||||
|
|
||||||
|
# Initialize alert service if provided
|
||||||
|
if self.alert_service_class:
|
||||||
|
await self._initialize_alert_service(app)
|
||||||
|
|
||||||
|
# Register custom metrics if provided
|
||||||
|
if self.custom_metrics:
|
||||||
|
self.register_custom_metrics(self.custom_metrics)
|
||||||
|
|
||||||
|
# Custom startup logic
|
||||||
|
await self.on_startup(app)
|
||||||
|
|
||||||
|
# Mark service as ready
|
||||||
|
app.state.ready = True
|
||||||
|
|
||||||
|
self.logger.info(f"{self.service_name} started successfully")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Startup failed for {self.service_name}", error=str(e))
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# Shutdown
|
||||||
|
self.logger.info(f"Shutting down {self.service_name}")
|
||||||
|
try:
|
||||||
|
await self.on_shutdown(app)
|
||||||
|
|
||||||
|
# Cleanup alert service if it exists
|
||||||
|
if self.alert_service:
|
||||||
|
await self._cleanup_alert_service()
|
||||||
|
|
||||||
|
# Cleanup messaging if enabled
|
||||||
|
if self.enable_messaging:
|
||||||
|
await self._cleanup_messaging()
|
||||||
|
|
||||||
|
if self.database_manager:
|
||||||
|
await self._cleanup_database()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Shutdown error for {self.service_name}", error=str(e))
|
||||||
|
|
||||||
|
return lifespan
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""
|
||||||
|
Override this method for custom startup logic
|
||||||
|
Called after database initialization but before marking as ready
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""
|
||||||
|
Override this method for custom shutdown logic
|
||||||
|
Called before database cleanup
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _initialize_database(self):
|
||||||
|
"""Initialize database connection"""
|
||||||
|
try:
|
||||||
|
# Test connection
|
||||||
|
if await self.database_manager.test_connection():
|
||||||
|
self.logger.info("Database initialized successfully")
|
||||||
|
else:
|
||||||
|
raise Exception("Database connection test failed")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Database initialization failed", error=str(e))
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _cleanup_database(self):
|
||||||
|
"""Cleanup database connections"""
|
||||||
|
try:
|
||||||
|
await self.database_manager.close_connections()
|
||||||
|
self.logger.info("Database connections closed")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("Database cleanup error", error=str(e))
|
||||||
|
|
||||||
|
async def _setup_messaging(self):
|
||||||
|
"""Setup messaging service - to be overridden by services that need it"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _cleanup_messaging(self):
|
||||||
|
"""Cleanup messaging service - to be overridden by services that need it"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _initialize_alert_service(self, app: FastAPI):
|
||||||
|
"""Initialize alert service - to be overridden by services that need it"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _cleanup_alert_service(self):
|
||||||
|
"""Cleanup alert service - to be overridden by services that need it"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _setup_cors(self):
|
||||||
|
"""Setup CORS middleware"""
|
||||||
|
if self.app is not None:
|
||||||
|
self.app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=self.cors_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_exception_handlers(self):
|
||||||
|
"""Setup standard exception handlers"""
|
||||||
|
if self.app is not None:
|
||||||
|
@self.app.exception_handler(ValueError)
|
||||||
|
async def value_error_handler(request: Request, exc: ValueError):
|
||||||
|
"""Handle validation errors"""
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={
|
||||||
|
"error": "Validation Error",
|
||||||
|
"detail": str(exc),
|
||||||
|
"type": "value_error"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.app.exception_handler(Exception)
|
||||||
|
async def general_exception_handler(request: Request, exc: Exception):
|
||||||
|
"""Handle general exceptions"""
|
||||||
|
self.logger.error(
|
||||||
|
"Unhandled exception",
|
||||||
|
error=str(exc),
|
||||||
|
path=request.url.path,
|
||||||
|
method=request.method
|
||||||
|
)
|
||||||
|
|
||||||
|
# Record error metric if available
|
||||||
|
if self.metrics_collector:
|
||||||
|
self.metrics_collector.increment_counter("errors_total", labels={"type": "unhandled"})
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
"detail": "An unexpected error occurred",
|
||||||
|
"type": "internal_error"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_health_checks(self):
|
||||||
|
"""Setup health check endpoints"""
|
||||||
|
self.health_manager = setup_fastapi_health_checks(
|
||||||
|
app=self.app,
|
||||||
|
service_name=self.service_name,
|
||||||
|
version=self.version,
|
||||||
|
database_manager=self.database_manager,
|
||||||
|
expected_tables=self.expected_tables,
|
||||||
|
custom_checks=self.custom_health_checks
|
||||||
|
)
|
||||||
|
|
||||||
|
def _setup_root_endpoint(self):
|
||||||
|
"""Setup root endpoint with service information"""
|
||||||
|
if self.app is not None:
|
||||||
|
@self.app.get("/")
|
||||||
|
async def root():
|
||||||
|
"""Root endpoint with service information"""
|
||||||
|
return {
|
||||||
|
"service": self.service_name,
|
||||||
|
"version": self.version,
|
||||||
|
"description": self.description,
|
||||||
|
"status": "running",
|
||||||
|
"docs_url": f"{self.api_prefix}/docs",
|
||||||
|
"health_url": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_router(self, router: APIRouter, **kwargs: Any):
|
||||||
|
"""Convenience method to add routers with default prefix"""
|
||||||
|
if self.app is not None:
|
||||||
|
prefix = kwargs.get('prefix', self.api_prefix)
|
||||||
|
kwargs['prefix'] = prefix
|
||||||
|
self.app.include_router(router, **kwargs)
|
||||||
|
|
||||||
|
def register_custom_metrics(self, metrics_config: Dict[str, Dict[str, Any]]):
|
||||||
|
"""
|
||||||
|
Register custom metrics for the service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metrics_config: Dict with metric name as key and config as value
|
||||||
|
Example: {
|
||||||
|
"user_registrations": {
|
||||||
|
"type": "counter",
|
||||||
|
"description": "Total user registrations",
|
||||||
|
"labels": ["status"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not self.metrics_collector:
|
||||||
|
self.logger.warning("Metrics collector not available")
|
||||||
|
return
|
||||||
|
|
||||||
|
for metric_name, config in metrics_config.items():
|
||||||
|
metric_type = config.get("type", "counter")
|
||||||
|
description = config.get("description", f"{metric_name} metric")
|
||||||
|
labels = config.get("labels", [])
|
||||||
|
|
||||||
|
if metric_type == "counter":
|
||||||
|
self.metrics_collector.register_counter(metric_name, description, labels=labels)
|
||||||
|
elif metric_type == "histogram":
|
||||||
|
self.metrics_collector.register_histogram(metric_name, description, labels=labels)
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Unsupported metric type: {metric_type}")
|
||||||
|
|
||||||
|
def run_development_server(self, host: str = "0.0.0.0", port: int = 8000, reload: Optional[bool] = None):
|
||||||
|
"""
|
||||||
|
Run development server with uvicorn
|
||||||
|
"""
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
if reload is None:
|
||||||
|
reload = os.getenv("RELOAD", "false").lower() == "true"
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
f"{self.__module__}:app",
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
reload=reload,
|
||||||
|
log_level="info"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StandardFastAPIService(BaseFastAPIService):
|
||||||
|
"""
|
||||||
|
Standard service implementation for most microservices
|
||||||
|
|
||||||
|
Provides additional common patterns for services with database and standard endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def setup_standard_endpoints(self):
|
||||||
|
"""Setup standard service endpoints"""
|
||||||
|
if self.app is not None:
|
||||||
|
@self.app.get(f"{self.api_prefix}/info")
|
||||||
|
async def service_info():
|
||||||
|
"""Service information endpoint"""
|
||||||
|
return {
|
||||||
|
"service": self.service_name,
|
||||||
|
"version": self.version,
|
||||||
|
"description": self.description,
|
||||||
|
"api_version": "v1",
|
||||||
|
"environment": os.getenv("ENVIRONMENT", "development"),
|
||||||
|
"features": self.get_service_features()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_service_features(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Override this method to return service-specific features
|
||||||
|
"""
|
||||||
|
return ["health_checks", "metrics", "standardized_api"]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageProcessorService(BaseFastAPIService):
|
||||||
|
"""
|
||||||
|
Service implementation for message processing services
|
||||||
|
|
||||||
|
Provides patterns for background processing services
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any):
|
||||||
|
# Message processors typically don't need CORS or full API setup
|
||||||
|
kwargs.setdefault('enable_cors', False)
|
||||||
|
kwargs.setdefault('api_prefix', '/api/v1')
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
async def on_startup(self, app: FastAPI):
|
||||||
|
"""Initialize message processing components"""
|
||||||
|
await super().on_startup(app)
|
||||||
|
await self.setup_message_processing()
|
||||||
|
|
||||||
|
async def on_shutdown(self, app: FastAPI):
|
||||||
|
"""Cleanup message processing components"""
|
||||||
|
await self.cleanup_message_processing()
|
||||||
|
await super().on_shutdown(app)
|
||||||
|
|
||||||
|
async def setup_message_processing(self):
|
||||||
|
"""Override this method to setup message processing"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cleanup_message_processing(self):
|
||||||
|
"""Override this method to cleanup message processing"""
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user