# Story 10.24: Builder Cloud Sink Methods

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

---

## Context / Background

The current builder has `add_file()`, `add_stdout()`, `add_http()`, and `add_webhook()` methods. However, fapilog supports several cloud sinks that require verbose Settings configuration:

- **CloudWatch** - AWS CloudWatch Logs integration
- **Loki** - Grafana Loki integration
- **Postgres** - PostgreSQL structured log storage

These are commonly used in production and should have first-class builder support.

**Current state:**
```python
# To use CloudWatch, users must:
from fapilog import Settings, get_logger
from fapilog.core.settings import CloudWatchSinkSettings

settings = Settings(
    core=CoreSettings(sinks=["cloudwatch"]),
    sink_config=Settings.SinkConfig(
        cloudwatch=CloudWatchSinkSettings(
            log_group_name="/myapp/prod",
            region="us-east-1",
        )
    )
)
logger = get_logger(settings=settings)
```

**Target state:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch(
        log_group="/myapp/prod",
        region="us-east-1",
    )
    .build()
)
```

---

## Scope (In / Out)

### In Scope

- `add_cloudwatch()` builder method with all CloudWatchSinkSettings options
- `add_loki()` builder method with all LokiSinkSettings options
- `add_postgres()` builder method with all PostgresSinkSettings options
- Unit tests for each method
- Human-readable parameter names where applicable
- Duration parameters accept both strings ("5s") and floats via `_parse_duration()`

### Out of Scope

- New sink implementations (sinks already exist)
- Third-party sink registration (handled by `extra` config)
- Sink routing configuration (Story 10.26)

---

## Acceptance Criteria

### AC1: CloudWatch Sink Builder

**Description:** Users can add CloudWatch sink via fluent builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch(
        log_group="/myapp/prod",
        stream="web-server-1",
        region="us-east-1",
        batch_size=100,
        batch_timeout="5s",
        create_group=True,
        create_stream=True,
    )
    .build()
)

# Equivalent to Settings with CloudWatchSinkSettings
```

### AC2: Loki Sink Builder

**Description:** Users can add Loki sink via fluent builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_loki(
        url="http://loki:3100",
        tenant_id="myapp",
        labels={"service": "api", "env": "prod"},
        label_keys=["level", "logger"],
        batch_size=100,
        auth_token="secret",
    )
    .build()
)

# Equivalent to Settings with LokiSinkSettings
```

### AC3: Postgres Sink Builder

**Description:** Users can add PostgreSQL sink via fluent builder.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_postgres(
        host="db.example.com",
        database="logs",
        user="logger",
        password="secret",
        table="app_logs",
        batch_size=100,
    )
    .build()
)

# Or with DSN:
logger = (
    LoggerBuilder()
    .add_postgres(dsn="postgresql://user:pass@host/db")
    .build()
)
```

### AC4: Circuit Breaker Integration

**Description:** Cloud sink builders support per-sink circuit breaker config.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch(
        log_group="/myapp/prod",
        circuit_breaker=True,
        circuit_breaker_threshold=3,
    )
    .build()
)
```

### AC5: Multiple Sinks Chainable

**Description:** Users can add multiple cloud sinks in one builder chain.

**Validation:**
```python
logger = (
    LoggerBuilder()
    .add_cloudwatch(log_group="/myapp/prod", region="us-east-1")
    .add_loki(url="http://loki:3100")
    .add_file("logs/backup")
    .build()
)
```

### AC6: Parity Test Passes for Sink Config

**Description:** The parity test passes for all SinkConfig types.

**Validation:**
```bash
pytest tests/test_builder_parity.py::test_all_sink_configs_have_builder_methods -v
# Should pass
```

---

## Implementation Notes

### File Changes

```
src/fapilog/builder.py (MODIFIED - add 3 sink methods)
tests/unit/test_builder_cloud_sinks.py (NEW)
tests/test_builder_parity.py (MODIFIED - update sink coverage)
```

### New Builder Methods

```python
def add_cloudwatch(
    self,
    log_group: str,
    *,
    stream: str | None = None,
    region: str | None = None,
    endpoint_url: str | None = None,
    batch_size: int = 100,
    batch_timeout: str | float = "5s",
    max_retries: int = 3,
    retry_delay: str | float = "0.5s",
    create_group: bool = True,
    create_stream: bool = True,
    circuit_breaker: bool = True,
    circuit_breaker_threshold: int = 5,
) -> LoggerBuilder:
    """Add AWS CloudWatch Logs sink.

    Args:
        log_group: CloudWatch log group name (required)
        stream: Log stream name (auto-generated if not provided)
        region: AWS region (uses default if not provided)
        endpoint_url: Custom endpoint (e.g., LocalStack)
        batch_size: Events per batch (default: 100)
        batch_timeout: Batch flush timeout ("5s" or 5.0)
        max_retries: Max retries for PutLogEvents (default: 3)
        retry_delay: Base delay for backoff ("0.5s" or 0.5)
        create_group: Create log group if missing (default: True)
        create_stream: Create log stream if missing (default: True)
        circuit_breaker: Enable circuit breaker (default: True)
        circuit_breaker_threshold: Failures before opening (default: 5)

    Example:
        >>> builder.add_cloudwatch("/myapp/prod", region="us-east-1")
    """
    config: dict[str, Any] = {
        "log_group_name": log_group,
        "batch_size": batch_size,
        "batch_timeout_seconds": self._parse_duration(batch_timeout),
        "max_retries": max_retries,
        "retry_base_delay": self._parse_duration(retry_delay),
        "create_log_group": create_group,
        "create_log_stream": create_stream,
        "circuit_breaker_enabled": circuit_breaker,
        "circuit_breaker_threshold": circuit_breaker_threshold,
    }

    if stream is not None:
        config["log_stream_name"] = stream
    if region is not None:
        config["region"] = region
    if endpoint_url is not None:
        config["endpoint_url"] = endpoint_url

    self._sinks.append({"name": "cloudwatch", "config": config})
    return self

