Sink routing by level
Route different log levels to different sinks for cost control, compliance, and targeted alerting.
Quick start (settings)
export FAPILOG_SINK_ROUTING__ENABLED=true
export FAPILOG_SINK_ROUTING__RULES='[
{"levels": ["ERROR", "CRITICAL"], "sinks": ["postgres"]},
{"levels": ["DEBUG", "INFO", "WARNING"], "sinks": ["stdout_json"]}
]'
export FAPILOG_SINK_ROUTING__FALLBACK_SINKS='["rotating_file"]'
from fapilog import runtime
with runtime() as log:
log.error("Stored in postgres")
log.info("Goes to stdout")
Configuration schema
enabled: turn routing on (defaults to fanout when false)rules: list of{levels: [...], sinks: [...]}; levels are case-insensitiveoverlap: when true, multiple rules can apply; when false, first match winsfallback_sinks: used when no rule matches (empty drops unmatched events)
YAML configuration
If loading settings from a YAML file:
sink_routing:
enabled: true
overlap: true
rules:
- levels: [ERROR, CRITICAL]
sinks: [postgres, webhook]
- levels: [INFO, WARNING]
sinks: [stdout_json]
- levels: [DEBUG]
sinks: [rotating_file]
fallback_sinks:
- rotating_file
Programmatic example
from fapilog import Settings, get_logger
from fapilog.core.settings import RoutingRule
settings = Settings()
settings.sink_routing.enabled = True
settings.sink_routing.rules = [
RoutingRule(levels=["ERROR", "CRITICAL"], sinks=["postgres", "webhook"]),
RoutingRule(levels=["INFO", "WARNING"], sinks=["stdout_json"]),
]
settings.sink_routing.fallback_sinks = ["rotating_file"]
logger = get_logger(settings=settings)
Builder API with custom sink names
Use the name parameter to create multiple sinks of the same type with unique names for routing:
from fapilog import LoggerBuilder
logger = (
LoggerBuilder()
.add_file("/logs/errors", name="error_file")
.add_file("/logs/info", name="info_file")
.with_routing([
{"levels": ["ERROR", "CRITICAL"], "sinks": ["error_file"]},
{"levels": ["DEBUG", "INFO", "WARNING"], "sinks": ["info_file"]},
])
.build()
)
logger.error("Goes to /logs/errors")
logger.info("Goes to /logs/info")
All add_* sink methods support the name parameter:
add_file(directory, name="rotating_file")add_stdout(name="stdout_json")add_http(endpoint, name="http")add_webhook(endpoint, name="webhook")add_cloudwatch(log_group, name="cloudwatch")add_loki(url, name="loki")add_postgres(dsn, name="postgres")
Duplicate sink names raise ValueError at build time.
RoutingSink plugin
For manual composition without touching global settings:
from fapilog.plugins.sinks.routing import RoutingSink, RoutingSinkConfig
routing_sink = RoutingSink(
RoutingSinkConfig(
routes={
"ERROR": ["postgres"],
"CRITICAL": ["postgres", "webhook"],
"INFO": ["stdout_json"],
"*": ["rotating_file"], # fallback
},
sink_configs={"postgres": {"table_name": "errors_only"}},
parallel=True,
)
)
Circuit breaker fallback routing
When a sink’s circuit breaker opens (after consecutive failures), events destined for that sink can be automatically rerouted to a fallback sink instead of being dropped.
from fapilog import LoggerBuilder
logger = (
LoggerBuilder()
.with_preset("production")
.add_http("https://logs.example.com/ingest")
.with_circuit_breaker(
enabled=True,
failure_threshold=5,
recovery_timeout="30s",
fallback_sink="rotating_file",
)
.build()
)
In this example, if the HTTP sink fails 5 times in a row, the circuit opens and events are written to the rotating file sink instead. After 30 seconds, the circuit enters a half-open state and probes the HTTP sink with a single event to check recovery.
The adaptive preset enables this automatically with rotating_file as the fallback:
logger = get_logger(preset="adaptive")
# Circuit breaker enabled with rotating_file fallback
See Circuit Breaker for a complete guide on configuring fallback routing patterns.
Tips
Keep rule lists small; routing is O(1) per event.
Use overlap=true to send errors to multiple sinks (e.g., DB + webhook).
Provide fallback_sinks to avoid accidental drops when rules change.
Routing respects sink circuit breakers; open circuits are skipped automatically.