Set up structured JSON logging in a FastAPI app and ship logs to LogFlow with automatic request context, error tracking, and custom metadata.
FastAPI's default logging is bare — uvicorn prints access logs to stdout in plain text, and your application code either uses print() or Python's built-in logging module with no structure. Neither gives you searchable, filterable logs you can alert on.
This tutorial sets up structured JSON logging in a FastAPI app and ships everything to LogFlow. By the end you'll have:
pip install fastapi uvicorn structlog httpx
We use structlog for structured logging. It outputs JSON by default and integrates cleanly with Python's standard logging module.
Create a logging_config.py file:
import structlog
import logging
import sys
def setup_logging():
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
This gives you JSON output like:
{"event": "User signed up", "user_id": 42, "plan": "starter", "level": "info", "timestamp": "2026-07-14T09:23:15Z"}
Create middleware that automatically logs every request with context:
import time
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
logger = structlog.get_logger()
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
try:
response = await call_next(request)
duration_ms = round((time.perf_counter() - start) * 1000)
logger.info(
"request_completed",
method=request.method,
path=request.url.path,
status=response.status_code,
duration_ms=duration_ms,
client=request.client.host if request.client else None,
)
return response
except Exception as exc:
duration_ms = round((time.perf_counter() - start) * 1000)
logger.error(
"request_failed",
method=request.method,
path=request.url.path,
duration_ms=duration_ms,
error=str(exc),
exc_info=True,
)
raise
from fastapi import FastAPI
from logging_config import setup_logging
from middleware import LoggingMiddleware
import structlog
setup_logging()
logger = structlog.get_logger()
app = FastAPI()
app.add_middleware(LoggingMiddleware)
@app.get("/")
async def root():
return {"status": "ok"}
@app.post("/orders")
async def create_order(order: dict):
logger.info("order_created", order_id=order.get("id"), total=order.get("total"))
return {"created": True}
The simplest way is to use LogFlow's REST API. Add a custom structlog processor that sends each log entry:
import httpx
LOGFLOW_API_KEY = "lf_your_api_key_here"
LOGFLOW_URL = "https://api.getlogflow.com/v1/logs"
client = httpx.AsyncClient()
async def send_to_logflow(log_entry: dict):
level_map = {"info": "info", "warning": "warn", "error": "error", "critical": "fatal"}
payload = [{
"level": level_map.get(log_entry.get("level", "info"), "info"),
"message": log_entry.get("event", ""),
"service": "fastapi-app",
"attributes": {
k: str(v) for k, v in log_entry.items()
if k not in ("event", "level", "timestamp")
},
}]
try:
await client.post(
LOGFLOW_URL,
json=payload,
headers={"Authorization": f"Bearer {LOGFLOW_API_KEY}"},
timeout=5.0,
)
except Exception:
pass # don't let logging failures crash the app
For production, batch logs and flush periodically instead of sending one at a time. Or use a log forwarder like Fluent Bit to read stdout and forward to LogFlow — see the Docker logging tutorial for that approach.
The real value of structured logging is attaching business context to events:
@app.post("/payments")
async def process_payment(payment: dict):
logger.info(
"payment_initiated",
user_id=payment["user_id"],
amount=payment["amount"],
currency=payment["currency"],
provider="stripe",
)
try:
result = await charge(payment)
logger.info("payment_succeeded", charge_id=result["id"], user_id=payment["user_id"])
return result
except PaymentError as e:
logger.error(
"payment_failed",
user_id=payment["user_id"],
error_code=e.code,
amount=payment["amount"],
)
raise
Now in LogFlow you can search level:error AND service:fastapi-app AND payment_failed and instantly see which users were affected, how much money was involved, and which error codes are most common.
In the LogFlow dashboard, go to Alerts → Create Alert and set:
fastapi-appYou'll get notified before your users start complaining.
Your FastAPI app now has:
The entire setup is under 100 lines of code and adds negligible overhead to request processing.
Free plan available. No credit card required. Up and running in 2 minutes.
Get started freeCentralized Logging for NestJS Applications
Replace NestJS's default logger with structured JSON and ship logs to LogFlow for real-time search and alerts.
Pino Logging Integration
Connect Pino (the fastest Node.js logger) to LogFlow in under 5 minutes.
Winston Logging Integration
Plug LogFlow into an existing Winston setup without changing your application code.