# Story 4.41: Sink Failure Signaling and Fallback Policy

**Status:** Ready for Code Review  
**Priority:** High  
**Depends on:** Story 10.6 (Enhanced Default Behaviors)  
**Effort:** 2-3 days

---

## Context / Background

Story 10.6 introduced stderr fallback on sink write failures. The core writers
(`_fanout_writer` at `src/fapilog/__init__.py:566`, `RoutingSinkWriter` at
`src/fapilog/core/routing.py:67`) already catch exceptions and call
`handle_sink_write_failure` from `src/fapilog/plugins/sinks/fallback.py`.

**The problem**: Built-in sinks **swallow errors internally** with patterns like
`except Exception: return None` (see `stdout_json.py:90-92`, `rotating_file.py:223-225`).
Errors are contained before they can propagate to the core, so fallback never
triggers and circuit breaker behavior is ineffective.

We need sinks to **signal failures to the core** instead of swallowing them.
This is a **breaking change** for the 0.4 release.

---

## Scope (In / Out)

### In Scope

- Define explicit sink failure signaling (exception and/or return value).
- Update BaseSink documentation and protocol to reflect the new contract.
- Update built-in sinks to signal failures (not swallow).
- Core should treat signaled failures as failures for fallback/circuit breaker.
- Add tests for failure signaling and fallback integration.
- Document migration guidance for third-party sink authors.

### Out of Scope

- New sink types or new destinations.
- Changing fallback policy behavior (stderr fallback remains the same).
- Changes to unrelated plugin types (enrichers, processors, redactors).

---

## Acceptance Criteria

1. **Explicit failure signaling contract**
   - A sink **signals failure** by raising a `SinkWriteError` (preferred).
   - Alternatively, a sink may return `False` from `write`/`write_serialized`.
   - Sinks must not swallow write failures silently.

   **Return value semantics** (breaking change in 0.4):

   | Return | Meaning | Core action |
   |--------|---------|-------------|
   | `None` / no return | Success | None |
   | `True` | Success | None |
   | `False` | Failure | Trigger fallback, increment circuit breaker |
   | `SinkWriteError` raised | Failure | Trigger fallback, increment circuit breaker |

2. **Core handles failures safely**
   - Core catches sink failures, emits diagnostics, and triggers fallback.
   - No sink failure propagates to user code.
   - Circuit breaker counts a signaled failure as a failure.

3. **Built-in sinks comply**
   - `stdout_json`, `stdout_pretty`, `rotating_file`, `http`, `webhook`, and
     contrib sinks are updated to signal failures.
   - On impossible-to-detect failures, sinks emit diagnostics and return `False`.

4. **Tests**
   - Unit tests cover:
     - `SinkWriteError` handling in fanout and routing writers.
     - `False` return value handling.
     - Updated sink error paths (at least stdout + rotating file).
   - Integration tests confirm fallback triggers on signaled failures.

5. **Docs**
   - Update plugin author docs with the new contract and examples.
   - Add migration guidance for third-party sinks.

---

## Implementation Notes

### SinkWriteError

Add `SinkWriteError` as a subclass of `PluginError` in `src/fapilog/core/errors.py`:

```python
class SinkWriteError(PluginError):
    """Raised when a sink fails to write a log entry."""

    def __init__(
        self,
        message: str,
        sink_name: str | None = None,
        cause: Exception | None = None,
        **kwargs: Any,
    ) -> None:
        super().__init__(
            message,
            plugin_name=sink_name,
            category=ErrorCategory.IO,
            severity=ErrorSeverity.HIGH,
            recovery_strategy=ErrorRecoveryStrategy.FALLBACK,
            cause=cause,
            **kwargs,
        )
```

### BaseSink contract update

Update `src/fapilog/plugins/sinks/__init__.py` docstring:
- Replace "Must never raise upstream; contain errors internally"
- With "Must signal failures via `SinkWriteError` or return `False`. Core catches
  exceptions to protect user code."

### Core writer updates

The core writers already catch exceptions and call `handle_sink_write_failure`.
Add handling for `False` return values:

- `_fanout_writer` (`src/fapilog/__init__.py:534`): Check return value, treat
  `False` as failure.
- `RoutingSinkWriter._write_one` (`src/fapilog/core/routing.py:171`): Same.

### Built-in sink updates

Replace `except Exception: return None` patterns with `raise SinkWriteError`:

```python
# Before (swallows error)
except Exception:
    return None

# After (signals error)
except Exception as e:
    raise SinkWriteError(
        f"Failed to write to {self.name}",
        sink_name=self.name,
        cause=e,
    ) from e
```

Where a sink cannot reliably detect errors (e.g., fire-and-forget UDP), emit
diagnostics and return `False`.

---

## Tasks

- [ ] Add `SinkWriteError` class and exports.
- [ ] Update BaseSink documentation and protocol signature (`-> bool | None`).
- [ ] Update core writers to treat `False` as failure.
- [ ] Update built-in sinks to signal failures:
  - [ ] stdout_json (`src/fapilog/plugins/sinks/stdout_json.py`)
  - [ ] stdout_pretty (`src/fapilog/plugins/sinks/stdout_pretty.py`)
  - [ ] rotating_file (`src/fapilog/plugins/sinks/rotating_file.py`)
  - [ ] http (`src/fapilog/plugins/sinks/http_client.py`)
  - [ ] webhook (`src/fapilog/plugins/sinks/webhook.py`)
  - [ ] audit (`src/fapilog/plugins/sinks/audit.py`)
  - [ ] contrib sinks (`src/fapilog/plugins/sinks/contrib/`):
    - [ ] cloudwatch
    - [ ] loki
    - [ ] postgres
- [ ] Add unit tests for signaled failures (raise + return False).
- [ ] Add integration tests for fallback on signaled failures.
- [ ] Update plugin author docs (`docs/plugins/error-handling.md` or similar).
- [ ] Update CHANGELOG for the new contract.

---

## Tests

### Unit Tests

- `tests/unit/test_sink_failure_signaling.py`
  - fanout writer handles `SinkWriteError`
  - fanout writer handles `False` return values
  - routing writer handles `SinkWriteError`
  - routing writer handles `False` return values

- Update existing sink tests to assert failures signal `SinkWriteError`.

### Integration Tests

- `tests/integration/test_sink_fallback_signaling.py`
  - `get_logger` with a sink that raises `SinkWriteError` triggers fallback
  - `get_logger` with a sink returning `False` triggers fallback

---

## Risks / Rollback / Monitoring

### Risks

- **Breaking change for third-party sinks** that swallow errors (acceptable for 0.4).
- **Behavioral change** in how sink failures are surfaced to diagnostics.

### Mitigation

- Clear migration guidance in release notes and plugin docs.
- `False` return provides a simple migration path for sinks that can't easily
  raise exceptions.

### Rollback

- Revert BaseSink doc changes and built-in sink updates; core `False` handling
  can remain without breaking existing sinks that return `None`.