def add_loki(
    self,
    url: str = "http://localhost:3100",
    *,
    tenant_id: str | None = None,
    labels: dict[str, str] | None = None,
    label_keys: list[str] | None = None,
    batch_size: int = 100,
    batch_timeout: str | float = "5s",
    timeout: str | float = "10s",
    max_retries: int = 3,
    retry_delay: str | float = "0.5s",
    auth_username: str | None = None,
    auth_password: str | None = None,
    auth_token: str | None = None,
    circuit_breaker: bool = True,
    circuit_breaker_threshold: int = 5,
) -> LoggerBuilder:
    """Add Grafana Loki sink.

    Args:
        url: Loki push endpoint (default: http://localhost:3100)
        tenant_id: Multi-tenant identifier
        labels: Static labels for log streams
        label_keys: Event keys to promote to labels
        batch_size: Events per batch (default: 100)
        batch_timeout: Batch flush timeout ("5s" or 5.0)
        timeout: HTTP request timeout ("10s" or 10.0)
        max_retries: Max retries on failure (default: 3)
        retry_delay: Base delay for backoff ("0.5s" or 0.5)
        auth_username: Basic auth username
        auth_password: Basic auth password
        auth_token: Bearer token
        circuit_breaker: Enable circuit breaker (default: True)
        circuit_breaker_threshold: Failures before opening (default: 5)

    Example:
        >>> builder.add_loki("http://loki:3100", tenant_id="myapp")
    """
    config: dict[str, Any] = {
        "url": url,
        "batch_size": batch_size,
        "batch_timeout_seconds": self._parse_duration(batch_timeout),
        "timeout_seconds": self._parse_duration(timeout),
        "max_retries": max_retries,
        "retry_base_delay": self._parse_duration(retry_delay),
        "circuit_breaker_enabled": circuit_breaker,
        "circuit_breaker_threshold": circuit_breaker_threshold,
    }

    if tenant_id is not None:
        config["tenant_id"] = tenant_id
    if labels is not None:
        config["labels"] = labels
    if label_keys is not None:
        config["label_keys"] = label_keys
    if auth_username is not None:
        config["auth_username"] = auth_username
    if auth_password is not None:
        config["auth_password"] = auth_password
    if auth_token is not None:
        config["auth_token"] = auth_token

    self._sinks.append({"name": "loki", "config": config})
    return self

def add_postgres(
    self,
    dsn: str | None = None,
    *,
    host: str = "localhost",
    port: int = 5432,
    database: str = "fapilog",
    user: str = "fapilog",
    password: str | None = None,
    table: str = "logs",
    schema: str = "public",
    batch_size: int = 100,
    batch_timeout: str | float = "5s",
    max_retries: int = 3,
    retry_delay: str | float = "0.5s",
    min_pool: int = 2,
    max_pool: int = 10,
    pool_acquire_timeout: str | float = "10s",
    create_table: bool = True,
    use_jsonb: bool = True,
    circuit_breaker: bool = True,
    circuit_breaker_threshold: int = 5,
) -> LoggerBuilder:
    """Add PostgreSQL sink for structured log storage.

    Args:
        dsn: Full connection string (overrides host/port/database/user/password)
        host: Database host (default: localhost)
        port: Database port (default: 5432)
        database: Database name (default: fapilog)
        user: Database user (default: fapilog)
        password: Database password
        table: Target table name (default: logs)
        schema: Database schema (default: public)
        batch_size: Events per batch (default: 100)
        batch_timeout: Batch flush timeout ("5s" or 5.0)
        max_retries: Max retries on failure (default: 3)
        retry_delay: Base delay for backoff ("0.5s" or 0.5)
        min_pool: Minimum pool connections (default: 2)
        max_pool: Maximum pool connections (default: 10)
        pool_acquire_timeout: Timeout for acquiring connections ("10s" or 10.0)
        create_table: Auto-create table if missing (default: True)
        use_jsonb: Use JSONB column type (default: True)
        circuit_breaker: Enable circuit breaker (default: True)
        circuit_breaker_threshold: Failures before opening (default: 5)

    Example:
        >>> builder.add_postgres(dsn="postgresql://user:pass@host/db")
        >>> builder.add_postgres(host="db.example.com", database="logs")
    """
    config: dict[str, Any] = {
        "host": host,
        "port": port,
        "database": database,
        "user": user,
        "table_name": table,
        "schema_name": schema,
        "batch_size": batch_size,
        "batch_timeout_seconds": self._parse_duration(batch_timeout),
        "max_retries": max_retries,
        "retry_base_delay": self._parse_duration(retry_delay),
        "min_pool_size": min_pool,
        "max_pool_size": max_pool,
        "pool_acquire_timeout": self._parse_duration(pool_acquire_timeout),
        "create_table": create_table,
        "use_jsonb": use_jsonb,
        "circuit_breaker_enabled": circuit_breaker,
        "circuit_breaker_threshold": circuit_breaker_threshold,
    }

    if dsn is not None:
        config["dsn"] = dsn
    if password is not None:
        config["password"] = password

    self._sinks.append({"name": "postgres", "config": config})
    return self
