# Story 10.26: Builder Advanced Features

**Status:** Complete
**Priority:** Medium
**Depends on:** Story 10.22 (Builder API Parity Foundation)

---

## Context / Background

After Stories 10.23-10.25 cover core settings, sinks, filters, and processors, several advanced features remain without builder support:

- **Sink Routing** - Level-based routing to different sinks
- **Advanced Redactor Config** - Full control over field/regex masking
- **Enricher Config** - Configure built-in enrichers
- **Plugin Management** - Allow/deny lists, external plugins

These features are used by sophisticated deployments that need fine-grained control.

**Current state:**
```python
# Sink routing requires:
settings = Settings(
    sink_routing=SinkRoutingSettings(
        enabled=True,
        rules=[
            RoutingRule(levels=["ERROR", "WARNING"], sinks=["cloudwatch"]),
            RoutingRule(levels=["DEBUG", "INFO"], sinks=["file"]),
        ],
        fallback_sinks=["stdout_json"],
    )
)
```

**Target state:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch("/myapp/errors")
    .add_file("logs/debug")
    .with_sink_routing()
        .route(levels=["ERROR", "WARNING"], to=["cloudwatch"])
        .route(levels=["DEBUG", "INFO"], to=["file"])
        .fallback(["stdout_json"])
    .end_routing()
    .build()
)
```

---

## Scope (In / Out)

### In Scope

- Sink routing builder methods
- Advanced redactor configuration methods
- Enricher configuration methods
- Plugin allow/deny list methods
- Unit tests for each feature

### Out of Scope

- Custom plugin development
- Third-party plugin configuration (use `extra` config)
- Integrity/tamper-evident features (enterprise addon)

---

## Acceptance Criteria

### AC1: Sink Routing Configuration

**Description:** Users can configure level-based sink routing via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch("/errors", region="us-east-1")
    .add_file("logs/all")
    .with_routing(
        rules=[
            {"levels": ["ERROR", "WARNING"], "sinks": ["cloudwatch"]},
            {"levels": ["DEBUG", "INFO"], "sinks": ["rotating_file"]},
        ],
        fallback=["rotating_file"],
        overlap=False,
    )
    .build()
)
```

### AC2: Advanced Redactor Configuration

**Description:** Users can configure redactors with full options.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_field_mask(
        fields=["password", "ssn", "credit_card"],
        mask="[REDACTED]",
        max_depth=10,
    )
    .with_regex_mask(
        patterns=[r"(?i).*secret.*", r"(?i).*token.*"],
        mask="***",
    )
    .with_url_credential_redaction(max_string_length=8192)
    .build()
)
```

### AC3: Enricher Configuration

**Description:** Users can configure enricher behavior.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_enrichers("runtime_info", "context_vars", "kubernetes")
    .configure_enricher("runtime_info", service="my-api", env="prod")
    .build()
)
```

### AC4: Plugin Management

**Description:** Users can control plugin loading.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_plugins(
        allow_external=False,
        allowlist=["rotating_file", "stdout_json"],
        denylist=["experimental_sink"],
    )
    .build()
)
```

### AC5: All Features Chainable

**Description:** Advanced features integrate with basic builder methods.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_level("INFO")
    .with_preset("production")
    .add_cloudwatch("/app/logs")
    .add_file("logs/backup")
    .with_routing(
        rules=[{"levels": ["ERROR"], "sinks": ["cloudwatch"]}],
        fallback=["rotating_file"],
    )
    .with_field_mask(fields=["password"])
    .with_circuit_breaker(enabled=True)
    .build()
)
```

---

## Implementation Notes

### File Changes

```
src/fapilog/builder.py (MODIFIED - add advanced methods)
tests/unit/test_builder_advanced.py (NEW)
```

### New Builder Methods

