# Plugin Author Quickstart Create custom fapilog plugins in 10 minutes. ## Overview Fapilog supports five plugin types: | Type | Purpose | Method | |------|---------|--------| | **Sink** | Output destinations (file, HTTP, cloud) | `write()` | | **Enricher** | Add context/metadata to events | `enrich()` | | **Processor** | Transform serialized data | `process()` | | **Redactor** | Mask sensitive data | `redact()` | | **Filter** | Drop/pass events | `filter()` | All plugin methods are **async**. Lifecycle methods (`start`, `stop`) are optional but recommended. ## Minimal Sink Plugin Here's a complete sink plugin in ~20 lines: ```python # my_sink.py from typing import Any class MySink: """Minimal sink that prints events.""" name = "my_sink" # Required: unique plugin name async def start(self) -> None: """Called when logger starts.""" print("MySink started") async def stop(self) -> None: """Called when logger stops.""" print("MySink stopped") async def write(self, entry: dict[str, Any]) -> None: """Write a log event.""" print(f"LOG: {entry.get('message', entry)}") ``` ### Using Your Plugin ```python import fapilog from my_sink import MySink logger = fapilog.get_logger(sinks=[MySink()]) logger.info("Hello from my sink!") ``` ## Registering via Entry Points For distributable plugins, register via `pyproject.toml`: ```toml [project] name = "fapilog-my-sink" version = "1.0.0" dependencies = ["fapilog>=0.7.0"] [project.entry-points."fapilog.sinks"] my_sink = "my_sink:MySink" ``` ### Entry Point Groups | Plugin Type | Entry Point Group | |-------------|-------------------| | Sink | `fapilog.sinks` | | Enricher | `fapilog.enrichers` | | Processor | `fapilog.processors` | | Redactor | `fapilog.redactors` | | Filter | `fapilog.filters` | ## Plugin Validation Modes Fapilog validates plugins at load time. Configure validation strictness: ```python from fapilog import Settings # Modes: "disabled", "warn", "strict" settings = Settings(plugins={"validation_mode": "warn"}) ``` | Mode | Invalid Plugin Behavior | |------|------------------------| | `disabled` | Load anyway, may fail at runtime | | `warn` | Load with diagnostic warning | | `strict` | Reject plugin, raise error | ### What Gets Validated - Required `name` attribute exists and is a string - Required methods exist and are async (`write`, `enrich`, etc.) - Method signatures have correct parameters - Optional lifecycle methods are async if present ## Plugin Protocols ### Sink Protocol ```python from typing import Any, Protocol class SinkProtocol(Protocol): name: str async def start(self) -> None: ... async def stop(self) -> None: ... async def write(self, entry: dict[str, Any]) -> None: ... # Optional: fast path for pre-serialized data async def write_serialized(self, view: "SerializedView") -> None: ... # Optional: health check async def health_check(self) -> bool: ... ``` ### Enricher Protocol ```python from typing import Any, Protocol class EnricherProtocol(Protocol): name: str async def start(self) -> None: ... async def stop(self) -> None: ... async def enrich(self, event: dict[str, Any]) -> dict[str, Any]: ... ``` ### Redactor Protocol ```python from typing import Any, Protocol class RedactorProtocol(Protocol): name: str async def start(self) -> None: ... async def stop(self) -> None: ... async def redact(self, event: dict[str, Any]) -> dict[str, Any]: ... ``` ### Filter Protocol ```python from typing import Any, Protocol class FilterProtocol(Protocol): name: str async def start(self) -> None: ... async def stop(self) -> None: ... async def filter(self, event: dict[str, Any]) -> dict[str, Any] | None: ... ``` Filters return the event to pass it through, or `None` to drop it. ### Processor Protocol ```python from typing import Any, Protocol class ProcessorProtocol(Protocol): name: str async def start(self) -> None: ... async def stop(self) -> None: ... async def process(self, data: bytes) -> bytes: ... ``` ## Security Considerations ### For Plugin Authors 1. **Never log secrets** - Don't log API keys, tokens, or credentials 2. **Validate inputs** - Don't trust event data blindly 3. **Handle errors gracefully** - Don't crash the logger; use diagnostics 4. **Document dependencies** - Declare all requirements in `pyproject.toml` 5. **Pin fapilog version** - Use `fapilog>=X.Y.Z, None: try: await self._send(entry) except Exception as exc: from fapilog.core.diagnostics import warn warn("my-sink", "delivery failed", error=str(exc)) ``` ### For Plugin Users 1. **Use allowlist for external plugins**: ```python settings = Settings(plugins={ "allow_external": True, "allowlist": ["trusted-plugin"], }) ``` 2. **Review plugin code** before installing third-party plugins 3. **Use strict validation** in production: ```python settings = Settings(plugins={"validation_mode": "strict"}) ``` ## Testing Your Plugin Use fapilog's testing utilities. For pytest fixtures, install the testing extra: ```bash pip install fapilog[testing] ``` ```python import pytest from fapilog.testing import validate_sink, ValidationResult def test_my_sink_protocol(): """Validate your sink follows the protocol.""" from my_sink import MySink sink = MySink() result: ValidationResult = validate_sink(sink) assert result.valid, f"Protocol violations: {result.errors}" @pytest.mark.asyncio async def test_my_sink_writes(): """Test your sink receives events.""" from my_sink import MySink sink = MySink() await sink.start() # Write a test event await sink.write({"message": "Test", "level": "INFO"}) await sink.stop() ``` ### Available Validators ```python from fapilog.testing import ( validate_sink, validate_enricher, validate_redactor, validate_filter, validate_processor, validate_plugin_lifecycle, # Tests start/stop actually work ) ``` ## Example: HTTP Webhook Sink A more complete example with configuration, retries, and health checks: ```python from typing import Any import httpx class WebhookSink: """Send logs to a webhook endpoint.""" name = "webhook" def __init__(self, url: str, timeout: float = 5.0): self.url = url self.timeout = timeout self._client: httpx.AsyncClient | None = None self._last_error: str | None = None async def start(self) -> None: self._client = httpx.AsyncClient(timeout=self.timeout) async def stop(self) -> None: if self._client: await self._client.aclose() self._client = None async def write(self, entry: dict[str, Any]) -> None: if not self._client: return try: await self._client.post(self.url, json=entry) self._last_error = None except httpx.HTTPError as e: self._last_error = str(e) # Log errors via diagnostics, don't raise from fapilog.core.diagnostics import warn warn("webhook-sink", "delivery failed", error=str(e)) async def health_check(self) -> bool: return self._client is not None and self._last_error is None ``` ## Example: Sampling Filter A filter that randomly samples events: ```python import random from typing import Any class SamplingFilter: """Probabilistic sampling filter.""" name = "sampling" def __init__(self, rate: float = 1.0, seed: int | None = None): self._rate = rate if seed is not None: random.seed(seed) async def start(self) -> None: pass async def stop(self) -> None: pass async def filter(self, event: dict[str, Any]) -> dict[str, Any] | None: if self._rate >= 1.0: return event if self._rate <= 0.0: return None return event if random.random() < self._rate else None async def health_check(self) -> bool: return True ``` ## Plugin Metadata (Optional) Add metadata for discovery and tooling: ```python PLUGIN_METADATA = { "name": "my_sink", "version": "1.0.0", "plugin_type": "sink", "entry_point": "my_package.my_sink:MySink", "description": "Short description of what this plugin does.", "author": "Your Name", "compatibility": {"min_fapilog_version": "0.7.0"}, "api_version": "1.0", } ``` ## Next Steps - [Plugin Authoring Guide](authoring.md) - Detailed authoring guidance - [Plugin Testing](testing.md) - Comprehensive testing patterns - [Plugin Catalog](../plugin-guide.md) - Reference implementations - [Error Handling](error-handling.md) - Graceful failure patterns