refactor: fix architecture violations with provider patterns and dependency inversion
Major changes: - Add AuditProvider protocol for cross-module audit logging - Move customer order operations to orders module (dependency inversion) - Add customer order metrics via MetricsProvider pattern - Fix missing db parameter in get_admin_context() calls - Move ProductMedia relationship to catalog module (proper ownership) - Add marketplace breakdown stats to marketplace_widgets New files: - contracts/audit.py - AuditProviderProtocol - core/services/audit_aggregator.py - Aggregates audit providers - monitoring/services/audit_provider.py - Monitoring audit implementation - orders/services/customer_order_service.py - Customer order operations - orders/routes/api/vendor_customer_orders.py - Customer order endpoints - catalog/services/product_media_service.py - Product media service - Architecture documentation for patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
265
docs/architecture/audit-provider-pattern.md
Normal file
265
docs/architecture/audit-provider-pattern.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Audit Provider Pattern
|
||||
|
||||
The audit provider pattern enables modules to provide audit logging backends without creating cross-module dependencies. This allows the monitoring module to be truly optional while still providing audit logging when enabled.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Admin Action Request │
|
||||
│ (Settings update, User management, etc.) │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ AuditAggregatorService │
|
||||
│ (app/modules/core/services/audit_aggregator.py) │
|
||||
│ │
|
||||
│ • Discovers AuditProviders from all registered modules │
|
||||
│ • Calls log_action() on all available providers │
|
||||
│ • Handles graceful degradation if no providers available │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ DatabaseAudit │ │ (Future) │ │ (Future) │
|
||||
│ Provider │ │ FileProvider │ │ CloudWatch │
|
||||
│ (monitoring) │ │ │ │ │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│AdminAuditLog │
|
||||
│ (table) │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## Problem Solved
|
||||
|
||||
Before this pattern, core modules had **hard imports** from the monitoring module:
|
||||
|
||||
```python
|
||||
# BAD: Core module with hard dependency on optional/internal module
|
||||
from app.modules.monitoring.services.admin_audit_service import admin_audit_service
|
||||
|
||||
def create_setting(...):
|
||||
result = settings_service.create_setting(...)
|
||||
admin_audit_service.log_action(...) # Crashes if monitoring disabled!
|
||||
```
|
||||
|
||||
This violated the architecture rule: **Core modules cannot depend on optional modules.**
|
||||
|
||||
## Solution: Protocol-Based Audit Logging
|
||||
|
||||
The monitoring module implements `AuditProviderProtocol` and registers it in its `definition.py`. The `AuditAggregatorService` in core discovers and uses audit providers from all modules.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. AuditEvent Dataclass
|
||||
|
||||
Standard structure for audit events:
|
||||
|
||||
```python
|
||||
# app/modules/contracts/audit.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class AuditEvent:
|
||||
admin_user_id: int # ID of admin performing action
|
||||
action: str # Action name (e.g., "create_setting")
|
||||
target_type: str # Target type (e.g., "setting", "user")
|
||||
target_id: str # Target identifier
|
||||
details: dict[str, Any] | None = None # Additional context
|
||||
ip_address: str | None = None # Admin's IP address
|
||||
user_agent: str | None = None # Browser user agent
|
||||
request_id: str | None = None # Request correlation ID
|
||||
```
|
||||
|
||||
### 2. AuditProviderProtocol
|
||||
|
||||
Protocol that modules implement:
|
||||
|
||||
```python
|
||||
# app/modules/contracts/audit.py
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
@runtime_checkable
|
||||
class AuditProviderProtocol(Protocol):
|
||||
@property
|
||||
def audit_backend(self) -> str:
|
||||
"""Backend name (e.g., 'database', 'file', 'cloudwatch')."""
|
||||
...
|
||||
|
||||
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
||||
"""Log an audit event. Returns True on success."""
|
||||
...
|
||||
```
|
||||
|
||||
### 3. AuditAggregatorService
|
||||
|
||||
The aggregator in core that discovers and uses providers:
|
||||
|
||||
```python
|
||||
# app/modules/core/services/audit_aggregator.py
|
||||
|
||||
class AuditAggregatorService:
|
||||
def _get_enabled_providers(self, db: Session):
|
||||
"""Discover audit providers from registered modules."""
|
||||
from app.modules.registry import MODULES
|
||||
|
||||
for module in MODULES.values():
|
||||
if module.has_audit_provider():
|
||||
provider = module.get_audit_provider_instance()
|
||||
if provider:
|
||||
yield (module, provider)
|
||||
|
||||
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
||||
"""Log to all available providers."""
|
||||
providers = list(self._get_enabled_providers(db))
|
||||
|
||||
if not providers:
|
||||
# No providers - acceptable, audit is optional
|
||||
return False
|
||||
|
||||
any_success = False
|
||||
for module, provider in providers:
|
||||
try:
|
||||
if provider.log_action(db, event):
|
||||
any_success = True
|
||||
except Exception as e:
|
||||
logger.warning(f"Audit provider {module.code} failed: {e}")
|
||||
|
||||
return any_success
|
||||
|
||||
def log(self, db, admin_user_id, action, target_type, target_id, **kwargs):
|
||||
"""Convenience method with individual parameters."""
|
||||
event = AuditEvent(
|
||||
admin_user_id=admin_user_id,
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
target_id=str(target_id),
|
||||
**kwargs
|
||||
)
|
||||
return self.log_action(db, event)
|
||||
```
|
||||
|
||||
## Implementing a Provider
|
||||
|
||||
### Step 1: Create the Provider
|
||||
|
||||
```python
|
||||
# app/modules/monitoring/services/audit_provider.py
|
||||
|
||||
from app.modules.contracts.audit import AuditEvent, AuditProviderProtocol
|
||||
from app.modules.tenancy.models import AdminAuditLog
|
||||
|
||||
class DatabaseAuditProvider:
|
||||
@property
|
||||
def audit_backend(self) -> str:
|
||||
return "database"
|
||||
|
||||
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
||||
try:
|
||||
audit_log = AdminAuditLog(
|
||||
admin_user_id=event.admin_user_id,
|
||||
action=event.action,
|
||||
target_type=event.target_type,
|
||||
target_id=event.target_id,
|
||||
details=event.details or {},
|
||||
ip_address=event.ip_address,
|
||||
user_agent=event.user_agent,
|
||||
request_id=event.request_id,
|
||||
)
|
||||
db.add(audit_log)
|
||||
db.flush()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to log audit: {e}")
|
||||
return False
|
||||
|
||||
audit_provider = DatabaseAuditProvider()
|
||||
```
|
||||
|
||||
### Step 2: Register in Module Definition
|
||||
|
||||
```python
|
||||
# app/modules/monitoring/definition.py
|
||||
|
||||
def _get_audit_provider():
|
||||
"""Lazy import to avoid circular imports."""
|
||||
from app.modules.monitoring.services.audit_provider import audit_provider
|
||||
return audit_provider
|
||||
|
||||
monitoring_module = ModuleDefinition(
|
||||
code="monitoring",
|
||||
name="Platform Monitoring",
|
||||
audit_provider=_get_audit_provider, # Register the provider
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Use via Aggregator
|
||||
|
||||
```python
|
||||
# app/modules/core/routes/api/admin_settings.py
|
||||
|
||||
from app.modules.core.services.audit_aggregator import audit_aggregator
|
||||
|
||||
@router.post("/settings")
|
||||
def create_setting(setting_data: SettingCreate, ...):
|
||||
result = settings_service.create_setting(db, setting_data)
|
||||
|
||||
# Log via aggregator - works even if monitoring is disabled
|
||||
audit_aggregator.log(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="create_setting",
|
||||
target_type="setting",
|
||||
target_id=setting_data.key,
|
||||
details={"category": setting_data.category},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
return result
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
When no audit providers are registered (e.g., monitoring module disabled):
|
||||
|
||||
- `audit_aggregator.log()` returns `False`
|
||||
- No exceptions are raised
|
||||
- The operation continues normally
|
||||
- A debug log message notes the absence of providers
|
||||
|
||||
This ensures core functionality works regardless of whether auditing is available.
|
||||
|
||||
## Multiple Backends
|
||||
|
||||
The pattern supports multiple audit backends simultaneously:
|
||||
|
||||
```python
|
||||
# All registered providers receive the event
|
||||
audit_aggregator.log(...)
|
||||
|
||||
# Provider 1: Database (AdminAuditLog table)
|
||||
# Provider 2: CloudWatch Logs (if aws-monitoring module enabled)
|
||||
# Provider 3: File-based audit log (if file-audit module enabled)
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Decoupling**: Core modules don't depend on monitoring
|
||||
2. **Optional Auditing**: Monitoring can be disabled without breaking the app
|
||||
3. **Extensibility**: Easy to add new audit backends (file, cloud, SIEM)
|
||||
4. **Testability**: Can mock audit providers in tests
|
||||
5. **Fault Tolerance**: One provider failure doesn't affect others
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions
|
||||
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Similar pattern for statistics
|
||||
- [Widget Provider Pattern](widget-provider-pattern.md) - Similar pattern for dashboard widgets
|
||||
- [Module System Architecture](module-system.md) - Module structure and auto-discovery
|
||||
Reference in New Issue
Block a user