# Story 4.43: Audit Trail Instance Management

**Status:** Complete
**Priority:** Medium
**Depends on:** None

---

## Context / Background

The GPT-5.2 audit identified an operability risk: the audit trail uses a global singleton that can cause cross-test and cross-application interference.

**Current state in `packages/fapilog-audit/src/fapilog_audit/audit.py:758-779`:**

```python
# Global audit trail instance
_audit_trail: AuditTrail | None = None

async def get_audit_trail(
    policy: CompliancePolicy | None = None, storage_path: Path | None = None
) -> AuditTrail:
    """Get global audit trail instance."""
    global _audit_trail
    if _audit_trail is None:
        _audit_trail = AuditTrail(policy, storage_path)
        await _audit_trail.start()
    return _audit_trail
```

**Problems:**

1. **Test interference:** Tests share the same instance, causing:
   - Events from one test appearing in another
   - Cleanup complexity between tests
   - Flaky tests when running in parallel

2. **Multi-tenant risk:** Applications with multiple tenants/contexts can't have isolated audit trails

3. **Configuration inflexibility:** First caller's `policy`/`storage_path` wins; subsequent calls ignore parameters

---

## Scope (In / Out)

### In Scope

- Add instance management for audit trails
- Provide `reset_audit_trail()` for test cleanup
- Support named/scoped audit trail instances
- Maintain backward compatibility with global convenience functions

### Out of Scope

- Multi-tenant audit trail routing (complex, future story)
- Changing audit trail storage backend

---

## Acceptance Criteria

### AC1: Reset Function for Tests

**Description:** Tests can reset the global audit trail between tests.

**Validation:**

```python
from fapilog_audit import reset_audit_trail, get_audit_trail

async def test_audit_isolation():
    await reset_audit_trail()  # Clean slate
    trail = await get_audit_trail()
    await trail.log_event(...)

@pytest.fixture(autouse=True)
async def reset_audit():
    yield
    await reset_audit_trail()
```

### AC2: Named Instances Supported

**Description:** Multiple named audit trail instances can coexist.

**Validation:**

```python
from fapilog_audit import get_audit_trail

# Default global instance
default_trail = await get_audit_trail()

# Named instance for specific context
admin_trail = await get_audit_trail(name="admin-operations")
user_trail = await get_audit_trail(name="user-activity")

assert admin_trail is not user_trail
assert default_trail is not admin_trail
```

### AC3: Configuration Respected on Reset

**Description:** After reset, new configuration is applied.

**Validation:**

```python
trail1 = await get_audit_trail(storage_path=Path("/tmp/trail1"))
await reset_audit_trail()
trail2 = await get_audit_trail(storage_path=Path("/tmp/trail2"))
# trail2 uses /tmp/trail2, not /tmp/trail1
```

### AC4: Backward Compatible

**Description:** Existing code using `get_audit_trail()` continues to work.

**Validation:**

```python
# This still works exactly as before
from fapilog_audit import get_audit_trail, audit_error, audit_security_event

trail = await get_audit_trail()
await audit_error(exc, operation="test")
```

---

## Implementation Notes

### File Changes

```text
packages/fapilog-audit/src/fapilog_audit/audit.py (MODIFIED)
packages/fapilog-audit/tests/conftest.py (MODIFIED - add reset fixture)
```

### Key Changes

```python
# packages/fapilog-audit/src/fapilog_audit/audit.py

# Registry of named instances
_audit_trails: dict[str, AuditTrail] = {}
_default_trail: Optional[AuditTrail] = None

async def get_audit_trail(
    policy: Optional[CompliancePolicy] = None,
    storage_path: Optional[Path] = None,
    *,
    name: str | None = None,
) -> AuditTrail:
    """Get audit trail instance, optionally by name."""
    global _default_trail

    if name is None:
        # Default global instance (backward compat)
        if _default_trail is None:
            _default_trail = AuditTrail(policy, storage_path)
            await _default_trail.start()
        return _default_trail

    # Named instance
    if name not in _audit_trails:
        trail = AuditTrail(policy, storage_path)
        await trail.start()
        _audit_trails[name] = trail
    return _audit_trails[name]


async def reset_audit_trail(name: str | None = None) -> None:
    """Reset audit trail instance(s) for testing.

    Args:
        name: If provided, reset only the named instance.
              If None, reset the default global instance.
    """
    global _default_trail

    if name is None:
        if _default_trail is not None:
            await _default_trail.stop()
            _default_trail = None
    elif name in _audit_trails:
        await _audit_trails[name].stop()
        del _audit_trails[name]


async def reset_all_audit_trails() -> None:
    """Reset all audit trail instances. Use in test teardown."""
    global _default_trail, _audit_trails

    if _default_trail is not None:
        await _default_trail.stop()
        _default_trail = None

    for trail in _audit_trails.values():
        await trail.stop()
    _audit_trails.clear()
```

---

## Tasks

- [ ] Add `_audit_trails` registry for named instances
- [ ] Add `name` parameter to `get_audit_trail()`
- [ ] Implement `reset_audit_trail()` function
- [ ] Implement `reset_all_audit_trails()` function
- [ ] Add pytest fixture for automatic reset
- [ ] Update CHANGELOG

---

## Tests

- `packages/fapilog-audit/tests/test_audit_instance_management.py`
  - Test reset clears instance
  - Test named instances are isolated
  - Test configuration respected after reset
  - Test backward compatibility

---

## Definition of Done

- [x] Reset function implemented and working
- [x] Named instances supported
- [x] All tests pass (including existing audit tests)
- [ ] ~~CHANGELOG updated~~ (N/A - no CHANGELOG in repo)

---

## Risks / Rollback

### Risks

1. **Risk:** Existing tests rely on shared state
   - **Mitigation:** Add fixture to conftest.py; gradual migration

### Rollback Plan

Revert to pure global singleton if issues arise.

---

---

## Code Review

**Date:** 2026-01-18
**Reviewer:** Claude
**Verdict:** OK to PR

### Summary

Reviewed instance management implementation: `reset_audit_trail()`, `reset_all_audit_trails()`, and named instance support via `name` parameter.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Reset Function for Tests | `audit.py:798-811` - `reset_audit_trail()` stops and clears instance |
| AC2: Named Instances Supported | `audit.py:767-795` - `get_audit_trail(name=...)` with `_audit_trails` registry |
| AC3: Configuration Respected on Reset | `audit.py:798-811` - resets to None, next call creates fresh instance |
| AC4: Backward Compatible | `audit.py:780-786` - `name=None` path unchanged |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover 100%
- [x] No weak assertions
- [x] All 75 tests pass

---

## Change Log

| Date       | Change        | Author |
|------------|---------------|--------|
| 2026-01-18 | Code review passed | Claude |
| 2026-01-16 | Initial draft | Claude |
