# Story 1.25: Eliminate Remaining Settings() Hot Path Calls

**Status:** Complete
**Priority:** High
**Depends on:** Story 1.23 (Settings hot path caching - completed)

---

## Context / Background

Story 1.23 addressed `Settings()` instantiation in `_prepare_payload()` by caching sampling/dedupe settings at logger init. However, an audit found several remaining places where `Settings()` is still called on hot paths, causing:

- Unnecessary CPU overhead (Pydantic parsing on every call)
- Inconsistent behavior (env changes mid-request)
- Harder-to-reason-about code (implicit global state reads)

### Remaining Hot Path Calls

**1. Diagnostics enablement** (`src/fapilog/core/diagnostics.py:69-73`):
```python
def _is_enabled() -> bool:
    try:
        from .settings import Settings
        return bool(Settings().core.internal_logging_enabled)  # Every diagnostic emit!
    except Exception:
        return False
```

**2. Strict envelope mode check** (`src/fapilog/core/worker.py:27-32`):
```python
def strict_envelope_mode_enabled() -> bool:
    try:
        from . import settings as _settings
        return bool(_settings.Settings().core.strict_envelope_mode)  # Worker processing!
    except Exception:
        return False
```

**3. Sink write paths** (`src/fapilog/plugins/sinks/rotating_file.py:143`, `stdout_json.py:47`):
```python
strict = bool(_settings.Settings().core.strict_envelope_mode)  # Every log write!
```

**4. Shutdown timeout** (`src/fapilog/core/lifecycle.py:33`):
```python
to = float(Settings().core.shutdown_timeout_seconds)  # Shutdown path
```

### Impact

- At 10k logs/sec, sink calls alone create 10k Settings() objects per second
- Diagnostics in error paths add more overhead when debugging
- Behavior can change mid-request if environment changes
- Memory churn from temporary Pydantic model instances

---

## Scope (In / Out)

### In Scope

- Pass configuration values into components at creation time
- Add configuration fields to `LoggerWorker` for strict_envelope_mode
- Add module-level caching for diagnostics enablement
- Pass shutdown_timeout to lifecycle functions
- Remove all `Settings()` calls from write/emit paths

### Out of Scope

- Dynamic settings reloading (use explicit hot-reload mechanism if needed)
- Refactoring Settings class itself
- Adding configuration validation beyond what exists

---

## Acceptance Criteria

### AC1: No Settings() in Diagnostic Emit Path

**Description:** `diagnostics.warn()` and `diagnostics.debug()` do not instantiate Settings() on every call.

**Validation:**
```python
# diagnostics._is_enabled() should use cached/configured value
# Not re-read Settings() on every call

import fapilog.core.diagnostics as diag

# After init, this should not create Settings() objects
for _ in range(1000):
    diag.warn("test", "message")

# Verify no Settings() instantiation in hot path
```

### AC2: No Settings() in Worker/Sink Write Paths

**Description:** `strict_envelope_mode_enabled()` and sink writes use configuration passed at creation time.

**Validation:**
```python
# LoggerWorker should receive strict_envelope_mode at __init__
# Sinks should receive it via config, not re-query Settings()

# After worker creation, processing should not call Settings()
worker = LoggerWorker(strict_envelope_mode=True, ...)

# grep should find no Settings() in write() methods
grep -n "Settings()" src/fapilog/plugins/sinks/*.py
# Should return empty or only in __init__ / class-level
```

### AC3: Configuration Passed at Creation Time

**Description:** Components receive configuration values as constructor arguments, not via global lookups.

**Validation:**
```python
# LoggerWorker signature should include:
class LoggerWorker:
    def __init__(
        self,
        ...,
        strict_envelope_mode: bool = False,
        internal_logging_enabled: bool = False,
    ): ...

# Sinks receive strict_envelope_mode via config dict
class RotatingFileSink:
    def __init__(self, config: RotatingFileSinkConfig, ...):
        self._strict_mode = config.strict_envelope_mode  # Not Settings()
```

### AC4: Explicit Refresh Mechanism for Dynamic Toggles

**Description:** If dynamic configuration changes are needed, use an explicit mechanism rather than silent per-call reads.

**Validation:**
```python
# If hot-reload is needed, it should be explicit:
from fapilog.core.hot_reload import refresh_diagnostics_config

# Not implicit via Settings() on every call
refresh_diagnostics_config()  # Explicit, auditable
```

---

## Implementation Notes

### File Changes

```
src/fapilog/core/diagnostics.py (MODIFIED - module-level caching)
src/fapilog/core/worker.py (MODIFIED - accept config at init)
src/fapilog/core/lifecycle.py (MODIFIED - accept timeout as parameter)
src/fapilog/plugins/sinks/rotating_file.py (MODIFIED - use config)
src/fapilog/plugins/sinks/stdout_json.py (MODIFIED - use config)
src/fapilog/core/logger.py (MODIFIED - pass config to worker)
tests/unit/test_settings_hot_path.py (NEW - verify no Settings() in hot path)
```

### Diagnostics Caching Pattern

