Files
orion/docs/architecture/audit-provider-pattern.md
Samir Boulahtit 39dff4ab7d 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>
2026-02-04 21:32:32 +01:00

266 lines
9.7 KiB
Markdown

# 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