# Story 10.25: Builder Filter & Processor Methods

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

---

## Context / Background

The current builder has `with_filters(*filters: str)` which only enables filters by name without configuration. Users need builder methods to configure:

**Filters:**
- `sampling` - Probabilistic sampling with rate and seed
- `rate_limit` - Token bucket rate limiting
- `adaptive_sampling` - Dynamic sampling based on load
- `trace_sampling` - Distributed trace-aware sampling
- `first_occurrence` - Deduplicate repeated messages

**Processors:**
- `size_guard` - Payload size limiting with truncation/drop/warn actions

**Current state:**
```python
# To configure sampling filter, users must:
settings = Settings(
    core=CoreSettings(filters=["sampling"]),
    filter_config=Settings.FilterConfig(
        sampling={"sample_rate": 0.1, "seed": 42}
    )
)
```

**Target state:**
```python
logger = (
    LoggerBuilder()
    .with_sampling(rate=0.1, seed=42)
    .build()
)
```

---

## Scope (In / Out)

### In Scope

- `with_sampling()` builder method
- `with_rate_limit()` builder method
- `with_size_guard()` builder method
- Basic `with_adaptive_sampling()` and `with_trace_sampling()` methods
- Unit tests for each method

### Out of Scope

- New filter/processor implementations
- Third-party filter/processor configuration
- Complex filter chaining logic

---

## Acceptance Criteria

### AC1: Sampling Filter Builder

**Description:** Users can configure sampling filter via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_sampling(rate=0.1, seed=42)
    .build()
)

# Equivalent to Settings with sampling filter config
```

### AC2: Rate Limit Filter Builder

**Description:** Users can configure rate limiting via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_rate_limit(
        capacity=100,
        refill_rate=10.0,
        key_field="user_id",
        overflow_action="drop",
    )
    .build()
)
```

### AC3: Size Guard Processor Builder

**Description:** Users can configure payload size limits via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_size_guard(
        max_bytes="256 KB",
        action="truncate",
        preserve_fields=["level", "timestamp", "correlation_id"],
    )
    .build()
)
```

### AC4: Adaptive Sampling Builder

**Description:** Users can configure adaptive sampling via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_adaptive_sampling(
        min_rate=0.01,
        max_rate=1.0,
        target_rate=1000,  # events/sec
    )
    .build()
)
```

### AC5: First Occurrence Filter Builder

**Description:** Users can configure first-occurrence deduplication via builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_first_occurrence(
        window_seconds=60.0,
        max_entries=5000,
    )
    .build()
)
```

### AC6: Filters and Processors Chainable

**Description:** Multiple filters and processors can be chained.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .with_level("INFO")
    .with_sampling(rate=0.5)
    .with_rate_limit(capacity=1000)
    .with_size_guard(max_bytes="1 MB")
    .add_stdout()
    .build()
)
```

---

## Implementation Notes

### File Changes

```
src/fapilog/builder.py (MODIFIED - add filter/processor methods)
tests/unit/test_builder_filters.py (NEW)
tests/unit/test_builder_processors.py (NEW)
```

### New Builder Methods