```python
# src/fapilog/core/diagnostics.py

# Module-level cache, set once at import or first use
_internal_logging_enabled: bool | None = None

def _is_enabled() -> bool:
    global _internal_logging_enabled
    if _internal_logging_enabled is None:
        try:
            from .settings import Settings
            _internal_logging_enabled = bool(Settings().core.internal_logging_enabled)
        except Exception:
            _internal_logging_enabled = False
    return _internal_logging_enabled

def configure_diagnostics(enabled: bool) -> None:
    """Explicitly configure diagnostics enablement."""
    global _internal_logging_enabled
    _internal_logging_enabled = enabled
```

### Worker Configuration Pattern

```python
# src/fapilog/core/worker.py

class LoggerWorker:
    def __init__(
        self,
        queue: asyncio.Queue,
        sinks: list[BaseSink],
        *,
        strict_envelope_mode: bool = False,
        # ... other config
    ):
        self._strict_envelope_mode = strict_envelope_mode

    async def _process_batch(self, batch: list[dict]) -> None:
        # Use self._strict_envelope_mode, not Settings()
        if self._strict_envelope_mode:
            ...
```

### Sink Configuration Pattern

```python
# src/fapilog/plugins/sinks/rotating_file.py

class RotatingFileSinkConfig(BaseModel):
    # ... existing fields
    strict_envelope_mode: bool = Field(
        default=False,
        description="Use strict envelope serialization mode",
    )

class RotatingFileSink:
    def __init__(self, config: RotatingFileSinkConfig, ...):
        self._strict_mode = config.strict_envelope_mode

    async def write(self, entry: dict) -> None:
        # Use self._strict_mode, not Settings()
```

---

## Tasks

### Phase 1: Diagnostics Module

- [ ] Add module-level caching to `diagnostics.py`
- [ ] Add `configure_diagnostics()` function for explicit setup
- [ ] Initialize cache from Settings() once at module load or first use
- [ ] Update tests to verify no per-call Settings() instantiation

### Phase 2: Worker and Lifecycle

- [ ] Add `strict_envelope_mode` parameter to `LoggerWorker.__init__`
- [ ] Pass config from logger creation to worker
- [ ] Update `lifecycle.py` to accept timeout as parameter
- [ ] Remove `strict_envelope_mode_enabled()` global function

### Phase 3: Sinks

- [ ] Add `strict_envelope_mode` to sink config models
- [ ] Update `RotatingFileSink` to use config value
- [ ] Update `StdoutJsonSink` to use config value
- [ ] Pass config from logger/settings at sink instantiation

### Phase 4: Testing and Documentation

- [ ] Add `tests/unit/test_settings_hot_path.py`
- [ ] Verify no Settings() calls in write/emit paths
- [ ] Update CHANGELOG

---

## Tests

### Unit Tests

- `tests/unit/test_settings_hot_path.py`
  - `test_diagnostics_warn_no_settings_instantiation`
  - `test_worker_process_no_settings_instantiation`
  - `test_sink_write_no_settings_instantiation`
  - `test_configure_diagnostics_overrides_settings`

### Performance Tests

- Benchmark log throughput before/after
- Verify no Settings() allocation in hot path (can use mock/spy)

---

## Definition of Done

### Code Complete

- [ ] No `Settings()` calls in diagnostic emit path
- [ ] No `Settings()` calls in worker process path
- [ ] No `Settings()` calls in sink write paths
- [ ] Configuration passed at creation time

### Quality Assurance

- [ ] Unit tests written and passing
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] No regression in existing tests
- [ ] Performance benchmark shows improvement

### Documentation

- [ ] Code comments explain caching strategy
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Cached config doesn't reflect runtime env changes
   - **Mitigation:** Document that config is read at init; provide explicit refresh mechanism

2. **Risk:** Breaking change for code relying on dynamic Settings() reads
   - **Mitigation:** Internal APIs only; no public API change

3. **Risk:** Initialization order issues with module-level caching
   - **Mitigation:** Use lazy initialization pattern (None check)

### Rollback Plan

If issues occur:
1. Revert to `Settings()` calls with deprecation warning
2. Configuration injection is additive; can coexist with fallback

---

## Related Stories

- **Depends on:** Story 1.23 - Settings hot path caching (initial work)
- **Related:** Story 1.24 - Strict mode drop accounting (uses strict_envelope_mode)

---

## Code Review

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

### Summary

Eliminated Settings() instantiation from hot paths by adding module-level caching to diagnostics and config injection to sinks.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: No Settings() in Diagnostic Emit Path | `diagnostics.py:80-89` - `_is_enabled()` uses `_internal_logging_enabled` cache |
| AC2: No Settings() in Worker/Sink Write Paths | `rotating_file.py:143`, `stdout_json.py:47` - use config values |
| AC3: Configuration Passed at Creation Time | `logger.py:159` - caches at init; sinks receive via constructor |
| AC4: Explicit Refresh Mechanism | `diagnostics.py:93-105` - `configure_diagnostics()` function |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover 100%
- [x] No weak assertions
- [x] No dead code (in changed files)

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-18 | Initial draft from audit findings | Claude |
| 2026-01-18 | Implementation complete, code review passed | Claude |
