[{"data":1,"prerenderedAt":1919},["ShallowReactive",2],{"page-\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F":3,"faq-schema-\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F":1898},{"id":4,"title":5,"body":6,"description":1888,"extension":1889,"meta":1890,"navigation":210,"path":1894,"seo":1895,"stem":1896,"__hash__":1897},"content\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002Findex.md","Structured Logging with structlog (and When stdlib Is Enough)",{"type":7,"value":8,"toc":1875},"minimark",[9,13,37,50,55,58,158,168,484,499,503,510,624,838,851,856,862,1112,1132,1136,1145,1235,1257,1261,1264,1271,1393,1438,1508,1534,1538,1640,1669,1685,1689,1726,1730,1751,1755,1765,1786,1795,1812,1818,1822,1827,1852,1857,1871],[10,11,5],"h1",{"id":12},"structured-logging-with-structlog-and-when-stdlib-is-enough",[14,15,16,17,21,22,26,27,30,31,36],"p",{},"Every production Python API eventually needs queryable logs — one JSON object per line, each carrying enough context to trace a single request end to end. The question is not ",[18,19,20],"em",{},"whether"," to log JSON; it is whether to reach for ",[23,24,25],"code",{},"structlog"," or stay on the standard library's ",[23,28,29],{},"logging"," module with a JSON formatter bolted on. This page makes the call with runnable code on both sides, then goes past the toy example into the production config, the redaction, and the cost math that decide it in practice. Part of the ",[32,33,35],"a",{"href":34},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002F","monitoring and logging Python APIs"," guide.",[14,38,39,40,42,43,46,47,49],{},"The short version: stdlib ",[23,41,29],{}," can produce JSON, but binding per-request context — a ",[23,44,45],{},"request_id"," that follows every log call without being passed around by hand — is awkward and easy to forget. ",[23,48,25],{}," makes that binding the default path and turns your log format into an ordered pipeline of small functions you can edit without touching a single call site. Below is the comparison, the config, the concurrency model, and the criteria for choosing.",[51,52,54],"h2",{"id":53},"stdlib-logging-vs-structlog-side-by-side","stdlib logging vs structlog, side by side",[14,56,57],{},"Both approaches end at the same place — structured JSON on stdout — but the developer experience of getting there differs sharply, and the difference is almost entirely about per-request context.",[59,60,61,76],"table",{},[62,63,64],"thead",{},[65,66,67,71,74],"tr",{},[68,69,70],"th",{},"Concern",[68,72,73],{},"stdlib logging (dictConfig)",[68,75,25],{},[77,78,79,101,121,132,147],"tbody",{},[65,80,81,85,94],{},[82,83,84],"td",{},"JSON output",[82,86,87,90,91],{},[23,88,89],{},"Formatter"," subclass or ",[23,92,93],{},"python-json-logger",[82,95,96,97,100],{},"Built-in ",[23,98,99],{},"JSONRenderer"," processor",[65,102,103,106,115],{},[82,104,105],{},"Per-request context",[82,107,108,111,112],{},[23,109,110],{},"extra={...}"," on every call, or ",[23,113,114],{},"LoggerAdapter",[82,116,117,120],{},[23,118,119],{},"bind_contextvars()"," once, inherited everywhere",[65,122,123,126,129],{},[82,124,125],{},"Adding a field",[82,127,128],{},"Edit the formatter",[82,130,131],{},"Append a processor",[65,133,134,137,142],{},[82,135,136],{},"Key-value ergonomics",[82,138,139],{},[23,140,141],{},"log.info(\"done\", extra={\"status\": 200})",[82,143,144],{},[23,145,146],{},"log.info(\"done\", status=200)",[65,148,149,152,155],{},[82,150,151],{},"Ecosystem reach",[82,153,154],{},"Universal; every library uses it",[82,156,157],{},"Bridges to stdlib so library logs still flow",[14,159,160,161,163,164,167],{},"Here is the same \"request completed\" log line in stdlib ",[23,162,29],{},", configured with ",[23,165,166],{},"dictConfig"," and a JSON formatter:",[169,170,175],"pre",{"className":171,"code":172,"language":173,"meta":174,"style":174},"language-python shiki shiki-themes github-light github-dark","# stdlib_logging.py\nimport logging.config\nimport os\n\nlogging.config.dictConfig({\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"formatters\": {\n        \"json\": {\n            \"()\": \"pythonjsonlogger.jsonlogger.JsonFormatter\",\n            \"format\": \"%(asctime)s %(levelname)s %(name)s %(message)s\",\n        }\n    },\n    \"handlers\": {\n        \"stdout\": {\"class\": \"logging.StreamHandler\", \"formatter\": \"json\"}\n    },\n    \"root\": {\n        \"handlers\": [\"stdout\"],\n        \"level\": os.getenv(\"LOG_LEVEL\", \"INFO\"),\n    },\n})\n\nlog = logging.getLogger(\"builder-api\")\n# Context must be threaded through manually on each call:\nlog.info(\"request_completed\", extra={\"request_id\": \"abc\", \"status\": 200})\n","python","",[23,176,177,186,197,205,212,218,235,248,257,265,278,305,311,317,325,356,361,369,384,404,409,415,420,438,444],{"__ignoreMap":174},[178,179,182],"span",{"class":180,"line":181},"line",1,[178,183,185],{"class":184},"sJ8bj","# stdlib_logging.py\n",[178,187,189,193],{"class":180,"line":188},2,[178,190,192],{"class":191},"szBVR","import",[178,194,196],{"class":195},"sVt8B"," logging.config\n",[178,198,200,202],{"class":180,"line":199},3,[178,201,192],{"class":191},[178,203,204],{"class":195}," os\n",[178,206,208],{"class":180,"line":207},4,[178,209,211],{"emptyLinePlaceholder":210},true,"\n",[178,213,215],{"class":180,"line":214},5,[178,216,217],{"class":195},"logging.config.dictConfig({\n",[178,219,221,225,228,232],{"class":180,"line":220},6,[178,222,224],{"class":223},"sZZnC","    \"version\"",[178,226,227],{"class":195},": ",[178,229,231],{"class":230},"sj4cs","1",[178,233,234],{"class":195},",\n",[178,236,238,241,243,246],{"class":180,"line":237},7,[178,239,240],{"class":223},"    \"disable_existing_loggers\"",[178,242,227],{"class":195},[178,244,245],{"class":230},"False",[178,247,234],{"class":195},[178,249,251,254],{"class":180,"line":250},8,[178,252,253],{"class":223},"    \"formatters\"",[178,255,256],{"class":195},": {\n",[178,258,260,263],{"class":180,"line":259},9,[178,261,262],{"class":223},"        \"json\"",[178,264,256],{"class":195},[178,266,268,271,273,276],{"class":180,"line":267},10,[178,269,270],{"class":223},"            \"()\"",[178,272,227],{"class":195},[178,274,275],{"class":223},"\"pythonjsonlogger.jsonlogger.JsonFormatter\"",[178,277,234],{"class":195},[178,279,281,284,286,289,292,295,298,301,303],{"class":180,"line":280},11,[178,282,283],{"class":223},"            \"format\"",[178,285,227],{"class":195},[178,287,288],{"class":223},"\"",[178,290,291],{"class":230},"%(asctime)s",[178,293,294],{"class":230}," %(levelname)s",[178,296,297],{"class":230}," %(name)s",[178,299,300],{"class":230}," %(message)s",[178,302,288],{"class":223},[178,304,234],{"class":195},[178,306,308],{"class":180,"line":307},12,[178,309,310],{"class":195},"        }\n",[178,312,314],{"class":180,"line":313},13,[178,315,316],{"class":195},"    },\n",[178,318,320,323],{"class":180,"line":319},14,[178,321,322],{"class":223},"    \"handlers\"",[178,324,256],{"class":195},[178,326,328,331,334,337,339,342,345,348,350,353],{"class":180,"line":327},15,[178,329,330],{"class":223},"        \"stdout\"",[178,332,333],{"class":195},": {",[178,335,336],{"class":223},"\"class\"",[178,338,227],{"class":195},[178,340,341],{"class":223},"\"logging.StreamHandler\"",[178,343,344],{"class":195},", ",[178,346,347],{"class":223},"\"formatter\"",[178,349,227],{"class":195},[178,351,352],{"class":223},"\"json\"",[178,354,355],{"class":195},"}\n",[178,357,359],{"class":180,"line":358},16,[178,360,316],{"class":195},[178,362,364,367],{"class":180,"line":363},17,[178,365,366],{"class":223},"    \"root\"",[178,368,256],{"class":195},[178,370,372,375,378,381],{"class":180,"line":371},18,[178,373,374],{"class":223},"        \"handlers\"",[178,376,377],{"class":195},": [",[178,379,380],{"class":223},"\"stdout\"",[178,382,383],{"class":195},"],\n",[178,385,387,390,393,396,398,401],{"class":180,"line":386},19,[178,388,389],{"class":223},"        \"level\"",[178,391,392],{"class":195},": os.getenv(",[178,394,395],{"class":223},"\"LOG_LEVEL\"",[178,397,344],{"class":195},[178,399,400],{"class":223},"\"INFO\"",[178,402,403],{"class":195},"),\n",[178,405,407],{"class":180,"line":406},20,[178,408,316],{"class":195},[178,410,412],{"class":180,"line":411},21,[178,413,414],{"class":195},"})\n",[178,416,418],{"class":180,"line":417},22,[178,419,211],{"emptyLinePlaceholder":210},[178,421,423,426,429,432,435],{"class":180,"line":422},23,[178,424,425],{"class":195},"log ",[178,427,428],{"class":191},"=",[178,430,431],{"class":195}," logging.getLogger(",[178,433,434],{"class":223},"\"builder-api\"",[178,436,437],{"class":195},")\n",[178,439,441],{"class":180,"line":440},24,[178,442,443],{"class":184},"# Context must be threaded through manually on each call:\n",[178,445,447,450,453,455,459,461,464,467,469,472,474,477,479,482],{"class":180,"line":446},25,[178,448,449],{"class":195},"log.info(",[178,451,452],{"class":223},"\"request_completed\"",[178,454,344],{"class":195},[178,456,458],{"class":457},"s4XuR","extra",[178,460,428],{"class":191},[178,462,463],{"class":195},"{",[178,465,466],{"class":223},"\"request_id\"",[178,468,227],{"class":195},[178,470,471],{"class":223},"\"abc\"",[178,473,344],{"class":195},[178,475,476],{"class":223},"\"status\"",[178,478,227],{"class":195},[178,480,481],{"class":230},"200",[178,483,414],{"class":195},[14,485,486,487,489,490,492,493,495,496,498],{},"That works, but ",[23,488,110],{}," has to appear on every single call, and forgetting it on one log line silently drops the ",[23,491,45],{}," you needed most — the one attached to the 500 you are trying to reproduce. A ",[23,494,114],{}," helps, but you still pass it down every function that logs. ",[23,497,25],{}," moves context binding out of the call site entirely, which is the whole reason to adopt it.",[51,500,502],{"id":501},"structlog-config-bound-to-request-context","structlog config bound to request context",[14,504,505,506,509],{},"The processor pipeline is the core idea: each log event flows through an ordered list of functions that enrich it, format it, and finally render it. ",[23,507,508],{},"merge_contextvars"," is the one that injects request-scoped fields automatically, so nothing in your route code has to know they exist.",[511,512,520,521,520,525,520,529,520,548,520,555,520,562,520,570,520,576,520,580,520,585,520,589,520,593,520,595,520,599,520,602,520,606,520,609,520,612,520,616,520,620],"svg",{"viewBox":513,"role":514,"ariaLabelledBy":515,"xmlns":518,"style":519},"0 0 720 150","img",[516,517],"slog-pipe-t","slog-pipe-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[522,523,524],"title",{"id":516},"structlog processor pipeline",[526,527,528],"desc",{"id":517},"A log event flows through merge contextvars, add log level, timestamper, and a JSON renderer to produce one JSON line on stdout.",[530,531,532,533,520],"defs",{},"\n    ",[534,535,542,543,532],"marker",{"id":536,"viewBox":537,"refX":538,"refY":539,"markerWidth":540,"markerHeight":540,"orient":541},"slog-pipe-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[544,545],"path",{"d":546,"fill":547},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[549,550],"rect",{"x":551,"y":551,"width":552,"height":553,"fill":554},"0","720","150","var(--c-surface)",[549,556],{"x":557,"y":558,"width":559,"height":558,"rx":538,"fill":554,"stroke":560,"style":561},"14","50","120","var(--c-blue)","stroke-width:2;",[563,564,569],"text",{"x":565,"y":566,"fill":567,"style":568},"74","80","var(--c-text)","text-anchor:middle;font-size:12;font-family:var(--font-sans);","log.info(...)",[180,571],{"x1":572,"y1":573,"x2":574,"y2":573,"stroke":547,"style":575},"134","75","166","stroke-width:2;marker-end:url(#slog-pipe-arrow);",[549,577],{"x":574,"y":558,"width":578,"height":558,"rx":538,"fill":554,"stroke":579,"style":561},"130","var(--c-teal)",[563,581,584],{"x":582,"y":583,"fill":567,"style":568},"231","73","merge",[563,586,588],{"x":582,"y":587,"fill":547,"style":568},"89","contextvars",[180,590],{"x1":591,"y1":573,"x2":592,"y2":573,"stroke":547,"style":575},"296","328",[549,594],{"x":592,"y":558,"width":578,"height":558,"rx":538,"fill":554,"stroke":579,"style":561},[563,596,598],{"x":597,"y":583,"fill":567,"style":568},"393","level +",[563,600,601],{"x":597,"y":587,"fill":547,"style":568},"timestamp",[180,603],{"x1":604,"y1":573,"x2":605,"y2":573,"stroke":547,"style":575},"458","490",[549,607],{"x":605,"y":558,"width":578,"height":558,"rx":538,"fill":554,"stroke":608,"style":561},"var(--c-coral)",[563,610,99],{"x":611,"y":566,"fill":567,"style":568},"555",[180,613],{"x1":614,"y1":573,"x2":615,"y2":573,"stroke":547,"style":575},"620","652",[549,617],{"x":615,"y":558,"width":618,"height":558,"rx":538,"fill":554,"stroke":619,"style":561},"54","var(--c-yellow)",[563,621,623],{"x":622,"y":566,"fill":567,"style":568},"679","stdout",[169,625,627],{"className":171,"code":626,"language":173,"meta":174,"style":174},"# structlog_setup.py\nimport logging\nimport os\nimport structlog\n\ndef configure_logging() -> None:\n    level = os.getenv(\"LOG_LEVEL\", \"INFO\").upper()\n    logging.basicConfig(format=\"%(message)s\", level=level)\n    structlog.configure(\n        processors=[\n            structlog.contextvars.merge_contextvars,   # inject request context\n            structlog.processors.add_log_level,\n            structlog.processors.TimeStamper(fmt=\"iso\"),\n            structlog.processors.StackInfoRenderer(),\n            structlog.processors.format_exc_info,\n            structlog.processors.JSONRenderer(),        # one JSON line out\n        ],\n        wrapper_class=structlog.make_filtering_bound_logger(\n            logging.getLevelName(level)\n        ),\n        cache_logger_on_first_use=True,\n    )\n\nlog = structlog.get_logger()\n",[23,628,629,634,641,647,654,658,676,695,722,727,737,745,750,765,770,775,783,788,798,803,808,820,825,829],{"__ignoreMap":174},[178,630,631],{"class":180,"line":181},[178,632,633],{"class":184},"# structlog_setup.py\n",[178,635,636,638],{"class":180,"line":188},[178,637,192],{"class":191},[178,639,640],{"class":195}," logging\n",[178,642,643,645],{"class":180,"line":199},[178,644,192],{"class":191},[178,646,204],{"class":195},[178,648,649,651],{"class":180,"line":207},[178,650,192],{"class":191},[178,652,653],{"class":195}," structlog\n",[178,655,656],{"class":180,"line":214},[178,657,211],{"emptyLinePlaceholder":210},[178,659,660,663,667,670,673],{"class":180,"line":220},[178,661,662],{"class":191},"def",[178,664,666],{"class":665},"sScJk"," configure_logging",[178,668,669],{"class":195},"() -> ",[178,671,672],{"class":230},"None",[178,674,675],{"class":195},":\n",[178,677,678,681,683,686,688,690,692],{"class":180,"line":237},[178,679,680],{"class":195},"    level ",[178,682,428],{"class":191},[178,684,685],{"class":195}," os.getenv(",[178,687,395],{"class":223},[178,689,344],{"class":195},[178,691,400],{"class":223},[178,693,694],{"class":195},").upper()\n",[178,696,697,700,703,705,707,710,712,714,717,719],{"class":180,"line":250},[178,698,699],{"class":195},"    logging.basicConfig(",[178,701,702],{"class":457},"format",[178,704,428],{"class":191},[178,706,288],{"class":223},[178,708,709],{"class":230},"%(message)s",[178,711,288],{"class":223},[178,713,344],{"class":195},[178,715,716],{"class":457},"level",[178,718,428],{"class":191},[178,720,721],{"class":195},"level)\n",[178,723,724],{"class":180,"line":259},[178,725,726],{"class":195},"    structlog.configure(\n",[178,728,729,732,734],{"class":180,"line":267},[178,730,731],{"class":457},"        processors",[178,733,428],{"class":191},[178,735,736],{"class":195},"[\n",[178,738,739,742],{"class":180,"line":280},[178,740,741],{"class":195},"            structlog.contextvars.merge_contextvars,   ",[178,743,744],{"class":184},"# inject request context\n",[178,746,747],{"class":180,"line":307},[178,748,749],{"class":195},"            structlog.processors.add_log_level,\n",[178,751,752,755,758,760,763],{"class":180,"line":313},[178,753,754],{"class":195},"            structlog.processors.TimeStamper(",[178,756,757],{"class":457},"fmt",[178,759,428],{"class":191},[178,761,762],{"class":223},"\"iso\"",[178,764,403],{"class":195},[178,766,767],{"class":180,"line":319},[178,768,769],{"class":195},"            structlog.processors.StackInfoRenderer(),\n",[178,771,772],{"class":180,"line":327},[178,773,774],{"class":195},"            structlog.processors.format_exc_info,\n",[178,776,777,780],{"class":180,"line":358},[178,778,779],{"class":195},"            structlog.processors.JSONRenderer(),        ",[178,781,782],{"class":184},"# one JSON line out\n",[178,784,785],{"class":180,"line":363},[178,786,787],{"class":195},"        ],\n",[178,789,790,793,795],{"class":180,"line":371},[178,791,792],{"class":457},"        wrapper_class",[178,794,428],{"class":191},[178,796,797],{"class":195},"structlog.make_filtering_bound_logger(\n",[178,799,800],{"class":180,"line":386},[178,801,802],{"class":195},"            logging.getLevelName(level)\n",[178,804,805],{"class":180,"line":406},[178,806,807],{"class":195},"        ),\n",[178,809,810,813,815,818],{"class":180,"line":411},[178,811,812],{"class":457},"        cache_logger_on_first_use",[178,814,428],{"class":191},[178,816,817],{"class":230},"True",[178,819,234],{"class":195},[178,821,822],{"class":180,"line":417},[178,823,824],{"class":195},"    )\n",[178,826,827],{"class":180,"line":422},[178,828,211],{"emptyLinePlaceholder":210},[178,830,831,833,835],{"class":180,"line":440},[178,832,425],{"class":195},[178,834,428],{"class":191},[178,836,837],{"class":195}," structlog.get_logger()\n",[14,839,840,841,843,844,846,847,850],{},"The pipeline order is not cosmetic. ",[23,842,508],{}," runs first so request fields are present before anything else reads the event dict, and ",[23,845,99],{}," runs last because nothing can render after the event has become a string. If you insert a redaction processor (below) it must sit ",[18,848,849],{},"before"," the renderer or it has nothing structured left to strip.",[852,853,855],"h3",{"id":854},"wiring-it-into-fastapi","Wiring it into FastAPI",[14,857,858,859,861],{},"Bind context once in middleware, and every log call inside that request — in your route, in a service function, in a dependency, anywhere down the async call stack — inherits the ",[23,860,45],{}," for free:",[169,863,865],{"className":171,"code":864,"language":173,"meta":174,"style":174},"# app.py\nimport uuid\nimport structlog\nfrom fastapi import FastAPI, Request\nfrom structlog_setup import configure_logging\n\nconfigure_logging()\nlog = structlog.get_logger()\napp = FastAPI()\n\n@app.middleware(\"http\")\nasync def bind_request_context(request: Request, call_next):\n    structlog.contextvars.bind_contextvars(\n        request_id=request.headers.get(\"X-Request-ID\", str(uuid.uuid4())),\n        path=request.url.path,\n    )\n    try:\n        return await call_next(request)\n    finally:\n        structlog.contextvars.clear_contextvars()\n\n@app.get(\"\u002Fitems\u002F{item_id}\")\nasync def get_item(item_id: int):\n    log.info(\"item_lookup\", item_id=item_id)  # request_id added automatically\n    return {\"item_id\": item_id}\n",[23,866,867,872,879,885,898,910,914,919,927,937,941,954,968,973,994,1004,1008,1015,1026,1033,1038,1042,1059,1077,1098],{"__ignoreMap":174},[178,868,869],{"class":180,"line":181},[178,870,871],{"class":184},"# app.py\n",[178,873,874,876],{"class":180,"line":188},[178,875,192],{"class":191},[178,877,878],{"class":195}," uuid\n",[178,880,881,883],{"class":180,"line":199},[178,882,192],{"class":191},[178,884,653],{"class":195},[178,886,887,890,893,895],{"class":180,"line":207},[178,888,889],{"class":191},"from",[178,891,892],{"class":195}," fastapi ",[178,894,192],{"class":191},[178,896,897],{"class":195}," FastAPI, Request\n",[178,899,900,902,905,907],{"class":180,"line":214},[178,901,889],{"class":191},[178,903,904],{"class":195}," structlog_setup ",[178,906,192],{"class":191},[178,908,909],{"class":195}," configure_logging\n",[178,911,912],{"class":180,"line":220},[178,913,211],{"emptyLinePlaceholder":210},[178,915,916],{"class":180,"line":237},[178,917,918],{"class":195},"configure_logging()\n",[178,920,921,923,925],{"class":180,"line":250},[178,922,425],{"class":195},[178,924,428],{"class":191},[178,926,837],{"class":195},[178,928,929,932,934],{"class":180,"line":259},[178,930,931],{"class":195},"app ",[178,933,428],{"class":191},[178,935,936],{"class":195}," FastAPI()\n",[178,938,939],{"class":180,"line":267},[178,940,211],{"emptyLinePlaceholder":210},[178,942,943,946,949,952],{"class":180,"line":280},[178,944,945],{"class":665},"@app.middleware",[178,947,948],{"class":195},"(",[178,950,951],{"class":223},"\"http\"",[178,953,437],{"class":195},[178,955,956,959,962,965],{"class":180,"line":307},[178,957,958],{"class":191},"async",[178,960,961],{"class":191}," def",[178,963,964],{"class":665}," bind_request_context",[178,966,967],{"class":195},"(request: Request, call_next):\n",[178,969,970],{"class":180,"line":313},[178,971,972],{"class":195},"    structlog.contextvars.bind_contextvars(\n",[178,974,975,978,980,983,986,988,991],{"class":180,"line":319},[178,976,977],{"class":457},"        request_id",[178,979,428],{"class":191},[178,981,982],{"class":195},"request.headers.get(",[178,984,985],{"class":223},"\"X-Request-ID\"",[178,987,344],{"class":195},[178,989,990],{"class":230},"str",[178,992,993],{"class":195},"(uuid.uuid4())),\n",[178,995,996,999,1001],{"class":180,"line":327},[178,997,998],{"class":457},"        path",[178,1000,428],{"class":191},[178,1002,1003],{"class":195},"request.url.path,\n",[178,1005,1006],{"class":180,"line":358},[178,1007,824],{"class":195},[178,1009,1010,1013],{"class":180,"line":363},[178,1011,1012],{"class":191},"    try",[178,1014,675],{"class":195},[178,1016,1017,1020,1023],{"class":180,"line":371},[178,1018,1019],{"class":191},"        return",[178,1021,1022],{"class":191}," await",[178,1024,1025],{"class":195}," call_next(request)\n",[178,1027,1028,1031],{"class":180,"line":386},[178,1029,1030],{"class":191},"    finally",[178,1032,675],{"class":195},[178,1034,1035],{"class":180,"line":406},[178,1036,1037],{"class":195},"        structlog.contextvars.clear_contextvars()\n",[178,1039,1040],{"class":180,"line":411},[178,1041,211],{"emptyLinePlaceholder":210},[178,1043,1044,1047,1049,1052,1055,1057],{"class":180,"line":417},[178,1045,1046],{"class":665},"@app.get",[178,1048,948],{"class":195},[178,1050,1051],{"class":223},"\"\u002Fitems\u002F",[178,1053,1054],{"class":230},"{item_id}",[178,1056,288],{"class":223},[178,1058,437],{"class":195},[178,1060,1061,1063,1065,1068,1071,1074],{"class":180,"line":422},[178,1062,958],{"class":191},[178,1064,961],{"class":191},[178,1066,1067],{"class":665}," get_item",[178,1069,1070],{"class":195},"(item_id: ",[178,1072,1073],{"class":230},"int",[178,1075,1076],{"class":195},"):\n",[178,1078,1079,1082,1085,1087,1090,1092,1095],{"class":180,"line":440},[178,1080,1081],{"class":195},"    log.info(",[178,1083,1084],{"class":223},"\"item_lookup\"",[178,1086,344],{"class":195},[178,1088,1089],{"class":457},"item_id",[178,1091,428],{"class":191},[178,1093,1094],{"class":195},"item_id)  ",[178,1096,1097],{"class":184},"# request_id added automatically\n",[178,1099,1100,1103,1106,1109],{"class":180,"line":446},[178,1101,1102],{"class":191},"    return",[178,1104,1105],{"class":195}," {",[178,1107,1108],{"class":223},"\"item_id\"",[178,1110,1111],{"class":195},": item_id}\n",[14,1113,1114,1115,1118,1119,1122,1123,1125,1126,1128,1129,1131],{},"The ",[23,1116,1117],{},"clear_contextvars()"," in the ",[23,1120,1121],{},"finally"," block is mandatory, and the reason is the most common structlog bug in production. ",[23,1124,588],{}," are not automatically request-scoped; they are task-scoped and the underlying worker task is reused. Skip the clear and one request's ",[23,1127,45],{}," leaks into whatever request the event loop schedules next on that task — so your logs blame customer A for an error customer B triggered. Always clear in ",[23,1130,1121],{}," so it runs even when the route raises.",[51,1133,1135],{"id":1134},"isolating-context-across-concurrent-requests","Isolating context across concurrent requests",[14,1137,1138,1139,1141,1142,1144],{},"The reason ",[23,1140,25],{}," beats a global dict or a thread-local for this job is that ",[23,1143,588],{}," are copied per async task. Two requests handled concurrently on the same worker each see only their own bound fields; there is no cross-talk even though they share one logger and one pipeline. That is what makes the shared pipeline safe.",[511,1146,520,1151,520,1154,520,1157,520,1164,520,1167,520,1173,520,1179,520,1183,520,1186,520,1190,520,1193,520,1199,520,1203,520,1207,520,1214,520,1216,520,1219,520,1223,520,1225,520,1229,520,1233],{"viewBox":1147,"role":514,"ariaLabelledBy":1148,"xmlns":518,"style":519},"0 0 720 250",[1149,1150],"slog-ctx-t","slog-ctx-d",[522,1152,1153],{"id":1149},"contextvars isolation across two concurrent async tasks",[526,1155,1156],{"id":1150},"Two async tasks each carry their own request id through one shared structlog pipeline and emit separate JSON lines with no cross-talk.",[530,1158,532,1159,520],{},[534,1160,542,1162,532],{"id":1161,"viewBox":537,"refX":538,"refY":539,"markerWidth":540,"markerHeight":540,"orient":541},"slog-ctx-arrow",[544,1163],{"d":546,"fill":547},[549,1165],{"x":551,"y":551,"width":552,"height":1166,"fill":554},"250",[549,1168],{"x":1169,"y":1170,"width":1171,"height":1172,"rx":538,"fill":554,"stroke":560,"style":561},"16","34","196","60",[563,1174,1178],{"x":1175,"y":1176,"fill":567,"style":1177},"114","58","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Task A (async)",[563,1180,1182],{"x":1175,"y":1181,"fill":547,"style":568},"78","ctx: request_id=a1f3",[549,1184],{"x":1169,"y":1185,"width":1171,"height":1172,"rx":538,"fill":554,"stroke":608,"style":561},"156",[563,1187,1189],{"x":1175,"y":1188,"fill":567,"style":1177},"180","Task B (async)",[563,1191,1192],{"x":1175,"y":481,"fill":547,"style":568},"ctx: request_id=b7e2",[549,1194],{"x":1195,"y":1196,"width":1197,"height":1172,"rx":538,"fill":1198,"stroke":579,"style":561},"286","95","148","var(--c-surface-alt)",[563,1200,1202],{"x":1201,"y":559,"fill":567,"style":1177},"360","shared structlog",[563,1204,1206],{"x":1201,"y":1205,"fill":547,"style":568},"138","pipeline",[180,1208],{"x1":1209,"y1":1210,"x2":1211,"y2":1212,"stroke":547,"style":1213},"212","70","284","112","stroke-width:2;marker-end:url(#slog-ctx-arrow);",[180,1215],{"x1":1209,"y1":1188,"x2":1211,"y2":1205,"stroke":547,"style":1213},[549,1217],{"x":1218,"y":1170,"width":1171,"height":1172,"rx":538,"fill":554,"stroke":560,"style":561},"508",[563,1220,1222],{"x":1221,"y":1210,"fill":567,"style":568},"606","{\"request_id\":\"a1f3\"}",[549,1224],{"x":1218,"y":1185,"width":1171,"height":1172,"rx":538,"fill":554,"stroke":608,"style":561},[563,1226,1228],{"x":1221,"y":1227,"fill":567,"style":568},"192","{\"request_id\":\"b7e2\"}",[180,1230],{"x1":1231,"y1":1212,"x2":1232,"y2":1210,"stroke":547,"style":1213},"434","506",[180,1234],{"x1":1231,"y1":1205,"x2":1232,"y2":1188,"stroke":547,"style":1213},[14,1236,1237,1238,1241,1242,1244,1245,1248,1249,1253,1254,1256],{},"There is one edge case worth knowing: ",[23,1239,1240],{},"asyncio.create_task()"," snapshots the current context, so a background task spawned mid-request inherits the ",[23,1243,45],{}," at spawn time — usually what you want. But if you hand work to a thread pool via ",[23,1246,1247],{},"run_in_executor",", the copy does not follow automatically; bind again inside the worker or pass the id explicitly. The same caveat applies when you push work onto a queue for ",[32,1250,1252],{"href":1251},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","background jobs with Celery",": the request context does not cross the process boundary, so propagate the ",[23,1255,45],{}," as a task argument and re-bind it in the worker.",[51,1258,1260],{"id":1259},"production-configuration-that-pays-off","Production configuration that pays off",[14,1262,1263],{},"The toy config renders ISO timestamps and levels. A commercial API needs three more things: level control from the environment, a redaction step so you never ship PII to your log store, and a faster serializer once volume climbs.",[14,1265,1266,1267,1270],{},"Redaction is a processor like any other. This one drops known-sensitive keys before the renderer sees them, so an ",[23,1268,1269],{},"authorization"," header or a raw email never lands in your log platform where it becomes a compliance liability:",[169,1272,1274],{"className":171,"code":1273,"language":173,"meta":174,"style":174},"# redaction.py\nimport os\n\n_REDACT = set(\n    os.getenv(\"LOG_REDACT_KEYS\", \"authorization,password,token,email\").split(\",\")\n)\n\ndef redact_pii(logger, method_name, event_dict):\n    for key in list(event_dict):\n        if key.lower() in _REDACT:\n            event_dict[key] = \"***\"\n    return event_dict\n",[23,1275,1276,1281,1287,1291,1305,1326,1330,1334,1344,1361,1376,1386],{"__ignoreMap":174},[178,1277,1278],{"class":180,"line":181},[178,1279,1280],{"class":184},"# redaction.py\n",[178,1282,1283,1285],{"class":180,"line":188},[178,1284,192],{"class":191},[178,1286,204],{"class":195},[178,1288,1289],{"class":180,"line":199},[178,1290,211],{"emptyLinePlaceholder":210},[178,1292,1293,1296,1299,1302],{"class":180,"line":207},[178,1294,1295],{"class":230},"_REDACT",[178,1297,1298],{"class":191}," =",[178,1300,1301],{"class":230}," set",[178,1303,1304],{"class":195},"(\n",[178,1306,1307,1310,1313,1315,1318,1321,1324],{"class":180,"line":214},[178,1308,1309],{"class":195},"    os.getenv(",[178,1311,1312],{"class":223},"\"LOG_REDACT_KEYS\"",[178,1314,344],{"class":195},[178,1316,1317],{"class":223},"\"authorization,password,token,email\"",[178,1319,1320],{"class":195},").split(",[178,1322,1323],{"class":223},"\",\"",[178,1325,437],{"class":195},[178,1327,1328],{"class":180,"line":220},[178,1329,437],{"class":195},[178,1331,1332],{"class":180,"line":237},[178,1333,211],{"emptyLinePlaceholder":210},[178,1335,1336,1338,1341],{"class":180,"line":250},[178,1337,662],{"class":191},[178,1339,1340],{"class":665}," redact_pii",[178,1342,1343],{"class":195},"(logger, method_name, event_dict):\n",[178,1345,1346,1349,1352,1355,1358],{"class":180,"line":259},[178,1347,1348],{"class":191},"    for",[178,1350,1351],{"class":195}," key ",[178,1353,1354],{"class":191},"in",[178,1356,1357],{"class":230}," list",[178,1359,1360],{"class":195},"(event_dict):\n",[178,1362,1363,1366,1369,1371,1374],{"class":180,"line":267},[178,1364,1365],{"class":191},"        if",[178,1367,1368],{"class":195}," key.lower() ",[178,1370,1354],{"class":191},[178,1372,1373],{"class":230}," _REDACT",[178,1375,675],{"class":195},[178,1377,1378,1381,1383],{"class":180,"line":280},[178,1379,1380],{"class":195},"            event_dict[key] ",[178,1382,428],{"class":191},[178,1384,1385],{"class":223}," \"***\"\n",[178,1387,1388,1390],{"class":180,"line":307},[178,1389,1102],{"class":191},[178,1391,1392],{"class":195}," event_dict\n",[14,1394,1395,1396,1399,1400,1403,1404,1406,1407,1410,1411,1414,1415,1418,1419,1422,1423,1426,1427,1430,1431,1434,1435,1437],{},"Insert ",[23,1397,1398],{},"redact_pii"," into the ",[23,1401,1402],{},"processors"," list just before ",[23,1405,99],{},". For throughput, swap the default renderer for ",[23,1408,1409],{},"structlog.processors.JSONRenderer(serializer=orjson.dumps)"," — ",[23,1412,1413],{},"orjson"," serializes two to three times faster than the stdlib ",[23,1416,1417],{},"json"," module, which matters only once you are emitting millions of lines a day but costs nothing to adopt early. Keep ",[23,1420,1421],{},"LOG_LEVEL"," at ",[23,1424,1425],{},"INFO"," in production and reserve ",[23,1428,1429],{},"DEBUG"," for a temporary environment override; ",[23,1432,1433],{},"make_filtering_bound_logger"," discards below-threshold events before they touch the pipeline, so a filtered ",[23,1436,1429],{}," call is close to free.",[59,1439,1440,1453],{},[62,1441,1442],{},[65,1443,1444,1447,1450],{},[68,1445,1446],{},"Env var",[68,1448,1449],{},"Default",[68,1451,1452],{},"Production recommendation",[77,1454,1455,1473,1488],{},[65,1456,1457,1461,1465],{},[82,1458,1459],{},[23,1460,1421],{},[82,1462,1463],{},[23,1464,1425],{},[82,1466,1467,1469,1470,1472],{},[23,1468,1425],{},"; flip to ",[23,1471,1429],{}," per-deploy only",[65,1474,1475,1480,1485],{},[82,1476,1477],{},[23,1478,1479],{},"LOG_REDACT_KEYS",[82,1481,1482],{},[23,1483,1484],{},"authorization,password,token,email",[82,1486,1487],{},"Extend per your schema",[65,1489,1490,1495,1499],{},[82,1491,1492],{},[23,1493,1494],{},"LOG_RENDERER",[82,1496,1497],{},[23,1498,1417],{},[82,1500,1501,1503,1504,1507],{},[23,1502,1417],{}," in prod, ",[23,1505,1506],{},"console"," in local dev",[14,1509,1510,1511,1514,1515,1517,1518,1521,1522,1525,1526,1528,1529,1533],{},"On cost: JSON serialization of a typical event dict runs in single-digit microseconds. An API serving 1M requests a month that emits five log lines per request produces 5M lines. At roughly 400–600 bytes per line that is about 2.5 GB of log volume a month — small enough that CPU spent logging is lost next to a single database round-trip, but large enough that your ",[18,1512,1513],{},"log ingestion bill"," is the real number to watch. This is why you sample: keep ",[23,1516,1425],{}," for request-completed lines and one ",[23,1519,1520],{},"WARNING","\u002F",[23,1523,1524],{},"ERROR"," per failure, but do not log every cache hit at ",[23,1527,1425],{},". Whether a log line earns its ingestion cost is the same margin question you face when ",[32,1530,1532],{"href":1531},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request",".",[51,1535,1537],{"id":1536},"when-to-choose-structlog","When to choose structlog",[511,1539,520,1544,520,1547,520,1550,520,1557,520,1560,520,1565,520,1569,520,1572,520,1577,520,1581,520,1583,520,1587,520,1592,520,1597,520,1601,520,1604,520,1608,520,1612,520,1616,520,1620,520,1623,520,1629,520,1633,520,1636],{"viewBox":1540,"role":514,"ariaLabelledBy":1541,"xmlns":518,"style":519},"0 0 720 300",[1542,1543],"slog-tree-t","slog-tree-d",[522,1545,1546],{"id":1542},"decision tree for structlog versus stdlib logging",[526,1548,1549],{"id":1543},"If you need per-request context on every log line and run an async API, choose structlog; otherwise stdlib logging with a JSON formatter is enough.",[530,1551,532,1552,520],{},[534,1553,542,1555,532],{"id":1554,"viewBox":537,"refX":538,"refY":539,"markerWidth":540,"markerHeight":540,"orient":541},"slog-tree-arrow",[544,1556],{"d":546,"fill":547},[549,1558],{"x":551,"y":551,"width":552,"height":1559,"fill":554},"300",[549,1561],{"x":1166,"y":1562,"width":1563,"height":1564,"rx":538,"fill":1198,"stroke":560,"style":561},"18","220","56",[563,1566,1568],{"x":1201,"y":1567,"fill":567,"style":1177},"42","Per-request context on",[563,1570,1571],{"x":1201,"y":1172,"fill":567,"style":1177},"every log line?",[180,1573],{"x1":1574,"y1":565,"x2":553,"y2":1575,"stroke":547,"style":1576},"290","128","stroke-width:2;marker-end:url(#slog-tree-arrow);",[563,1578,1580],{"x":1171,"y":1579,"fill":547,"style":568},"104","no",[180,1582],{"x1":1201,"y1":565,"x2":1201,"y2":1575,"stroke":547,"style":1576},[563,1584,1586],{"x":1585,"y":1579,"fill":547,"style":568},"378","yes",[549,1588],{"x":1589,"y":1590,"width":1209,"height":1591,"rx":538,"fill":554,"stroke":608,"style":561},"40","132","52",[563,1593,1596],{"x":1594,"y":1595,"fill":567,"style":568},"146","163","stdlib + JsonFormatter",[549,1598],{"x":1599,"y":1590,"width":1600,"height":1591,"rx":538,"fill":1198,"stroke":560,"style":561},"256","208",[563,1602,1603],{"x":1201,"y":1185,"fill":567,"style":568},"Async API or many",[563,1605,1607],{"x":1201,"y":1606,"fill":567,"style":568},"172","call sites?",[180,1609],{"x1":1559,"y1":1610,"x2":1563,"y2":1611,"stroke":547,"style":1576},"184","236",[563,1613,1580],{"x":1614,"y":1615,"fill":547,"style":568},"248","214",[180,1617],{"x1":1618,"y1":1610,"x2":1619,"y2":1611,"stroke":547,"style":1576},"410","500",[563,1621,1586],{"x":1622,"y":1615,"fill":547,"style":568},"470",[549,1624],{"x":1625,"y":1626,"width":1627,"height":1628,"rx":538,"fill":554,"stroke":619,"style":561},"96","240","176","46",[563,1630,1632],{"x":1610,"y":1631,"fill":567,"style":568},"268","either works",[549,1634],{"x":1635,"y":1626,"width":481,"height":1628,"rx":538,"fill":554,"stroke":579,"style":561},"452",[563,1637,1639],{"x":1638,"y":1631,"fill":567,"style":568},"552","structlog + contextvars",[14,1641,1642,1643,1645,1646,1648,1649,344,1652,1655,1656,1658,1659,1663,1664,1668],{},"Reach for ",[23,1644,25],{}," when you run an async API and need a ",[23,1647,45],{}," (or ",[23,1650,1651],{},"user_id",[23,1653,1654],{},"tenant",") on every log line without threading it through every function — the ",[23,1657,588],{}," binding is the decisive feature and nothing in stdlib matches it cleanly. Reach for it too when you want key-value ergonomics over f-strings, or you expect your log schema to evolve, since adding or redacting a field is a one-line processor change. That evolvability is what keeps your logs useful as you build out ",[32,1660,1662],{"href":1661},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002F","usage tracking and analytics"," or start ",[32,1665,1667],{"href":1666},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Flogging-api-usage-events-to-postgres\u002F","logging usage events to Postgres"," for billing.",[14,1670,1671,1672,1674,1675,1678,1679,1681,1682,1684],{},"Stay on stdlib ",[23,1673,29],{}," when your service is a small script, cron job, or single-purpose worker where one ",[23,1676,1677],{},"JsonFormatter"," and the occasional ",[23,1680,110],{}," cover every case; when you have no per-request context to propagate, which erases structlog's main advantage; or when you want zero added dependencies and your team already knows ",[23,1683,166],{},". Either way, log to stdout as one JSON object per line and let your platform ship it — that advice from the parent guide is library-agnostic and non-negotiable in a containerized deploy.",[51,1686,1688],{"id":1687},"migration-note-moving-from-stdlib-to-structlog","Migration note: moving from stdlib to structlog",[14,1690,1691,1692,1694,1695,1697,1698,1700,1701,1704,1705,1708,1709,1712,1713,1716,1717,1721,1722,1725],{},"You do not have to rewrite everything at once, and you should not try to. ",[23,1693,25],{}," can sit on top of the stdlib logging system, so logs from third-party libraries — every one of which uses stdlib ",[23,1696,29],{}," — and your new ",[23,1699,25],{}," calls render through the same JSON pipeline. Point stdlib's root logger at a ",[23,1702,1703],{},"structlog.stdlib.ProcessorFormatter",", then migrate your own call sites from ",[23,1706,1707],{},"logger.info(\"x\", extra={...})"," to ",[23,1710,1711],{},"log.info(\"x\", **kwargs)"," module by module. Existing ",[23,1714,1715],{},"logging.getLogger(__name__)"," calls keep working the whole time, so the transition is mechanical and reversible per file rather than a big-bang rewrite you have to land in one deploy. Verify the migrated modules with a quick assertion in your ",[32,1718,1720],{"href":1719},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","pytest suite"," using ",[23,1723,1724],{},"structlog.testing.capture_logs",", which records emitted events as plain dicts you can assert against.",[51,1727,1729],{"id":1728},"builder-verdict","Builder verdict",[14,1731,1732,1733,1735,1736,1738,1739,1741,1742,1744,1745,1747,1748,1750],{},"If you are running a commercial async API on FastAPI, use ",[23,1734,25],{},". The contextvars-bound ",[23,1737,45],{}," alone justifies it: it is the difference between grepping one id to trace a customer's failed request and reconstructing the call from scattered timestamps at 2 a.m. The processor pipeline keeps PII redaction and schema changes to one-line edits, the stdlib bridge means you adopt it without losing library logs, and the per-call overhead is measured in microseconds you will never feel behind a database round-trip. Reserve plain stdlib ",[23,1740,29],{}," for scripts and workers with no request context to carry — there, ",[23,1743,25],{}," is dependency weight for a feature you will not use. The one discipline it demands is the ",[23,1746,1117],{}," in ",[23,1749,1121],{},"; get that right and everything else is upside.",[51,1752,1754],{"id":1753},"faq","FAQ",[14,1756,1757,1761,1762,1764],{},[1758,1759,1760],"strong",{},"How much does structured logging cost to run at 1M requests a month?","\nThe CPU is negligible — JSON serialization is single-digit microseconds per line, lost next to any I\u002FO. The real cost is log ingestion: at five lines per request and roughly 500 bytes each you produce about 2.5 GB a month, so your bill is set by your log platform's per-GB rate, not by structlog. Sample aggressively at ",[23,1763,1425],{}," and the number stays small.",[14,1766,1767,1770,1771,1774,1775,1777,1778,1780,1781,1783,1784,1533],{},[1758,1768,1769],{},"Is structlog slower than stdlib logging?","\nNo meaningfully. Per-call overhead is comparable and dominated by JSON serialization, which both approaches pay. ",[23,1772,1773],{},"cache_logger_on_first_use=True"," and ",[23,1776,1433],{}," discard below-threshold calls before the pipeline runs, so filtered ",[23,1779,1429],{}," logs cost almost nothing. Swap in ",[23,1782,1413],{}," as the serializer and structlog is typically faster than a stdlib ",[23,1785,1677],{},[14,1787,1788,1791,1792,1794],{},[1758,1789,1790],{},"Can I keep using logging.getLogger() with structlog?","\nYes. structlog bridges to the stdlib logging system, so existing ",[23,1793,1715],{}," calls and third-party library logs continue to work and can render through the same JSON pipeline. That bridge is exactly what makes an incremental, per-module migration safe.",[14,1796,1797,1800,1802,1803,1805,1806,1808,1809,1811],{},[1758,1798,1799],{},"How does contextvars handle concurrent async requests without leaking data?",[23,1801,588],{}," are isolated per async task, so two requests handled concurrently each see only their own bound ",[23,1804,45],{}," — no cross-talk. The one rule is to ",[23,1807,1117],{}," in a ",[23,1810,1121],{}," block when the request finishes, so values never leak into the next request scheduled on that reused task.",[14,1813,1814,1817],{},[1758,1815,1816],{},"Do I still need Prometheus if I have structured logs?","\nYes — they solve different problems. Structured logs give per-request detail for debugging one incident; Prometheus gives aggregate metrics, percentiles, and alerting across all of them. The parent monitoring guide wires up both, because logs alone give you no dashboards and no way to page yourself when p99 latency doubles.",[51,1819,1821],{"id":1820},"related","Related",[14,1823,1824],{},[1758,1825,1826],{},"Same track:",[1828,1829,1830,1837,1843],"ul",{},[1831,1832,1833,1836],"li",{},[32,1834,1835],{"href":34},"Monitoring and Logging Python APIs"," — the parent guide that wires structlog together with Prometheus metrics.",[1831,1838,1839,1842],{},[32,1840,1841],{"href":1251},"Running Background Jobs with Celery"," — where you must re-bind request context because it does not cross the process boundary.",[1831,1844,1845,1848,1849,1533],{},[32,1846,1847],{"href":1719},"Testing Python APIs with pytest"," — assert on emitted log events with ",[23,1850,1851],{},"capture_logs",[14,1853,1854],{},[1758,1855,1856],{},"Other tracks:",[1828,1858,1859,1865],{},[1831,1860,1861,1864],{},[32,1862,1863],{"href":1666},"Logging API Usage Events to Postgres"," — structured events feeding a billing and analytics store.",[1831,1866,1867,1870],{},[32,1868,1869],{"href":1531},"Calculating Cost per API Request"," — put a number on what each log line costs to ingest.",[1872,1873,1874],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":174,"searchDepth":188,"depth":188,"links":1876},[1877,1878,1881,1882,1883,1884,1885,1886,1887],{"id":53,"depth":188,"text":54},{"id":501,"depth":188,"text":502,"children":1879},[1880],{"id":854,"depth":199,"text":855},{"id":1134,"depth":188,"text":1135},{"id":1259,"depth":188,"text":1260},{"id":1536,"depth":188,"text":1537},{"id":1687,"depth":188,"text":1688},{"id":1728,"depth":188,"text":1729},{"id":1753,"depth":188,"text":1754},{"id":1820,"depth":188,"text":1821},"Choose between structlog and stdlib logging for Python APIs: contextvars-bound request context, a JSON processor pipeline, redaction, and FastAPI wiring.","md",{"pageTitle":1891,"datePublished":1892,"dateModified":1893},"Structured Logging with structlog for Python APIs","2026-06-18","2026-07-23","\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog",{"title":5,"description":1888},"scaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002Findex","MV73nFIXpi3V8kP1huOf8Jf3IkZKxnhr_9EjcdyidIo",{"@context":1899,"@type":1900,"mainEntity":1901},"https:\u002F\u002Fschema.org","FAQPage",[1902,1907,1910,1913,1916],{"@type":1903,"name":1760,"acceptedAnswer":1904},"Question",{"@type":1905,"text":1906},"Answer","The CPU is negligible — JSON serialization is single-digit microseconds per line, lost next to any I\u002FO. The real cost is log ingestion: at five lines per request and roughly 500 bytes each you produce about 2.5 GB a month, so your bill is set by your log platform's per-GB rate, not by structlog. Sample aggressively at INFO and the number stays small.",{"@type":1903,"name":1769,"acceptedAnswer":1908},{"@type":1905,"text":1909},"No meaningfully. Per-call overhead is comparable and dominated by JSON serialization, which both approaches pay. cache_logger_on_first_use=True and make_filtering_bound_logger discard below-threshold calls before the pipeline runs, so filtered DEBUG logs cost almost nothing. Swap in orjson as the serializer and structlog is typically faster than a stdlib JsonFormatter.",{"@type":1903,"name":1790,"acceptedAnswer":1911},{"@type":1905,"text":1912},"Yes. structlog bridges to the stdlib logging system, so existing logging.getLogger(__name__) calls and third-party library logs continue to work and can render through the same JSON pipeline. That bridge is exactly what makes an incremental, per-module migration safe.",{"@type":1903,"name":1799,"acceptedAnswer":1914},{"@type":1905,"text":1915},"contextvars are isolated per async task, so two requests handled concurrently each see only their own bound request_id — no cross-talk. The one rule is to clear_contextvars() in a finally block when the request finishes, so values never leak into the next request scheduled on that reused task.",{"@type":1903,"name":1816,"acceptedAnswer":1917},{"@type":1905,"text":1918},"Yes — they solve different problems. Structured logs give per-request detail for debugging one incident; Prometheus gives aggregate metrics, percentiles, and alerting across all of them. The parent monitoring guide wires up both, because logs alone give you no dashboards and no way to page yourself when p99 latency doubles.",1784887028359]