```python
def with_routing(
    self,
    rules: list[dict[str, Any]],
    *,
    fallback: list[str] | None = None,
    overlap: bool = True,
) -> LoggerBuilder:
    """Configure level-based sink routing.

    Args:
        rules: List of routing rules, each with "levels" and "sinks" keys
        fallback: Sinks to use when no rules match
        overlap: Allow events to match multiple rules (default: True)

    Example:
        >>> builder.with_routing(
        ...     rules=[
        ...         {"levels": ["ERROR"], "sinks": ["cloudwatch"]},
        ...         {"levels": ["INFO", "DEBUG"], "sinks": ["file"]},
        ...     ],
        ...     fallback=["stdout_json"],
        ... )
    """
    # Pass rules as dicts - Settings will validate via RoutingRule model
    routing_config: dict[str, Any] = {
        "enabled": True,
        "rules": rules,  # Let Settings handle validation
        "overlap": overlap,
    }
    if fallback is not None:
        routing_config["fallback_sinks"] = fallback

    self._config["sink_routing"] = routing_config
    return self

def with_field_mask(
    self,
    fields: list[str],
    *,
    mask: str = "***",
    block_on_failure: bool = False,
    max_depth: int = 16,
    max_keys: int = 1000,
) -> LoggerBuilder:
    """Configure field-based redaction.

    Args:
        fields: Field paths to mask (e.g., ["password", "user.ssn"])
        mask: Replacement string (default: "***")
        block_on_failure: Block log entry if redaction fails (default: False)
        max_depth: Maximum nested depth to scan (default: 16)
        max_keys: Maximum keys to scan (default: 1000)

    Example:
        >>> builder.with_field_mask(["password", "api_key"], mask="[REDACTED]")
    """
    redactors = self._config.setdefault("core", {}).setdefault("redactors", [])
    if "field_mask" not in redactors:
        redactors.append("field_mask")

    redactor_config = self._config.setdefault("redactor_config", {})
    redactor_config["field_mask"] = {
        "fields_to_mask": fields,
        "mask_string": mask,
        "block_on_unredactable": block_on_failure,
        "max_depth": max_depth,
        "max_keys_scanned": max_keys,
    }
    return self

def with_regex_mask(
    self,
    patterns: list[str],
    *,
    mask: str = "***",
    block_on_failure: bool = False,
    max_depth: int = 16,
    max_keys: int = 1000,
) -> LoggerBuilder:
    """Configure regex-based field path redaction.

    Note: Patterns match field PATHS (e.g., "context.password"),
    not field content. Use patterns like "(?i).*password.*".

    Args:
        patterns: Regex patterns to match against field paths
        mask: Replacement string (default: "***")
        block_on_failure: Block log entry if redaction fails (default: False)
        max_depth: Maximum nested depth to scan (default: 16)
        max_keys: Maximum keys to scan (default: 1000)

    Example:
        >>> builder.with_regex_mask(["(?i).*password.*", "(?i).*secret.*"])
    """
    redactors = self._config.setdefault("core", {}).setdefault("redactors", [])
    if "regex_mask" not in redactors:
        redactors.append("regex_mask")

    redactor_config = self._config.setdefault("redactor_config", {})
    redactor_config["regex_mask"] = {
        "patterns": patterns,
        "mask_string": mask,
        "block_on_unredactable": block_on_failure,
        "max_depth": max_depth,
        "max_keys_scanned": max_keys,
    }
    return self

def with_url_credential_redaction(
    self,
    *,
    enabled: bool = True,
    max_string_length: int = 4096,
) -> LoggerBuilder:
    """Configure URL credential redaction.

    Scrubs credentials from URLs like "https://user:pass@host/..."

    Args:
        enabled: Enable URL credential redaction (default: True)
        max_string_length: Max string length to parse (default: 4096)

    Example:
        >>> builder.with_url_credential_redaction(max_string_length=8192)
    """
    redactors = self._config.setdefault("core", {}).setdefault("redactors", [])
    if enabled and "url_credentials" not in redactors:
        redactors.append("url_credentials")
    elif not enabled and "url_credentials" in redactors:
        redactors.remove("url_credentials")

    if enabled:
        redactor_config = self._config.setdefault("redactor_config", {})
        redactor_config["url_credentials"] = {
            "max_string_length": max_string_length,
        }
    return self

def configure_enricher(
    self,
    name: str,
    **config: Any,
) -> LoggerBuilder:
    """Configure a specific enricher.

    Args:
        name: Enricher name (e.g., "runtime_info", "context_vars")
        **config: Enricher-specific configuration

    Example:
        >>> builder.configure_enricher("runtime_info", service="my-api")
    """
    enricher_config = self._config.setdefault("enricher_config", {})
    enricher_config[name] = config
    return self

def with_plugins(
    self,
    *,
    enabled: bool = True,
    allow_external: bool = False,
    allowlist: list[str] | None = None,
    denylist: list[str] | None = None,
    validation_mode: str = "disabled",
) -> LoggerBuilder:
    """Configure plugin loading behavior.

    Args:
        enabled: Enable plugin loading (default: True)
        allow_external: Allow entry point plugins (default: False)
        allowlist: Only allow these plugins (empty = all allowed)
        denylist: Block these plugins
        validation_mode: Validation mode ("disabled", "warn", "strict")

    Example:
        >>> builder.with_plugins(allowlist=["rotating_file", "stdout_json"])
    """
    plugins_config: dict[str, Any] = {
        "enabled": enabled,
        "allow_external": allow_external,
        "validation_mode": validation_mode,
    }
    if allowlist is not None:
        plugins_config["allowlist"] = allowlist
    if denylist is not None:
        plugins_config["denylist"] = denylist

    self._config["plugins"] = plugins_config
    return self

def with_redaction_guardrails(
    self,
    *,
    max_depth: int = 6,
    max_keys: int = 5000,
) -> LoggerBuilder:
    """Configure global redaction guardrails.

    Args:
        max_depth: Maximum nested depth for redaction (default: 6)
        max_keys: Maximum keys scanned during redaction (default: 5000)

    Example:
        >>> builder.with_redaction_guardrails(max_depth=10, max_keys=10000)
    """
    core = self._config.setdefault("core", {})
    core["redaction_max_depth"] = max_depth
    core["redaction_max_keys_scanned"] = max_keys
    return self
```