```python
def with_sampling(
    self,
    rate: float = 1.0,
    *,
    seed: int | None = None,
) -> LoggerBuilder:
    """Configure probabilistic sampling filter.

    Args:
        rate: Sample rate 0.0-1.0 (1.0 = keep all, 0.1 = keep 10%)
        seed: Random seed for reproducibility

    Example:
        >>> builder.with_sampling(rate=0.1)  # Keep 10% of logs
    """
    filters = self._config.setdefault("core", {}).setdefault("filters", [])
    if "sampling" not in filters:
        filters.append("sampling")

    filter_config = self._config.setdefault("filter_config", {})
    sampling_config: dict[str, Any] = {"sample_rate": rate}
    if seed is not None:
        sampling_config["seed"] = seed
    filter_config["sampling"] = sampling_config

    return self

def with_rate_limit(
    self,
    capacity: int = 10,
    *,
    refill_rate: float = 5.0,
    key_field: str | None = None,
    max_keys: int = 10000,
    overflow_action: str = "drop",
) -> LoggerBuilder:
    """Configure token bucket rate limiting filter.

    Args:
        capacity: Token bucket capacity (default: 10)
        refill_rate: Tokens refilled per second (default: 5.0)
        key_field: Event field for partitioning buckets
        max_keys: Maximum buckets to track (default: 10000)
        overflow_action: Action on overflow ("drop" or "mark")

    Example:
        >>> builder.with_rate_limit(capacity=100, refill_rate=10.0)
    """
    filters = self._config.setdefault("core", {}).setdefault("filters", [])
    if "rate_limit" not in filters:
        filters.append("rate_limit")

    filter_config = self._config.setdefault("filter_config", {})
    rate_limit_config: dict[str, Any] = {
        "capacity": capacity,
        "refill_rate_per_sec": refill_rate,
        "max_keys": max_keys,
        "overflow_action": overflow_action,
    }
    if key_field is not None:
        rate_limit_config["key_field"] = key_field
    filter_config["rate_limit"] = rate_limit_config

    return self

def with_adaptive_sampling(
    self,
    min_rate: float = 0.01,
    max_rate: float = 1.0,
    *,
    target_events_per_sec: float = 1000.0,
    window_seconds: float = 60.0,
) -> LoggerBuilder:
    """Configure adaptive sampling based on event rate.

    Args:
        min_rate: Minimum sample rate (default: 0.01)
        max_rate: Maximum sample rate (default: 1.0)
        target_events_per_sec: Target event throughput (default: 1000)
        window_seconds: Measurement window (default: 60)

    Example:
        >>> builder.with_adaptive_sampling(target_events_per_sec=500)
    """
    filters = self._config.setdefault("core", {}).setdefault("filters", [])
    if "adaptive_sampling" not in filters:
        filters.append("adaptive_sampling")

    filter_config = self._config.setdefault("filter_config", {})
    filter_config["adaptive_sampling"] = {
        "min_rate": min_rate,
        "max_rate": max_rate,
        "target_events_per_sec": target_events_per_sec,
        "window_seconds": window_seconds,
    }

    return self

def with_trace_sampling(
    self,
    default_rate: float = 1.0,
    *,
    honor_upstream: bool = True,
) -> LoggerBuilder:
    """Configure distributed trace-aware sampling.

    Args:
        default_rate: Default sample rate when no trace context (default: 1.0)
        honor_upstream: Honor upstream sampling decisions (default: True)

    Example:
        >>> builder.with_trace_sampling(default_rate=0.1)
    """
    filters = self._config.setdefault("core", {}).setdefault("filters", [])
    if "trace_sampling" not in filters:
        filters.append("trace_sampling")

    filter_config = self._config.setdefault("filter_config", {})
    filter_config["trace_sampling"] = {
        "default_rate": default_rate,
        "honor_upstream": honor_upstream,
    }

    return self

def with_first_occurrence(
    self,
    window_seconds: float = 300.0,
    *,
    max_entries: int = 10000,
    key_fields: list[str] | None = None,
) -> LoggerBuilder:
    """Configure first-occurrence deduplication filter.

    Args:
        window_seconds: Deduplication window (default: 300 = 5 minutes)
        max_entries: Maximum tracked messages (default: 10000)
        key_fields: Fields to use as dedup key (default: message only)

    Example:
        >>> builder.with_first_occurrence(window_seconds=60)
    """
    filters = self._config.setdefault("core", {}).setdefault("filters", [])
    if "first_occurrence" not in filters:
        filters.append("first_occurrence")

    filter_config = self._config.setdefault("filter_config", {})
    config: dict[str, Any] = {
        "window_seconds": window_seconds,
        "max_entries": max_entries,
    }
    if key_fields is not None:
        config["key_fields"] = key_fields
    filter_config["first_occurrence"] = config

    return self

def with_size_guard(
    self,
    max_bytes: str | int = "256 KB",
    *,
    action: Literal["truncate", "drop", "warn"] = "truncate",
    preserve_fields: list[str] | None = None,
) -> LoggerBuilder:
    """Configure payload size limiting processor.

    Args:
        max_bytes: Maximum payload size ("256 KB" or 262144)
        action: Action on oversized payloads ("truncate", "drop", "warn")
        preserve_fields: Fields to never remove during truncation

    Example:
        >>> builder.with_size_guard(max_bytes="1 MB", action="truncate")
    """
    processors = self._config.setdefault("core", {}).setdefault("processors", [])
    if "size_guard" not in processors:
        processors.append("size_guard")

    processor_config = self._config.setdefault("processor_config", {})
    size_guard_config: dict[str, Any] = {
        "max_bytes": max_bytes,  # Will be parsed by Settings
        "action": action,
    }
    if preserve_fields is not None:
        size_guard_config["preserve_fields"] = preserve_fields
    else:
        size_guard_config["preserve_fields"] = [
            "level", "timestamp", "logger", "correlation_id"
        ]
    processor_config["size_guard"] = size_guard_config

    return self
```

