# 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