---

## Tasks

### Phase 1: Sink Routing

- [ ] Implement `with_routing()` method
- [ ] Handle RoutingRule model conversion
- [ ] Add unit tests

### Phase 2: Redactor Configuration

- [ ] Implement `with_field_mask()` method
- [ ] Implement `with_regex_mask()` method
- [ ] Implement `with_url_credential_redaction()` method
- [ ] Implement `with_redaction_guardrails()` method
- [ ] Add unit tests for each

### Phase 3: Enricher and Plugin Config

- [ ] Implement `configure_enricher()` method
- [ ] Implement `with_plugins()` method
- [ ] Add unit tests

### Phase 4: Integration

- [ ] Test all features together
- [ ] Verify Settings equivalence
- [ ] Update parity test coverage

---

## Tests

### Unit Tests

- `tests/unit/test_builder_advanced.py`
  - test_with_routing_basic
  - test_with_routing_fallback
  - test_with_routing_no_overlap
  - test_with_field_mask_basic
  - test_with_field_mask_custom_options
  - test_with_regex_mask
  - test_with_url_credential_redaction_disable
  - test_configure_enricher
  - test_with_plugins_allowlist
  - test_with_plugins_denylist
  - test_with_redaction_guardrails
  - test_advanced_features_chainable

---

## Definition of Done

### Code Complete

- [ ] All 7 advanced methods implemented
- [ ] All methods chainable
- [ ] All methods have docstrings

### Quality Assurance

- [ ] Unit tests for each method
- [ ] `ruff check` passes
- [ ] `mypy` passes

### Documentation

- [ ] Docstrings complete
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Routing rule complexity
   - **Mitigation:** Keep rule format simple (dict-based)

### Rollback Plan

If issues occur:
1. Revert builder.py changes
2. Advanced features remain configurable via Settings

---

## Related Stories

- **Depends on:** Story 10.22 - Builder API Parity Foundation
- **Related:** Stories 10.23-10.25

---

## Code Review

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

### Summary

Reviewed implementation of 7 builder methods for advanced features including sink routing, redaction configuration, enricher configuration, and plugin management.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Sink Routing | `builder.py:672-703` - `with_routing()` accepts rules, fallback, overlap |
| AC2: Advanced Redactors | `builder.py:705-814` - `with_field_mask()`, `with_regex_mask()`, `with_url_credential_redaction()` |
| AC3: Enricher Config | `builder.py:830-844` - `configure_enricher(name, **config)` |
| AC4: Plugin Management | `builder.py:846-883` - `with_plugins()` with all options |
| AC5: All Chainable | `test_builder_advanced.py:224-265` - chains all methods |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] diff-cover >= 90% (100%)
- [x] No weak assertions
- [x] No dead code
- [x] Builder parity check passed

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-21 | Code review passed | Claude |
| 2026-01-20 | Initial draft | Claude |
