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>
9.7 KiB
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:
# 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:
# 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:
# 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:
# 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
# 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
# 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
# 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()returnsFalse- 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:
# 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
- Decoupling: Core modules don't depend on monitoring
- Optional Auditing: Monitoring can be disabled without breaking the app
- Extensibility: Easy to add new audit backends (file, cloud, SIEM)
- Testability: Can mock audit providers in tests
- Fault Tolerance: One provider failure doesn't affect others
Related Documentation
- Cross-Module Import Rules - Import restrictions
- Metrics Provider Pattern - Similar pattern for statistics
- Widget Provider Pattern - Similar pattern for dashboard widgets
- Module System Architecture - Module structure and auto-discovery