# Story 5.9: Add flush() Method to Plugin Protocols

## Status: Draft

## Priority: Low

## Estimated Effort: Small (1 day)

## Dependencies: None

## Epic: API Refinement

---

## Context

The current plugin protocols (`BaseSink`, `BaseEnricher`, `BaseRedactor`, `BaseProcessor`) have `start()` and `stop()` lifecycle methods, but lack an explicit `flush()` method.

For sinks that buffer data (HTTP batching, file buffering), there's a need to:

- Force flush on demand without stopping
- Flush before graceful shutdown
- Flush on signal handlers (SIGTERM)

Currently, sinks must implement this internally without protocol guidance.

---

## Problem Statement

1. No standard way to request buffered sinks to flush
2. Inconsistent flush behavior across sink implementations
3. No protocol contract for flush semantics
4. Users cannot request flush without stopping logger

---

## Acceptance Criteria

### AC1: Protocol Update

- [ ] Add optional `async def flush() -> None` to `BaseSink` protocol
- [ ] Default implementation is no-op (backwards compatible)
- [ ] Document flush semantics and timing guarantees

### AC2: Built-in Sink Updates

- [ ] `StdoutJsonSink.flush()` - calls sys.stdout.flush()
- [ ] `RotatingFileSink.flush()` - flushes file buffer
- [ ] `HttpSink.flush()` - flushes batch immediately
- [ ] `WebhookSink.flush()` - flushes batch immediately

### AC3: Logger Integration

- [ ] Add `logger.flush_sinks()` method
- [ ] Flush called automatically on graceful shutdown
- [ ] Flush called on SIGTERM/SIGINT handlers

### AC4: Documentation

- [ ] Document flush() in protocol documentation
- [ ] Add flush examples to sink authoring guide
- [ ] Update lifecycle diagram

---

## Technical Design

### 1. Protocol Update

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

@runtime_checkable
class BaseSink(Protocol):
    """Authoring contract for sinks that emit finalized log entries.

    ...existing docstring...
    """

    name: str

    async def start(self) -> None:
        """Initialize resources for the sink."""

    async def stop(self) -> None:
        """Flush and release resources for the sink."""

    async def write(self, _entry: dict) -> None:
        """Emit a single structured JSON-serializable mapping."""
        ...

    async def flush(self) -> None:
        """Flush any buffered data to the destination.

        This method is called:
        - On explicit flush request from the logger
        - Before graceful shutdown (stop() should also flush)
        - On signal handlers for graceful termination

        Implementations should:
        - Complete any pending writes
        - Not block indefinitely (use timeouts)
        - Be safe to call multiple times
        - Not raise exceptions (contain errors)

        Default implementation is no-op for sinks without buffering.
        """
        pass

    async def health_check(self) -> bool:
        """Return True if the sink is healthy."""
        return True
```

### 2. Built-in Sink Updates

```python
# Example: HttpSink with flush

class HttpSink:
    # ... existing implementation ...

    async def flush(self) -> None:
        """Flush any buffered batch immediately."""
        try:
            await self._flush_batch()
        except Exception:
            # Contain errors during flush
            pass
```

```python
# Example: RotatingFileSink with flush

class RotatingFileSink:
    # ... existing implementation ...

    async def flush(self) -> None:
        """Flush file buffer to disk."""
        try:
            async with self._lock:
                if self._active_file is not None:
                    file_obj = self._active_file
                    await asyncio.to_thread(file_obj.flush)
        except Exception:
            pass
```

### 3. Logger Integration

```python
# Update __init__.py

class SyncLoggerFacade:
    # ... existing implementation ...

    def flush_sinks(self) -> None:
        """Flush all sinks immediately.

        Useful for ensuring logs are written before:
        - Critical operations
        - Process shutdown
        - Signal handlers
        """
        async def _flush_all() -> None:
            for sink in self._sinks:
                if hasattr(sink, "flush"):
                    try:
                        await sink.flush()
                    except Exception:
                        pass

        self._schedule_async(_flush_all)

    async def aflush_sinks(self) -> None:
        """Async version of flush_sinks."""
        for sink in self._sinks:
            if hasattr(sink, "flush"):
                try:
                    await sink.flush()
                except Exception:
                    pass
```

### 4. Signal Handler Integration

```python
# Update runtime() context manager

@contextmanager
def runtime(
    *,
    settings: _Settings | None = None,
    handle_signals: bool = True,
) -> Iterator[SyncLoggerFacade]:
    """Context manager that initializes and drains the default runtime."""

    logger = get_logger(settings=settings)

    def _signal_handler(signum: int, frame: Any) -> None:
        """Graceful shutdown on signal."""
        try:
            logger.flush_sinks()
        except Exception:
            pass

    old_handlers = {}
    if handle_signals:
        import signal
        for sig in (signal.SIGTERM, signal.SIGINT):
            try:
                old_handlers[sig] = signal.signal(sig, _signal_handler)
            except (OSError, ValueError):
                pass

    try:
        yield logger
    finally:
        # Restore signal handlers
        for sig, handler in old_handlers.items():
            try:
                signal.signal(sig, handler)
            except (OSError, ValueError):
                pass

        # Stop and drain
        # ... existing cleanup ...
```

---

## Test Plan

### Unit Tests

1. **test_sink_flush.py**

   - Test flush() is called on flush_sinks()
   - Test flush() errors are contained
   - Test flush() is idempotent

2. **test_builtin_sink_flush.py**
   - Test StdoutJsonSink.flush()
   - Test RotatingFileSink.flush()
   - Test HttpSink.flush() flushes batch

### Integration Tests

1. **test_flush_before_shutdown.py**
   - Verify data is flushed before stop
   - Verify signal handler triggers flush

---

## Documentation Updates

1. Update `docs/plugins/sinks.md` with flush() documentation
2. Add flush examples to sink authoring guide
3. Update lifecycle diagram to show flush

---

## Backwards Compatibility

- `flush()` is optional with default no-op
- Existing sinks continue to work without modification
- `stop()` continues to be the primary cleanup method

---

## Related Stories

- Story 5.6: HTTP sink batching (uses flush)
- Story 5.5: Cloud sink examples (should implement flush)

