Strukturerad JSON-loggning och korrelations-ID:n
Skapa strukturerade loggar med request-scopeade korrelations-ID:n som bevaras över asynkrona gränser och tjänster.
Strukturerad JSON-loggning och korrelations-ID:n är en gratis lektion i Bootcamp i backendutveckling med FastAPI på CoddyKit. Detta är lektion 1 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med FastAPI, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.
Varför strukturerade loggar
I produktion är loggar data, inte prosa. En rad som User 42 failed login from 10.0.0.3 är lättläst för en människa men besvärlig för maskiner: det går inte att filtrera, aggregera eller larma på den på ett tillförlitligt sätt.
Strukturerad loggning skickar varje händelse som ett JSON-objekt med stabila och sökbara fält:
timestamp,level,messagerequest_idochcorrelation_id- sammanhang som
user_id,path,status_codeochduration_ms
Loggaggregatorer som Loki, Elasticsearch och Datadog indexerar sedan dessa fält, så att ni kan köra frågor som level=ERROR AND path=/checkout.
En JSON-logg på en rad
Den enklaste strukturerade loggen är en dict som serialiseras till JSON på en enda rad. Ett JSON-objekt per rad är formatet JSON Lines (NDJSON), som i princip alla verktyg för logginsamling förstår.
Det här fristående exemplet visar strukturen vi siktar på. Observera att fälten är platta och namngivna på ett konsekvent sätt.
import json
import time
def log(level, message, **fields):
record = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"level": level,
"message": message,
**fields,
}
print(json.dumps(record))
log("INFO", "request completed", path="/checkout", status_code=200, duration_ms=42)
log("ERROR", "db timeout", path="/orders", correlation_id="abc-123")En anpassad JSON-formaterare
Om ni skriver egen print(json.dumps(...)) kringgår ni Pythons logging-modul och förlorar nivåer, hanterare och biblioteksloggar. Anslut i stället en JSON-formaterare till den vanliga logging-stacken.
En formaterares uppgift är att omvandla en LogRecord till en sträng. Här returnerar vi JSON. record.__dict__ innehåller alla fält från extra={...} som ni skickar vid anropet.
import json
import logging
class JsonFormatter(logging.Formatter):
def format(self, record):
payload = {
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
payload["exc"] = self.formatException(record.exc_info)
return json.dumps(payload)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
logging.getLogger("app").info("service started", extra={"port": 8000})Problemet med korrelations-ID
En enskild användarbegäran förgrenas ofta: API-hanterare → servicelager → databasanrop → utgående HTTP-anrop till en annan tjänst. Om varje loggrad saknar identitet går det inte att sammanfoga dem till en sammanhängande berättelse.
Ett korrelations-ID, även kallat request-ID eller trace-ID, är ett unikt värde som genereras en gång per inkommande begäran och kopplas till varje loggrad som skapas medan den hanteras. Då hämtar correlation_id=abc-123 hela tidslinjen genom funktioner och även mellan tjänster.
Utmaningen är hur ni gör ID:t tillgängligt djupt nere i anropsstacken utan att skicka det genom argumenten till varje funktion.
ContextVar: tillstånd för en begäran
Det rena svaret är contextvars.ContextVar. Till skillnad från en global variabel innehåller en ContextVar ett värde som är isolerat per logisk exekveringskontext och, vilket är avgörande, sprids korrekt över async-awaits.
Varje samtidig begäran körs i sin egen kontext, så ett korrelations-ID som sätts i en begäran läcker aldrig över till en annan, även när många begäranden körs omlott i samma händelseloop.
import asyncio
from contextvars import ContextVar
correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
async def handle(name, cid):
correlation_id.set(cid)
await asyncio.sleep(0.01)
# value survives the await and stays isolated per task
print(name, "->", correlation_id.get())
async def main():
await asyncio.gather(
handle("req-A", "aaa"),
handle("req-B", "bbb"),
)
asyncio.run(main())Infoga ID:t med ett loggfilter
För att automatiskt få med korrelations-ID:t på varje loggrad ansluter ni ett logging.Filter som läser ContextVar och kopierar värdet till posten. Ett filter körs för varje post som passerar genom hanteraren, så ingen anropsplats behöver komma ihåg att skicka med ID:t.
Formateraren läser sedan record.correlation_id precis som vilket annat fält som helst.
import json
import logging
from contextvars import ContextVar
correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
class CorrelationFilter(logging.Filter):
def filter(self, record):
record.correlation_id = correlation_id.get()
return True
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"level": record.levelname,
"message": record.getMessage(),
"correlation_id": getattr(record, "correlation_id", "-"),
})
h = logging.StreamHandler()
h.addFilter(CorrelationFilter())
h.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[h])
correlation_id.set("abc-123")
logging.getLogger("app").info("order placed")FastAPI-middleware för att ange ID:t
I FastAPI är en HTTP-middleware rätt plats för att upprätta korrelations-ID:t, eftersom den omsluter varje begäran. Mönstret är:
- Läs en inkommande header,
X-Request-IDellerX-Correlation-ID, om en anropare, gateway eller uppströms tjänst redan har angett en. - Generera annars ett nytt UUID.
- Lagra det i
ContextVarså att alla efterföljande loggar kan använda det. - Skicka tillbaka det i svarshuvudet så att klienterna kan ange det i felrapporter.
Det här är ramverkskod som kräver en körande server, så exemplet är illustrativt snarare än körbart.
import uuid
from fastapi import FastAPI, Request
from contextvars import ContextVar
correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
app = FastAPI()
@app.middleware("http")
async def correlation_middleware(request: Request, call_next):
cid = request.headers.get("X-Request-ID") or str(uuid.uuid4())
token = correlation_id.set(cid)
try:
response = await call_next(request)
finally:
correlation_id.reset(token)
response.headers["X-Request-ID"] = cid
return responseVarför reset() med en Token är viktigt
Observera token = correlation_id.set(cid) följt av correlation_id.reset(token) i ett finally-block. Token återställer det tidigare värdet när begäran avslutas.
I en ASGI-server kan arbetaruppgifter och kontexter återanvändas. Återställningen förhindrar att ett inaktuellt ID från en avslutad begäran läcker in i en senare begäran som glömde att ange ett eget. Para alltid ihop set() med reset() i middleware och gör det i finally, så att det körs även när hanteraren kastar ett undantag.
from contextvars import ContextVar
cv: ContextVar[str] = ContextVar("cv", default="-")
print(cv.get()) # -
token = cv.set("req-1")
print(cv.get()) # req-1
cv.reset(token)
print(cv.get()) # back to -Bakgrundsuppgifter och trådar som överlever
ContextVar sprids automatiskt över await inom samma uppgift, men ett värde följer inte automatiskt med arbete som skickas till en annan tråd, till exempel run_in_executor eller blockerande databasintegreringar.
För att ta med kontexten över en trådgräns fångar du den med contextvars.copy_context() och kör den anropbara funktionen i den kopian. asyncio gör redan detta för create_task; för råa exekverare måste du göra det manuellt.
import contextvars
from concurrent.futures import ThreadPoolExecutor
cid = contextvars.ContextVar("cid", default="-")
def work():
return cid.get()
cid.set("trace-9")
ctx = contextvars.copy_context()
with ThreadPoolExecutor() as pool:
# ctx.run carries the ContextVar value into the worker thread
result = pool.submit(ctx.run, work).result()
print("in thread:", result) # trace-9Spridning mellan tjänster
Ett korrelations-ID är bara användbart från början till slut om det passerar tjänstegränser. När din FastAPI-tjänst anropar en annan tjänst vidarebefordrar du ID:t som en HTTP-header, så att loggarna nedströms delar samma värde.
Läs det från ContextVar och lägg in det i varje utgående klientanrop. Den mottagande tjänstens middleware läser den headern i stället för att generera ett nytt ID, så att ett och samma ID omfattar hela anropskedjan.
import httpx
from contextvars import ContextVar
correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
async def call_downstream(url: str):
headers = {"X-Request-ID": correlation_id.get()}
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers=headers)
return resp.json()Så får du ihop allt med structlog
I stället för att bygga formaterare för hand använder många team structlog, som sätter samman en pipeline av processorer och renderar JSON i slutet. En processor kan hämta korrelations-ID:t från ContextVar och automatiskt slå ihop det med varje händelse.
Fördelarna förstärker varandra: konsekvent JSON-utdata, enkel bindning av kontext per händelse via logger.bind(...) och smidig integrering med standardbibliotekets logging-modul, så att även biblioteksloggar fångas upp.
import structlog
from contextvars import ContextVar
correlation_id: ContextVar[str] = ContextVar("correlation_id", default="-")
def add_correlation_id(logger, method_name, event_dict):
event_dict["correlation_id"] = correlation_id.get()
return event_dict
structlog.configure(
processors=[
add_correlation_id,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
]
)
correlation_id.set("abc-123")
log = structlog.get_logger()
log.info("checkout_completed", amount=49.9, currency="EUR")Snabbtest
Testa din förståelse av hur korrelations-ID:n sprids i asynkrona FastAPI-tjänster.
Sammanfattning
Du har byggt strukturerad loggning med begäransspecifik kontext för FastAPI:
- Strukturerade JSON-loggar via en anpassad
logging.Formatter(eller structlog) gör loggarna sökbara. - Korrelations-ID:n knyter samman varje loggrad för en begäran mellan funktioner och tjänster.
- contextvars.ContextVar lagrar ID:t med isolering per begäran och överlever
await-gränser. - Ett loggningsfilter lägger till ID:t i varje post, så att ingen anropsplats behöver komma ihåg det.
- FastAPI-middleware läser
X-Request-IDeller genererar ett UUID och parar sedan ihopset()medreset(token)ifinally. - Ta med kontexten till trådar med
copy_context()och mellan tjänster genom att vidarebefordra ID-headern.
Resultatet: ett enda ID som du söker efter i din loggaggregator visar hela förloppet för valfri begäran.
Lär dig Bootcamp i backendutveckling med FastAPI med en AI-lärare – gratis
Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.
- Kurser
- 21
- Lektioner
- 84
Vanliga frågor
Är lektionen ”Strukturerad JSON-loggning och korrelations-ID:n” gratis?
Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Bootcamp i backendutveckling med FastAPI, inklusive ”Strukturerad JSON-loggning och korrelations-ID:n”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.
Vad lär jag mig i ”Strukturerad JSON-loggning och korrelations-ID:n”?
Skapa strukturerade loggar med request-scopeade korrelations-ID:n som bevaras över asynkrona gränser och tjänster. Ni övar på Bootcamp i backendutveckling med FastAPI med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.
Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med FastAPI?
Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med FastAPI på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 1 av 4.
Hur lång tid tar lektionen ”Strukturerad JSON-loggning och korrelations-ID:n”?
De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.
Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med FastAPI-lektionen?
Ja. Varje Bootcamp i backendutveckling med FastAPI-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.
Alla lektioner i den här kursen
- Strukturerad JSON-loggning och korrelations-ID:n
- Distribuerad tracing med OpenTelemetry
- Prometheus-mått och RED-/USE-dashboards
- Larm för SLO:er och error budgets