---

## Tasks

### Phase 1: Sampling Filters

- [ ] Implement `with_sampling()` method
- [ ] Implement `with_adaptive_sampling()` method
- [ ] Implement `with_trace_sampling()` method
- [ ] Add unit tests for each

### Phase 2: Rate Limiting and Dedup

- [ ] Implement `with_rate_limit()` method
- [ ] Implement `with_first_occurrence()` method
- [ ] Add unit tests for each

### Phase 3: Processors

- [ ] Implement `with_size_guard()` method
- [ ] Add unit tests with size string parsing

### Phase 4: Integration

- [ ] Test filter/processor chaining
- [ ] Verify Settings equivalence
- [ ] Update parity test coverage

---

## Tests

### Unit Tests

- `tests/unit/test_builder_filters.py`
  - test_with_sampling_basic
  - test_with_sampling_with_seed
  - test_with_rate_limit_basic
  - test_with_rate_limit_with_key_field
  - test_with_adaptive_sampling
  - test_with_trace_sampling
  - test_with_first_occurrence
  - test_multiple_filters_chainable

- `tests/unit/test_builder_processors.py`
  - test_with_size_guard_string_size
  - test_with_size_guard_int_size
  - test_with_size_guard_preserve_fields
  - test_size_guard_action_options

---

## Definition of Done

### Code Complete

- [ ] All 6 filter/processor 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:** Filter config schema drift
   - **Mitigation:** Contract tests verify config validity

### Rollback Plan

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

---

## Related Stories

- **Depends on:** Story 10.22 - Builder API Parity Foundation
- **Related:** Story 10.23 - Builder Core Settings Methods
- **Related:** Story 10.24 - Builder Cloud Sink Methods

---

## Code Review

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

### Summary

Added 6 builder methods for configuring filters and processors with comprehensive test coverage.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: Sampling Filter Builder | `builder.py:232-257` - `with_sampling(rate, seed)` |
| AC2: Rate Limit Filter Builder | `builder.py:319-355` - `with_rate_limit()` with all params |
| AC3: Size Guard Processor Builder | `builder.py:389-426` - `with_size_guard()` |
| AC4: Adaptive Sampling Builder | `builder.py:259-290` - `with_adaptive_sampling()` |
| AC5: First Occurrence Filter Builder | `builder.py:357-387` - `with_first_occurrence()` |
| AC6: Filters and Processors Chainable | `test_builder_filters.py:367-378` - full chain test |

### Quality Gates

- [x] ruff check passed
- [x] ruff format passed
- [x] mypy passed
- [x] 41 tests passed
- [x] No weak assertions
- [x] No dead code
- [x] Builder parity check passed

---

## Change Log

| Date | Change | Author |
|------|--------|--------|
| 2026-01-21 | Marked complete after code review | Claude |
| 2026-01-20 | Initial draft | Claude |