```

---

## Tasks

### Phase 1: CloudWatch

- [ ] Implement `add_cloudwatch()` method
- [ ] Map all CloudWatchSinkSettings fields to parameters
- [ ] Add unit tests for basic usage
- [ ] Add unit tests for all optional parameters

### Phase 2: Loki

- [ ] Implement `add_loki()` method
- [ ] Map all LokiSinkSettings fields to parameters
- [ ] Add unit tests for basic usage
- [ ] Add unit tests for auth options

### Phase 3: Postgres

- [ ] Implement `add_postgres()` method
- [ ] Support both DSN and individual parameters
- [ ] Map all PostgresSinkSettings fields to parameters
- [ ] Add unit tests for DSN usage
- [ ] Add unit tests for parameter usage

### Phase 4: Integration

- [ ] Update parity test sink coverage mapping
- [ ] Test multiple sinks in one chain
- [ ] Verify Settings equivalence
- [ ] Run full test suite

---

## Tests

### Unit Tests

- `tests/unit/test_builder_cloud_sinks.py`
  - test_add_cloudwatch_basic
  - test_add_cloudwatch_all_options
  - test_add_cloudwatch_circuit_breaker
  - test_add_loki_basic
  - test_add_loki_with_auth_token
  - test_add_loki_with_labels
  - test_add_postgres_with_dsn
  - test_add_postgres_with_params
  - test_add_postgres_circuit_breaker
  - test_multiple_cloud_sinks_chainable
  - test_cloud_sinks_produce_valid_settings

### Contract Tests

- `tests/integration/test_builder_sink_contract.py`
  - test_cloudwatch_builder_matches_settings
  - test_loki_builder_matches_settings
  - test_postgres_builder_matches_settings

---

## Definition of Done

### Code Complete

- [ ] `add_cloudwatch()` implemented with all options
- [ ] `add_loki()` implemented with all options
- [ ] `add_postgres()` implemented with all options
- [ ] All methods have docstrings with examples

### Quality Assurance

- [ ] Unit tests for each method
- [ ] Contract tests verifying Settings equivalence
- [ ] `ruff check` passes
- [ ] `mypy` passes
- [ ] Parity test for SinkConfig passes

### Documentation

- [ ] Docstrings complete
- [ ] CHANGELOG updated

---

## Risks / Rollback

### Risks

1. **Risk:** Cloud sink dependencies not installed
   - **Mitigation:** Builder adds sink to config; import errors happen at runtime

2. **Risk:** Parameter naming diverges from Settings
   - **Mitigation:** Document mapping in implementation notes

### Rollback Plan

If issues occur:
1. Revert builder.py changes
2. Cloud sinks 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.25 - Builder Filter & Processor Methods
- **Enables:** Story 10.27 - Builder API Parity Enforcement (partial)

---

## Code Review

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

### Summary

Reviewed implementation of three cloud sink builder methods with full parameter coverage and comprehensive unit tests.

### AC Verification

| Criterion | Evidence |
|-----------|----------|
| AC1: CloudWatch Sink Builder | `builder.py:476-539` - all params mapped |
| AC2: Loki Sink Builder | `builder.py:541-604` - labels, auth options included |
| AC3: Postgres Sink Builder | `builder.py:606-687` - DSN and individual params supported |
| AC4: Circuit Breaker Integration | All methods have circuit_breaker params |
| AC5: Multiple Sinks Chainable | `test_builder_cloud_sinks.py:261-293` |
| AC6: Parity Test Passes | `test_builder_parity.py:280-291` - xfail removed |

### 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
- [x] Builder parity check passed

---

## Change Log

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