[{"data":1,"prerenderedAt":2278},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002F":3,"content-directory":2032},{"id":4,"title":5,"body":6,"date":2019,"description":2020,"difficulty":2021,"draft":2022,"extension":2023,"meta":2024,"navigation":265,"path":2025,"seo":2026,"stem":2027,"tags":2028,"updated":2019,"__hash__":2031},"content\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002Findex.md","Structured JSON Logging in Python CLIs",{"type":7,"value":8,"toc":2000},"minimark",[9,30,35,92,96,110,114,192,195,199,209,536,547,668,734,764,768,788,791,1044,1074,1078,1095,1098,1191,1212,1216,1229,1411,1433,1437,1451,1640,1661,1665,1671,1849,1864,1868,1926,1930,1935,1938,1942,1945,1949,1952,1956,1959,1963,1966,1970,1996],[10,11,12,13,17,18,21,22,29],"p",{},"When your CLI runs in CI, under a service manager, or inside a container, its logs are read by machines before humans: a collector ships them, ",[14,15,16],"code",{},"jq"," filters them, a query indexes them. Free-form text like ",[14,19,20],{},"INFO connected to db in 0.3s"," fights every one of those tools. This page shows how to emit one JSON object per log line — with a stdlib formatter or ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fwww.structlog.org\u002F",[27],"nofollow","structlog"," — attach context fields such as a request ID, and still fall back to pretty console output when a human is watching.",[31,32,34],"h2",{"id":33},"tldr","TL;DR",[36,37,38,45,60,74,81],"ul",{},[39,40,41,42,44],"li",{},"One JSON object per line (JSON Lines) makes logs greppable, ",[14,43,16],{},"-able, and ready for any log pipeline.",[39,46,47,48,51,52,55,56,59],{},"The zero-dependency route is a custom ",[14,49,50],{},"logging.Formatter"," whose ",[14,53,54],{},"format()"," returns ",[14,57,58],{},"json.dumps(...)",".",[39,61,62,63,65,66,69,70,73],{},"The ergonomic route is ",[14,64,28],{},": composable processors, ",[14,67,68],{},"add_log_level",", an ISO timestamp, and ",[14,71,72],{},"JSONRenderer"," at the end.",[39,75,76,77,80],{},"Bind context once (",[14,78,79],{},"log = log.bind(request_id=...)",") and every later record carries it automatically.",[39,82,83,84,87,88,91],{},"Switch between JSON and a console renderer based on ",[14,85,86],{},"stderr.isatty()"," or an explicit ",[14,89,90],{},"--log-format"," flag.",[31,93,95],{"id":94},"why-json-logs-at-all","Why JSON logs at all",[10,97,98,99,102,103,102,106,109],{},"Structured logs turn \"search the text\" into \"query the fields.\" Once each line is an object with ",[14,100,101],{},"level",", ",[14,104,105],{},"event",[14,107,108],{},"timestamp",", and your own keys, you can answer operational questions with standard tools instead of fragile regexes:",[111,112],"inline-diagram",{"name":113},"json-log-consumers",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-bash shiki shiki-themes github-light github-dark","$ mycli sync --log-format json 2>logs.jsonl\n$ jq -c 'select(.level==\"error\")' logs.jsonl        # only errors\n$ jq -r 'select(.request_id==\"r-42\") | .event' logs.jsonl   # one request's trail\n","bash","",[14,122,123,153,174],{"__ignoreMap":120},[124,125,128,132,136,139,143,146,150],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"sScJk","$",[124,133,135],{"class":134},"sZZnC"," mycli",[124,137,138],{"class":134}," sync",[124,140,142],{"class":141},"sj4cs"," --log-format",[124,144,145],{"class":134}," json",[124,147,149],{"class":148},"szBVR"," 2>",[124,151,152],{"class":134},"logs.jsonl\n",[124,154,156,158,161,164,167,170],{"class":126,"line":155},2,[124,157,131],{"class":130},[124,159,160],{"class":134}," jq",[124,162,163],{"class":141}," -c",[124,165,166],{"class":134}," 'select(.level==\"error\")'",[124,168,169],{"class":134}," logs.jsonl",[124,171,173],{"class":172},"sJ8bj","        # only errors\n",[124,175,177,179,181,184,187,189],{"class":126,"line":176},3,[124,178,131],{"class":130},[124,180,160],{"class":134},[124,182,183],{"class":141}," -r",[124,185,186],{"class":134}," 'select(.request_id==\"r-42\") | .event'",[124,188,169],{"class":134},[124,190,191],{"class":172},"   # one request's trail\n",[10,193,194],{},"Text logs can't do that reliably — a message format change breaks the grep. JSON logs are also what platforms like CloudWatch, Loki, and the systemd journal expect for automatic field extraction. The cost is readability for a human at a terminal, which is exactly why you keep a console renderer for interactive runs and reserve JSON for when output is redirected.",[31,196,198],{"id":197},"the-stdlib-route-a-custom-json-formatter","The stdlib route: a custom JSON formatter",[10,200,201,202,204,205,208],{},"You don't need a dependency to emit JSON. A ",[14,203,50],{}," subclass that serializes the ",[14,206,207],{},"LogRecord"," is enough, and it slots into the same handler setup any CLI already has:",[115,210,214],{"className":211,"code":212,"language":213,"meta":120,"style":120},"language-python shiki shiki-themes github-light github-dark","from __future__ import annotations\nimport datetime as dt\nimport json\nimport logging\n\n# Attributes the stdlib puts on every LogRecord; anything else is user context.\n_RESERVED = set(logging.makeLogRecord({}).__dict__)\n\n\nclass JsonFormatter(logging.Formatter):\n    def format(self, record: logging.LogRecord) -> str:\n        payload = {\n            \"timestamp\": dt.datetime.fromtimestamp(\n                record.created, tz=dt.timezone.utc\n            ).isoformat(),\n            \"level\": record.levelname,\n            \"logger\": record.name,\n            \"event\": record.getMessage(),\n        }\n        # Merge any fields passed via logging's `extra=` argument.\n        for key, value in record.__dict__.items():\n            if key not in _RESERVED and not key.startswith(\"_\"):\n                payload[key] = value\n        if record.exc_info:\n            payload[\"exc_info\"] = self.formatException(record.exc_info)\n        return json.dumps(payload, default=str)\n","python",[14,215,216,231,245,252,260,267,273,294,299,304,327,345,357,366,381,387,396,405,414,420,426,446,478,489,498,518],{"__ignoreMap":120},[124,217,218,221,224,227],{"class":126,"line":127},[124,219,220],{"class":148},"from",[124,222,223],{"class":141}," __future__",[124,225,226],{"class":148}," import",[124,228,230],{"class":229},"sVt8B"," annotations\n",[124,232,233,236,239,242],{"class":126,"line":155},[124,234,235],{"class":148},"import",[124,237,238],{"class":229}," datetime ",[124,240,241],{"class":148},"as",[124,243,244],{"class":229}," dt\n",[124,246,247,249],{"class":126,"line":176},[124,248,235],{"class":148},[124,250,251],{"class":229}," json\n",[124,253,255,257],{"class":126,"line":254},4,[124,256,235],{"class":148},[124,258,259],{"class":229}," logging\n",[124,261,263],{"class":126,"line":262},5,[124,264,266],{"emptyLinePlaceholder":265},true,"\n",[124,268,270],{"class":126,"line":269},6,[124,271,272],{"class":172},"# Attributes the stdlib puts on every LogRecord; anything else is user context.\n",[124,274,276,279,282,285,288,291],{"class":126,"line":275},7,[124,277,278],{"class":141},"_RESERVED",[124,280,281],{"class":148}," =",[124,283,284],{"class":141}," set",[124,286,287],{"class":229},"(logging.makeLogRecord({}).",[124,289,290],{"class":141},"__dict__",[124,292,293],{"class":229},")\n",[124,295,297],{"class":126,"line":296},8,[124,298,266],{"emptyLinePlaceholder":265},[124,300,302],{"class":126,"line":301},9,[124,303,266],{"emptyLinePlaceholder":265},[124,305,307,310,313,316,319,321,324],{"class":126,"line":306},10,[124,308,309],{"class":148},"class",[124,311,312],{"class":130}," JsonFormatter",[124,314,315],{"class":229},"(",[124,317,318],{"class":130},"logging",[124,320,59],{"class":229},[124,322,323],{"class":130},"Formatter",[124,325,326],{"class":229},"):\n",[124,328,330,333,336,339,342],{"class":126,"line":329},11,[124,331,332],{"class":148},"    def",[124,334,335],{"class":141}," format",[124,337,338],{"class":229},"(self, record: logging.LogRecord) -> ",[124,340,341],{"class":141},"str",[124,343,344],{"class":229},":\n",[124,346,348,351,354],{"class":126,"line":347},12,[124,349,350],{"class":229},"        payload ",[124,352,353],{"class":148},"=",[124,355,356],{"class":229}," {\n",[124,358,360,363],{"class":126,"line":359},13,[124,361,362],{"class":134},"            \"timestamp\"",[124,364,365],{"class":229},": dt.datetime.fromtimestamp(\n",[124,367,369,372,376,378],{"class":126,"line":368},14,[124,370,371],{"class":229},"                record.created, ",[124,373,375],{"class":374},"s4XuR","tz",[124,377,353],{"class":148},[124,379,380],{"class":229},"dt.timezone.utc\n",[124,382,384],{"class":126,"line":383},15,[124,385,386],{"class":229},"            ).isoformat(),\n",[124,388,390,393],{"class":126,"line":389},16,[124,391,392],{"class":134},"            \"level\"",[124,394,395],{"class":229},": record.levelname,\n",[124,397,399,402],{"class":126,"line":398},17,[124,400,401],{"class":134},"            \"logger\"",[124,403,404],{"class":229},": record.name,\n",[124,406,408,411],{"class":126,"line":407},18,[124,409,410],{"class":134},"            \"event\"",[124,412,413],{"class":229},": record.getMessage(),\n",[124,415,417],{"class":126,"line":416},19,[124,418,419],{"class":229},"        }\n",[124,421,423],{"class":126,"line":422},20,[124,424,425],{"class":172},"        # Merge any fields passed via logging's `extra=` argument.\n",[124,427,429,432,435,438,441,443],{"class":126,"line":428},21,[124,430,431],{"class":148},"        for",[124,433,434],{"class":229}," key, value ",[124,436,437],{"class":148},"in",[124,439,440],{"class":229}," record.",[124,442,290],{"class":141},[124,444,445],{"class":229},".items():\n",[124,447,449,452,455,458,461,464,467,470,473,476],{"class":126,"line":448},22,[124,450,451],{"class":148},"            if",[124,453,454],{"class":229}," key ",[124,456,457],{"class":148},"not",[124,459,460],{"class":148}," in",[124,462,463],{"class":141}," _RESERVED",[124,465,466],{"class":148}," and",[124,468,469],{"class":148}," not",[124,471,472],{"class":229}," key.startswith(",[124,474,475],{"class":134},"\"_\"",[124,477,326],{"class":229},[124,479,481,484,486],{"class":126,"line":480},23,[124,482,483],{"class":229},"                payload[key] ",[124,485,353],{"class":148},[124,487,488],{"class":229}," value\n",[124,490,492,495],{"class":126,"line":491},24,[124,493,494],{"class":148},"        if",[124,496,497],{"class":229}," record.exc_info:\n",[124,499,501,504,507,510,512,515],{"class":126,"line":500},25,[124,502,503],{"class":229},"            payload[",[124,505,506],{"class":134},"\"exc_info\"",[124,508,509],{"class":229},"] ",[124,511,353],{"class":148},[124,513,514],{"class":141}," self",[124,516,517],{"class":229},".formatException(record.exc_info)\n",[124,519,521,524,527,530,532,534],{"class":126,"line":520},26,[124,522,523],{"class":148},"        return",[124,525,526],{"class":229}," json.dumps(payload, ",[124,528,529],{"class":374},"default",[124,531,353],{"class":148},[124,533,341],{"class":141},[124,535,293],{"class":229},[10,537,538,539,542,543,546],{},"Wire it to a ",[14,540,541],{},"stderr"," handler and log with ",[14,544,545],{},"extra="," to attach fields:",[115,548,550],{"className":211,"code":549,"language":213,"meta":120,"style":120},"import logging\nimport sys\n\nhandler = logging.StreamHandler(sys.stderr)\nhandler.setFormatter(JsonFormatter())\nlogging.basicConfig(level=logging.INFO, handlers=[handler])\n\nlog = logging.getLogger(\"mycli\")\nlog.info(\"sync complete\", extra={\"request_id\": \"r-42\", \"rows\": 128})\n",[14,551,552,558,565,569,579,584,609,613,628],{"__ignoreMap":120},[124,553,554,556],{"class":126,"line":127},[124,555,235],{"class":148},[124,557,259],{"class":229},[124,559,560,562],{"class":126,"line":155},[124,561,235],{"class":148},[124,563,564],{"class":229}," sys\n",[124,566,567],{"class":126,"line":176},[124,568,266],{"emptyLinePlaceholder":265},[124,570,571,574,576],{"class":126,"line":254},[124,572,573],{"class":229},"handler ",[124,575,353],{"class":148},[124,577,578],{"class":229}," logging.StreamHandler(sys.stderr)\n",[124,580,581],{"class":126,"line":262},[124,582,583],{"class":229},"handler.setFormatter(JsonFormatter())\n",[124,585,586,589,591,593,596,599,601,604,606],{"class":126,"line":269},[124,587,588],{"class":229},"logging.basicConfig(",[124,590,101],{"class":374},[124,592,353],{"class":148},[124,594,595],{"class":229},"logging.",[124,597,598],{"class":141},"INFO",[124,600,102],{"class":229},[124,602,603],{"class":374},"handlers",[124,605,353],{"class":148},[124,607,608],{"class":229},"[handler])\n",[124,610,611],{"class":126,"line":275},[124,612,266],{"emptyLinePlaceholder":265},[124,614,615,618,620,623,626],{"class":126,"line":296},[124,616,617],{"class":229},"log ",[124,619,353],{"class":148},[124,621,622],{"class":229}," logging.getLogger(",[124,624,625],{"class":134},"\"mycli\"",[124,627,293],{"class":229},[124,629,630,633,636,638,641,643,646,649,652,655,657,660,662,665],{"class":126,"line":301},[124,631,632],{"class":229},"log.info(",[124,634,635],{"class":134},"\"sync complete\"",[124,637,102],{"class":229},[124,639,640],{"class":374},"extra",[124,642,353],{"class":148},[124,644,645],{"class":229},"{",[124,647,648],{"class":134},"\"request_id\"",[124,650,651],{"class":229},": ",[124,653,654],{"class":134},"\"r-42\"",[124,656,102],{"class":229},[124,658,659],{"class":134},"\"rows\"",[124,661,651],{"class":229},[124,663,664],{"class":141},"128",[124,666,667],{"class":229},"})\n",[115,669,673],{"className":670,"code":671,"language":672,"meta":120,"style":120},"language-json shiki shiki-themes github-light github-dark","{\"timestamp\": \"2026-07-05T12:00:00+00:00\", \"level\": \"INFO\", \"logger\": \"mycli\", \"event\": \"sync complete\", \"request_id\": \"r-42\", \"rows\": 128}\n","json",[14,674,675],{"__ignoreMap":120},[124,676,677,679,682,684,687,689,692,694,697,699,702,704,706,708,711,713,715,717,719,721,723,725,727,729,731],{"class":126,"line":127},[124,678,645],{"class":229},[124,680,681],{"class":141},"\"timestamp\"",[124,683,651],{"class":229},[124,685,686],{"class":134},"\"2026-07-05T12:00:00+00:00\"",[124,688,102],{"class":229},[124,690,691],{"class":141},"\"level\"",[124,693,651],{"class":229},[124,695,696],{"class":134},"\"INFO\"",[124,698,102],{"class":229},[124,700,701],{"class":141},"\"logger\"",[124,703,651],{"class":229},[124,705,625],{"class":134},[124,707,102],{"class":229},[124,709,710],{"class":141},"\"event\"",[124,712,651],{"class":229},[124,714,635],{"class":134},[124,716,102],{"class":229},[124,718,648],{"class":141},[124,720,651],{"class":229},[124,722,654],{"class":134},[124,724,102],{"class":229},[124,726,659],{"class":141},[124,728,651],{"class":229},[124,730,664],{"class":141},[124,732,733],{"class":229},"}\n",[10,735,736,737,739,740,742,743,747,748,751,752,755,756,759,760,763],{},"The ",[14,738,278],{}," trick is what makes ",[14,741,545],{}," work cleanly: it computes the set of attributes a bare record already has, so anything ",[744,745,746],"em",{},"else"," on the record must be a field you added. ",[14,749,750],{},"default=str"," keeps ",[14,753,754],{},"json.dumps"," from crashing on a ",[14,757,758],{},"Path"," or ",[14,761,762],{},"datetime"," value someone logs.",[31,765,767],{"id":766},"the-structlog-route-processors-and-renderers","The structlog route: processors and renderers",[10,769,770,772,773,777,778,781,782,784,785,59],{},[14,771,28],{}," is worth the dependency once you want context binding and a clean pipeline. You compose a list of ",[774,775,776],"strong",{},"processors"," — small functions that each mutate the event dict — ending in a ",[774,779,780],{},"renderer"," that turns the dict into a string. For JSON that's ",[14,783,72],{},"; for humans it's ",[14,786,787],{},"ConsoleRenderer",[111,789],{"name":790},"structlog-processor-chain",[115,792,794],{"className":211,"code":793,"language":213,"meta":120,"style":120},"import logging\nimport structlog\n\ndef configure_structlog(json_logs: bool) -> None:\n    shared = [\n        structlog.contextvars.merge_contextvars,\n        structlog.processors.add_log_level,\n        structlog.processors.TimeStamper(fmt=\"iso\", utc=True),\n        structlog.processors.StackInfoRenderer(),\n        structlog.processors.format_exc_info,\n    ]\n    renderer = (\n        structlog.processors.JSONRenderer()\n        if json_logs\n        else structlog.dev.ConsoleRenderer(colors=True)\n    )\n    structlog.configure(\n        processors=[*shared, renderer],\n        wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),\n        logger_factory=structlog.PrintLoggerFactory(),\n        cache_logger_on_first_use=True,\n    )\n\nlog = structlog.get_logger(\"mycli\")\nlog.info(\"sync complete\", request_id=\"r-42\", rows=128)\n",[14,795,796,802,809,813,835,845,850,855,881,886,891,896,906,911,918,935,940,945,961,975,985,997,1001,1005,1018],{"__ignoreMap":120},[124,797,798,800],{"class":126,"line":127},[124,799,235],{"class":148},[124,801,259],{"class":229},[124,803,804,806],{"class":126,"line":155},[124,805,235],{"class":148},[124,807,808],{"class":229}," structlog\n",[124,810,811],{"class":126,"line":176},[124,812,266],{"emptyLinePlaceholder":265},[124,814,815,818,821,824,827,830,833],{"class":126,"line":254},[124,816,817],{"class":148},"def",[124,819,820],{"class":130}," configure_structlog",[124,822,823],{"class":229},"(json_logs: ",[124,825,826],{"class":141},"bool",[124,828,829],{"class":229},") -> ",[124,831,832],{"class":141},"None",[124,834,344],{"class":229},[124,836,837,840,842],{"class":126,"line":262},[124,838,839],{"class":229},"    shared ",[124,841,353],{"class":148},[124,843,844],{"class":229}," [\n",[124,846,847],{"class":126,"line":269},[124,848,849],{"class":229},"        structlog.contextvars.merge_contextvars,\n",[124,851,852],{"class":126,"line":275},[124,853,854],{"class":229},"        structlog.processors.add_log_level,\n",[124,856,857,860,863,865,868,870,873,875,878],{"class":126,"line":296},[124,858,859],{"class":229},"        structlog.processors.TimeStamper(",[124,861,862],{"class":374},"fmt",[124,864,353],{"class":148},[124,866,867],{"class":134},"\"iso\"",[124,869,102],{"class":229},[124,871,872],{"class":374},"utc",[124,874,353],{"class":148},[124,876,877],{"class":141},"True",[124,879,880],{"class":229},"),\n",[124,882,883],{"class":126,"line":301},[124,884,885],{"class":229},"        structlog.processors.StackInfoRenderer(),\n",[124,887,888],{"class":126,"line":306},[124,889,890],{"class":229},"        structlog.processors.format_exc_info,\n",[124,892,893],{"class":126,"line":329},[124,894,895],{"class":229},"    ]\n",[124,897,898,901,903],{"class":126,"line":347},[124,899,900],{"class":229},"    renderer ",[124,902,353],{"class":148},[124,904,905],{"class":229}," (\n",[124,907,908],{"class":126,"line":359},[124,909,910],{"class":229},"        structlog.processors.JSONRenderer()\n",[124,912,913,915],{"class":126,"line":368},[124,914,494],{"class":148},[124,916,917],{"class":229}," json_logs\n",[124,919,920,923,926,929,931,933],{"class":126,"line":383},[124,921,922],{"class":148},"        else",[124,924,925],{"class":229}," structlog.dev.ConsoleRenderer(",[124,927,928],{"class":374},"colors",[124,930,353],{"class":148},[124,932,877],{"class":141},[124,934,293],{"class":229},[124,936,937],{"class":126,"line":389},[124,938,939],{"class":229},"    )\n",[124,941,942],{"class":126,"line":398},[124,943,944],{"class":229},"    structlog.configure(\n",[124,946,947,950,952,955,958],{"class":126,"line":407},[124,948,949],{"class":374},"        processors",[124,951,353],{"class":148},[124,953,954],{"class":229},"[",[124,956,957],{"class":148},"*",[124,959,960],{"class":229},"shared, renderer],\n",[124,962,963,966,968,971,973],{"class":126,"line":416},[124,964,965],{"class":374},"        wrapper_class",[124,967,353],{"class":148},[124,969,970],{"class":229},"structlog.make_filtering_bound_logger(logging.",[124,972,598],{"class":141},[124,974,880],{"class":229},[124,976,977,980,982],{"class":126,"line":422},[124,978,979],{"class":374},"        logger_factory",[124,981,353],{"class":148},[124,983,984],{"class":229},"structlog.PrintLoggerFactory(),\n",[124,986,987,990,992,994],{"class":126,"line":428},[124,988,989],{"class":374},"        cache_logger_on_first_use",[124,991,353],{"class":148},[124,993,877],{"class":141},[124,995,996],{"class":229},",\n",[124,998,999],{"class":126,"line":448},[124,1000,939],{"class":229},[124,1002,1003],{"class":126,"line":480},[124,1004,266],{"emptyLinePlaceholder":265},[124,1006,1007,1009,1011,1014,1016],{"class":126,"line":491},[124,1008,617],{"class":229},[124,1010,353],{"class":148},[124,1012,1013],{"class":229}," structlog.get_logger(",[124,1015,625],{"class":134},[124,1017,293],{"class":229},[124,1019,1020,1022,1024,1026,1029,1031,1033,1035,1038,1040,1042],{"class":126,"line":500},[124,1021,632],{"class":229},[124,1023,635],{"class":134},[124,1025,102],{"class":229},[124,1027,1028],{"class":374},"request_id",[124,1030,353],{"class":148},[124,1032,654],{"class":134},[124,1034,102],{"class":229},[124,1036,1037],{"class":374},"rows",[124,1039,353],{"class":148},[124,1041,664],{"class":141},[124,1043,293],{"class":229},[10,1045,1046,1047,1050,1051,1053,1054,102,1056,1059,1060,1062,1063,1066,1067,1070,1071,1073],{},"With ",[14,1048,1049],{},"json_logs=True"," you get the same one-object-per-line output as the stdlib formatter, but the pipeline is declarative: ",[14,1052,68],{}," injects ",[14,1055,101],{},[14,1057,1058],{},"TimeStamper"," injects an ISO ",[14,1061,108],{},", and ",[14,1064,1065],{},"format_exc_info"," renders exceptions. Flip the flag and the identical call sites render as colorized ",[14,1068,1069],{},"key=value"," lines for a developer. Note that context fields are keyword arguments here — no ",[14,1072,545],{}," wrapper — which is the main ergonomic win over the stdlib.",[31,1075,1077],{"id":1076},"binding-context-so-every-line-carries-it","Binding context so every line carries it",[10,1079,1080,1081,102,1083,1086,1087,1090,1091,1094],{},"The reason to log structurally is context: you want every record within an operation to carry the same ",[14,1082,1028],{},[14,1084,1085],{},"user",", or ",[14,1088,1089],{},"command"," without repeating it at each call. ",[14,1092,1093],{},"bind()"," returns a new logger with those fields baked in:",[111,1096],{"name":1097},"log-context-binding",[115,1099,1101],{"className":211,"code":1100,"language":213,"meta":120,"style":120},"log = structlog.get_logger(\"mycli\").bind(request_id=\"r-42\", command=\"sync\")\nlog.info(\"started\")                      # includes request_id + command\nlog.info(\"fetched\", rows=128)            # includes them too, plus rows\nlog.warning(\"retrying\", attempt=2)       # still carried\n",[14,1102,1103,1133,1146,1167],{"__ignoreMap":120},[124,1104,1105,1107,1109,1111,1113,1116,1118,1120,1122,1124,1126,1128,1131],{"class":126,"line":127},[124,1106,617],{"class":229},[124,1108,353],{"class":148},[124,1110,1013],{"class":229},[124,1112,625],{"class":134},[124,1114,1115],{"class":229},").bind(",[124,1117,1028],{"class":374},[124,1119,353],{"class":148},[124,1121,654],{"class":134},[124,1123,102],{"class":229},[124,1125,1089],{"class":374},[124,1127,353],{"class":148},[124,1129,1130],{"class":134},"\"sync\"",[124,1132,293],{"class":229},[124,1134,1135,1137,1140,1143],{"class":126,"line":155},[124,1136,632],{"class":229},[124,1138,1139],{"class":134},"\"started\"",[124,1141,1142],{"class":229},")                      ",[124,1144,1145],{"class":172},"# includes request_id + command\n",[124,1147,1148,1150,1153,1155,1157,1159,1161,1164],{"class":126,"line":176},[124,1149,632],{"class":229},[124,1151,1152],{"class":134},"\"fetched\"",[124,1154,102],{"class":229},[124,1156,1037],{"class":374},[124,1158,353],{"class":148},[124,1160,664],{"class":141},[124,1162,1163],{"class":229},")            ",[124,1165,1166],{"class":172},"# includes them too, plus rows\n",[124,1168,1169,1172,1175,1177,1180,1182,1185,1188],{"class":126,"line":254},[124,1170,1171],{"class":229},"log.warning(",[124,1173,1174],{"class":134},"\"retrying\"",[124,1176,102],{"class":229},[124,1178,1179],{"class":374},"attempt",[124,1181,353],{"class":148},[124,1183,1184],{"class":141},"2",[124,1186,1187],{"class":229},")       ",[124,1189,1190],{"class":172},"# still carried\n",[10,1192,1193,1194,1196,1197,1199,1200,1203,1204,1207,1208,59],{},"Every line inherits ",[14,1195,1028],{}," and ",[14,1198,1089],{},". For fields that should span function boundaries without threading a logger object through every call, use ",[14,1201,1202],{},"structlog.contextvars.bind_contextvars(request_id=\"r-42\")"," at the top of your command; the ",[14,1205,1206],{},"merge_contextvars"," processor folds them into every record on the current context — which is exactly how you'd stamp one ID across an entire CLI invocation set up through a ",[23,1209,1211],{"href":1210},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002F","Click context object",[31,1213,1215],{"id":1214},"switching-json-on-and-off","Switching JSON on and off",[10,1217,1218,1219,1221,1222,1225,1226,1228],{},"Autodetect the common case, then let a flag win. Emit console output when ",[14,1220,541],{}," is a real terminal and JSON otherwise (pipes, CI, ",[14,1223,1224],{},"systemd","), with ",[14,1227,90],{}," as an explicit override:",[115,1230,1232],{"className":211,"code":1231,"language":213,"meta":120,"style":120},"import sys\nimport click\n\n@click.command()\n@click.option(\"--log-format\", type=click.Choice([\"auto\", \"json\", \"console\"]),\n              default=\"auto\")\ndef main(log_format: str) -> None:\n    if log_format == \"auto\":\n        json_logs = not sys.stderr.isatty()   # redirected -> JSON\n    else:\n        json_logs = log_format == \"json\"\n    configure_structlog(json_logs=json_logs)\n    structlog.get_logger(\"mycli\").info(\"ready\", log_format=log_format)\n",[14,1233,1234,1240,1247,1251,1259,1295,1306,1324,1340,1355,1362,1375,1388],{"__ignoreMap":120},[124,1235,1236,1238],{"class":126,"line":127},[124,1237,235],{"class":148},[124,1239,564],{"class":229},[124,1241,1242,1244],{"class":126,"line":155},[124,1243,235],{"class":148},[124,1245,1246],{"class":229}," click\n",[124,1248,1249],{"class":126,"line":176},[124,1250,266],{"emptyLinePlaceholder":265},[124,1252,1253,1256],{"class":126,"line":254},[124,1254,1255],{"class":130},"@click.command",[124,1257,1258],{"class":229},"()\n",[124,1260,1261,1264,1266,1269,1271,1274,1276,1279,1282,1284,1287,1289,1292],{"class":126,"line":262},[124,1262,1263],{"class":130},"@click.option",[124,1265,315],{"class":229},[124,1267,1268],{"class":134},"\"--log-format\"",[124,1270,102],{"class":229},[124,1272,1273],{"class":374},"type",[124,1275,353],{"class":148},[124,1277,1278],{"class":229},"click.Choice([",[124,1280,1281],{"class":134},"\"auto\"",[124,1283,102],{"class":229},[124,1285,1286],{"class":134},"\"json\"",[124,1288,102],{"class":229},[124,1290,1291],{"class":134},"\"console\"",[124,1293,1294],{"class":229},"]),\n",[124,1296,1297,1300,1302,1304],{"class":126,"line":269},[124,1298,1299],{"class":374},"              default",[124,1301,353],{"class":148},[124,1303,1281],{"class":134},[124,1305,293],{"class":229},[124,1307,1308,1310,1313,1316,1318,1320,1322],{"class":126,"line":275},[124,1309,817],{"class":148},[124,1311,1312],{"class":130}," main",[124,1314,1315],{"class":229},"(log_format: ",[124,1317,341],{"class":141},[124,1319,829],{"class":229},[124,1321,832],{"class":141},[124,1323,344],{"class":229},[124,1325,1326,1329,1332,1335,1338],{"class":126,"line":296},[124,1327,1328],{"class":148},"    if",[124,1330,1331],{"class":229}," log_format ",[124,1333,1334],{"class":148},"==",[124,1336,1337],{"class":134}," \"auto\"",[124,1339,344],{"class":229},[124,1341,1342,1345,1347,1349,1352],{"class":126,"line":301},[124,1343,1344],{"class":229},"        json_logs ",[124,1346,353],{"class":148},[124,1348,469],{"class":148},[124,1350,1351],{"class":229}," sys.stderr.isatty()   ",[124,1353,1354],{"class":172},"# redirected -> JSON\n",[124,1356,1357,1360],{"class":126,"line":306},[124,1358,1359],{"class":148},"    else",[124,1361,344],{"class":229},[124,1363,1364,1366,1368,1370,1372],{"class":126,"line":329},[124,1365,1344],{"class":229},[124,1367,353],{"class":148},[124,1369,1331],{"class":229},[124,1371,1334],{"class":148},[124,1373,1374],{"class":134}," \"json\"\n",[124,1376,1377,1380,1383,1385],{"class":126,"line":347},[124,1378,1379],{"class":229},"    configure_structlog(",[124,1381,1382],{"class":374},"json_logs",[124,1384,353],{"class":148},[124,1386,1387],{"class":229},"json_logs)\n",[124,1389,1390,1393,1395,1398,1401,1403,1406,1408],{"class":126,"line":359},[124,1391,1392],{"class":229},"    structlog.get_logger(",[124,1394,625],{"class":134},[124,1396,1397],{"class":229},").info(",[124,1399,1400],{"class":134},"\"ready\"",[124,1402,102],{"class":229},[124,1404,1405],{"class":374},"log_format",[124,1407,353],{"class":148},[124,1409,1410],{"class":229},"log_format)\n",[10,1412,1413,1414,1418,1419,1422,1423,1418,1425,1428,1429,59],{},"This pairs naturally with verbosity: the ",[23,1415,1417],{"href":1416},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002F","verbose and quiet flags guide"," controls ",[744,1420,1421],{},"how much"," is logged while ",[14,1424,90],{},[744,1426,1427],{},"how it's rendered",". The two are orthogonal knobs on the same logger, and both are introduced in the ",[23,1430,1432],{"href":1431},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging overview",[31,1434,1436],{"id":1435},"routing-library-logs-through-the-same-renderer","Routing library logs through the same renderer",[10,1438,1439,1440,1442,1443,1446,1447,1450],{},"Your own logs are only half the story. The HTTP client, database driver, and other dependencies your CLI pulls in all log through the stdlib ",[14,1441,318],{}," module — and by default those records bypass structlog entirely, landing as unformatted text amid your clean JSON. Wire structlog's ",[14,1444,1445],{},"ProcessorFormatter"," into a stdlib handler so ",[744,1448,1449],{},"every"," record, yours and theirs, exits as one consistent JSON stream:",[115,1452,1454],{"className":211,"code":1453,"language":213,"meta":120,"style":120},"import logging\nimport structlog\n\ndef unify_logging(json_logs: bool) -> None:\n    renderer = (\n        structlog.processors.JSONRenderer()\n        if json_logs\n        else structlog.dev.ConsoleRenderer(colors=True)\n    )\n    formatter = structlog.stdlib.ProcessorFormatter(\n        processor=renderer,\n        foreign_pre_chain=[            # applied to records from stdlib loggers\n            structlog.processors.add_log_level,\n            structlog.processors.TimeStamper(fmt=\"iso\", utc=True),\n        ],\n    )\n    handler = logging.StreamHandler()          # stderr\n    handler.setFormatter(formatter)\n    root = logging.getLogger()\n    root.handlers[:] = [handler]\n    root.setLevel(logging.INFO)\n",[14,1455,1456,1462,1468,1472,1489,1497,1501,1507,1521,1525,1535,1545,1558,1563,1584,1589,1593,1606,1611,1621,1631],{"__ignoreMap":120},[124,1457,1458,1460],{"class":126,"line":127},[124,1459,235],{"class":148},[124,1461,259],{"class":229},[124,1463,1464,1466],{"class":126,"line":155},[124,1465,235],{"class":148},[124,1467,808],{"class":229},[124,1469,1470],{"class":126,"line":176},[124,1471,266],{"emptyLinePlaceholder":265},[124,1473,1474,1476,1479,1481,1483,1485,1487],{"class":126,"line":254},[124,1475,817],{"class":148},[124,1477,1478],{"class":130}," unify_logging",[124,1480,823],{"class":229},[124,1482,826],{"class":141},[124,1484,829],{"class":229},[124,1486,832],{"class":141},[124,1488,344],{"class":229},[124,1490,1491,1493,1495],{"class":126,"line":262},[124,1492,900],{"class":229},[124,1494,353],{"class":148},[124,1496,905],{"class":229},[124,1498,1499],{"class":126,"line":269},[124,1500,910],{"class":229},[124,1502,1503,1505],{"class":126,"line":275},[124,1504,494],{"class":148},[124,1506,917],{"class":229},[124,1508,1509,1511,1513,1515,1517,1519],{"class":126,"line":296},[124,1510,922],{"class":148},[124,1512,925],{"class":229},[124,1514,928],{"class":374},[124,1516,353],{"class":148},[124,1518,877],{"class":141},[124,1520,293],{"class":229},[124,1522,1523],{"class":126,"line":301},[124,1524,939],{"class":229},[124,1526,1527,1530,1532],{"class":126,"line":306},[124,1528,1529],{"class":229},"    formatter ",[124,1531,353],{"class":148},[124,1533,1534],{"class":229}," structlog.stdlib.ProcessorFormatter(\n",[124,1536,1537,1540,1542],{"class":126,"line":329},[124,1538,1539],{"class":374},"        processor",[124,1541,353],{"class":148},[124,1543,1544],{"class":229},"renderer,\n",[124,1546,1547,1550,1552,1555],{"class":126,"line":347},[124,1548,1549],{"class":374},"        foreign_pre_chain",[124,1551,353],{"class":148},[124,1553,1554],{"class":229},"[            ",[124,1556,1557],{"class":172},"# applied to records from stdlib loggers\n",[124,1559,1560],{"class":126,"line":359},[124,1561,1562],{"class":229},"            structlog.processors.add_log_level,\n",[124,1564,1565,1568,1570,1572,1574,1576,1578,1580,1582],{"class":126,"line":368},[124,1566,1567],{"class":229},"            structlog.processors.TimeStamper(",[124,1569,862],{"class":374},[124,1571,353],{"class":148},[124,1573,867],{"class":134},[124,1575,102],{"class":229},[124,1577,872],{"class":374},[124,1579,353],{"class":148},[124,1581,877],{"class":141},[124,1583,880],{"class":229},[124,1585,1586],{"class":126,"line":383},[124,1587,1588],{"class":229},"        ],\n",[124,1590,1591],{"class":126,"line":389},[124,1592,939],{"class":229},[124,1594,1595,1598,1600,1603],{"class":126,"line":398},[124,1596,1597],{"class":229},"    handler ",[124,1599,353],{"class":148},[124,1601,1602],{"class":229}," logging.StreamHandler()          ",[124,1604,1605],{"class":172},"# stderr\n",[124,1607,1608],{"class":126,"line":407},[124,1609,1610],{"class":229},"    handler.setFormatter(formatter)\n",[124,1612,1613,1616,1618],{"class":126,"line":416},[124,1614,1615],{"class":229},"    root ",[124,1617,353],{"class":148},[124,1619,1620],{"class":229}," logging.getLogger()\n",[124,1622,1623,1626,1628],{"class":126,"line":422},[124,1624,1625],{"class":229},"    root.handlers[:] ",[124,1627,353],{"class":148},[124,1629,1630],{"class":229}," [handler]\n",[124,1632,1633,1636,1638],{"class":126,"line":428},[124,1634,1635],{"class":229},"    root.setLevel(logging.",[124,1637,598],{"class":141},[124,1639,293],{"class":229},[10,1641,1642,1645,1646,759,1649,1652,1653,1656,1657,1660],{},[14,1643,1644],{},"foreign_pre_chain"," is the key: it runs the timestamp and level processors against records that originated in the stdlib (a ",[14,1647,1648],{},"requests",[14,1650,1651],{},"urllib3"," logger, say) so they carry the same fields as your structlog events. The result is a single JSON stream a collector can parse without special-casing which library emitted a line. This is also where verbosity and format meet: the level you set here comes from the ",[23,1654,1655],{"href":1416},"verbose and quiet flags",", and it applies to library logs too — a well-behaved reason to keep the default at ",[14,1658,1659],{},"WARNING"," so a chatty dependency doesn't drown your output.",[31,1662,1664],{"id":1663},"testing-captured-json","Testing captured JSON",[10,1666,1667,1668,1670],{},"Because each line is a JSON object, tests assert on parsed structure instead of substrings — far less brittle than matching formatted text. Capture ",[14,1669,541],{},", parse each line, and check the fields:",[115,1672,1674],{"className":211,"code":1673,"language":213,"meta":120,"style":120},"import json\nimport logging\n\ndef test_json_formatter_emits_fields(caplog):\n    handler = logging.StreamHandler()\n    handler.setFormatter(JsonFormatter())\n    record = logging.makeLogRecord({\n        \"name\": \"mycli\", \"levelname\": \"INFO\", \"levelno\": logging.INFO,\n        \"msg\": \"done\", \"request_id\": \"r-1\",\n    })\n    line = handler.format(record)\n    obj = json.loads(line)\n    assert obj[\"event\"] == \"done\"\n    assert obj[\"level\"] == \"INFO\"\n    assert obj[\"request_id\"] == \"r-1\"\n",[14,1675,1676,1682,1688,1692,1702,1711,1716,1726,1756,1777,1782,1792,1802,1819,1834],{"__ignoreMap":120},[124,1677,1678,1680],{"class":126,"line":127},[124,1679,235],{"class":148},[124,1681,251],{"class":229},[124,1683,1684,1686],{"class":126,"line":155},[124,1685,235],{"class":148},[124,1687,259],{"class":229},[124,1689,1690],{"class":126,"line":176},[124,1691,266],{"emptyLinePlaceholder":265},[124,1693,1694,1696,1699],{"class":126,"line":254},[124,1695,817],{"class":148},[124,1697,1698],{"class":130}," test_json_formatter_emits_fields",[124,1700,1701],{"class":229},"(caplog):\n",[124,1703,1704,1706,1708],{"class":126,"line":262},[124,1705,1597],{"class":229},[124,1707,353],{"class":148},[124,1709,1710],{"class":229}," logging.StreamHandler()\n",[124,1712,1713],{"class":126,"line":269},[124,1714,1715],{"class":229},"    handler.setFormatter(JsonFormatter())\n",[124,1717,1718,1721,1723],{"class":126,"line":275},[124,1719,1720],{"class":229},"    record ",[124,1722,353],{"class":148},[124,1724,1725],{"class":229}," logging.makeLogRecord({\n",[124,1727,1728,1731,1733,1735,1737,1740,1742,1744,1746,1749,1752,1754],{"class":126,"line":296},[124,1729,1730],{"class":134},"        \"name\"",[124,1732,651],{"class":229},[124,1734,625],{"class":134},[124,1736,102],{"class":229},[124,1738,1739],{"class":134},"\"levelname\"",[124,1741,651],{"class":229},[124,1743,696],{"class":134},[124,1745,102],{"class":229},[124,1747,1748],{"class":134},"\"levelno\"",[124,1750,1751],{"class":229},": logging.",[124,1753,598],{"class":141},[124,1755,996],{"class":229},[124,1757,1758,1761,1763,1766,1768,1770,1772,1775],{"class":126,"line":301},[124,1759,1760],{"class":134},"        \"msg\"",[124,1762,651],{"class":229},[124,1764,1765],{"class":134},"\"done\"",[124,1767,102],{"class":229},[124,1769,648],{"class":134},[124,1771,651],{"class":229},[124,1773,1774],{"class":134},"\"r-1\"",[124,1776,996],{"class":229},[124,1778,1779],{"class":126,"line":306},[124,1780,1781],{"class":229},"    })\n",[124,1783,1784,1787,1789],{"class":126,"line":329},[124,1785,1786],{"class":229},"    line ",[124,1788,353],{"class":148},[124,1790,1791],{"class":229}," handler.format(record)\n",[124,1793,1794,1797,1799],{"class":126,"line":347},[124,1795,1796],{"class":229},"    obj ",[124,1798,353],{"class":148},[124,1800,1801],{"class":229}," json.loads(line)\n",[124,1803,1804,1807,1810,1812,1814,1816],{"class":126,"line":359},[124,1805,1806],{"class":148},"    assert",[124,1808,1809],{"class":229}," obj[",[124,1811,710],{"class":134},[124,1813,509],{"class":229},[124,1815,1334],{"class":148},[124,1817,1818],{"class":134}," \"done\"\n",[124,1820,1821,1823,1825,1827,1829,1831],{"class":126,"line":368},[124,1822,1806],{"class":148},[124,1824,1809],{"class":229},[124,1826,691],{"class":134},[124,1828,509],{"class":229},[124,1830,1334],{"class":148},[124,1832,1833],{"class":134}," \"INFO\"\n",[124,1835,1836,1838,1840,1842,1844,1846],{"class":126,"line":383},[124,1837,1806],{"class":148},[124,1839,1809],{"class":229},[124,1841,648],{"class":134},[124,1843,509],{"class":229},[124,1845,1334],{"class":148},[124,1847,1848],{"class":134}," \"r-1\"\n",[10,1850,1851,1852,1854,1855,1858,1859,1196,1861,1863],{},"For ",[14,1853,28],{},", use ",[14,1856,1857],{},"structlog.testing.capture_logs()"," to collect emitted event dicts directly, so you can assert on ",[14,1860,1028],{},[14,1862,105],{}," without touching the renderer at all.",[31,1865,1867],{"id":1866},"production-notes","Production notes",[36,1869,1870,1880,1893,1903,1916],{},[39,1871,1872,1875,1876,1879],{},[774,1873,1874],{},"JSON Lines, not a JSON array."," Emit one object per line and never wrap the whole stream in ",[14,1877,1878],{},"[...]",". Streaming consumers read line by line and can't wait for a closing bracket.",[39,1881,1882,1885,1886,1888,1889,1892],{},[774,1883,1884],{},"Still log to stderr."," JSON is a rendering choice, not a routing one. Keep diagnostics on ",[14,1887,541],{}," so ",[14,1890,1891],{},"stdout"," stays clean for a tool's actual result.",[39,1894,1895,1898,1899,1902],{},[774,1896,1897],{},"Pin structlog."," APIs shift between majors; pin ",[14,1900,1901],{},"structlog>=24"," and test after upgrades. The stdlib formatter has no such risk if you want zero moving parts.",[39,1904,1905,1908,1909,1196,1912,1915],{},[774,1906,1907],{},"Don't log secrets."," Structured fields make logs easy to index — and easy to leak. Add a processor that drops or masks keys like ",[14,1910,1911],{},"password",[14,1913,1914],{},"token"," before the renderer.",[39,1917,1918,1921,1922,1925],{},[774,1919,1920],{},"UTC timestamps."," Log in UTC ISO-8601 (",[14,1923,1924],{},"TimeStamper(utc=True)","); mixed local zones make cross-machine correlation miserable.",[31,1927,1929],{"id":1928},"frequently-asked-questions","Frequently asked questions",[1931,1932,1934],"h3",{"id":1933},"should-the-json-go-to-stdout-so-i-can-pipe-it","Should the JSON go to stdout so I can pipe it?",[10,1936,1937],{},"No — send it to stderr like every other log line, and keep stdout for the command's result. Log collectors read both streams, so nothing is lost, whereas mixing JSON log lines into stdout means anyone piping the tool has to filter your logs out of their data.",[1931,1939,1941],{"id":1940},"do-i-need-structlog-or-is-the-standard-library-enough","Do I need structlog, or is the standard library enough?",[10,1943,1944],{},"The standard library is enough for one-line-per-event JSON: a custom Formatter whose format() builds a dict and returns json.dumps of it is about fifteen lines and has no dependencies. structlog earns its place when you want bound context, processor pipelines and the ability to render the same events as friendly text locally and JSON in production without writing two code paths.",[1931,1946,1948],{"id":1947},"how-do-i-keep-a-runs-log-lines-findable","How do I keep a run's log lines findable?",[10,1950,1951],{},"Bind a run identifier once at the boundary and let every line inherit it. A uuid4 generated in the callback, bound onto the logger, turns a query like run_id:\"…\" into the complete story of one invocation — which matters most for a CLI, since a hundred users may be running the same tool into the same aggregator at once.",[1931,1953,1955],{"id":1954},"should-json-logging-be-the-default","Should JSON logging be the default?",[10,1957,1958],{},"Not for an interactive tool. Default to human-readable output and switch to JSON on an explicit --log-format json flag or an environment variable that your deployment sets. Defaulting to JSON punishes the person at a terminal to please a log aggregator that could just as easily be told which flag to pass.",[1931,1960,1962],{"id":1961},"how-do-i-test-json-log-output","How do I test JSON log output?",[10,1964,1965],{},"Capture stderr, split it into lines and json.loads each one, then assert on the fields rather than on the whole string. That gives you tests that survive a formatting change, and it also fails loudly if a stray print() ever sneaks a non-JSON line into the stream — which is the failure mode that breaks a log pipeline in production.",[31,1967,1969],{"id":1968},"related","Related",[36,1971,1972,1978,1984,1990],{},[39,1973,1974,1975],{},"Up: ",[23,1976,1977],{"href":1431},"Structured Logging for CLI Apps",[39,1979,1974,1980],{},[23,1981,1983],{"href":1982},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing for Python CLIs",[39,1985,1986,1987],{},"Sideways: ",[23,1988,1989],{"href":1416},"Adding verbose and quiet logging flags",[39,1991,1986,1992],{},[23,1993,1995],{"href":1994},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","Error handling and exit codes",[1997,1998,1999],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":120,"searchDepth":155,"depth":155,"links":2001},[2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2018],{"id":33,"depth":155,"text":34},{"id":94,"depth":155,"text":95},{"id":197,"depth":155,"text":198},{"id":766,"depth":155,"text":767},{"id":1076,"depth":155,"text":1077},{"id":1214,"depth":155,"text":1215},{"id":1435,"depth":155,"text":1436},{"id":1663,"depth":155,"text":1664},{"id":1866,"depth":155,"text":1867},{"id":1928,"depth":155,"text":1929,"children":2012},[2013,2014,2015,2016,2017],{"id":1933,"depth":176,"text":1934},{"id":1940,"depth":176,"text":1941},{"id":1947,"depth":176,"text":1948},{"id":1954,"depth":176,"text":1955},{"id":1961,"depth":176,"text":1962},{"id":1968,"depth":155,"text":1969},"2026-07-05","Emit machine-readable JSON logs from a Python CLI with structlog or a custom formatter, add context fields, and keep human-friendly output for terminals.","advanced",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis",{"title":5,"description":2020},"advanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002Findex",[318,672,2029,2030],"cli","structure","N9wFlxIHpGjur-P_5ybmmyibDTQyxGgjeMqV7Rk4bGI",[2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2083,2086,2089,2092,2095,2098,2101,2104,2106,2107,2110,2113,2116,2119,2122,2125,2128,2131,2134,2137,2140,2143,2146,2149,2152,2155,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2194,2197,2200,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2233,2236,2239,2242,2245,2248,2251,2254,2257,2260,2263,2266,2269,2272,2275],{"path":2034,"title":2035},"\u002Fabout","About Python CLI Toolcraft",{"path":2037,"title":2038},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2040,"title":2041},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2043,"title":2044},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2046,"title":2047},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2049,"title":2050},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":2052,"title":2053},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2055,"title":2056},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2058,"title":2059},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2061,"title":2062},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2064,"title":2065},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2067,"title":2068},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2070,"title":2071},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2073,"title":2074},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2076,"title":2077},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2079,"title":2080},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":2082,"title":1983},"\u002Fadvanced-input-parsing-user-experience",{"path":2084,"title":2085},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2087,"title":2088},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2090,"title":2091},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2093,"title":2094},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2096,"title":2097},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2099,"title":2100},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2102,"title":2103},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2105,"title":1977},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps",{"path":2025,"title":5},{"path":2108,"title":2109},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2111,"title":2112},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2114,"title":2115},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2117,"title":2118},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2120,"title":2121},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2123,"title":2124},"\u002F","Python CLI Toolcraft",{"path":2126,"title":2127},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2129,"title":2130},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2132,"title":2133},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2135,"title":2136},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2138,"title":2139},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2141,"title":2142},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2144,"title":2145},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2147,"title":2148},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2150,"title":2151},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2153,"title":2154},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2156,"title":2157},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2159,"title":2160},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2162,"title":2163},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2165,"title":2166},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2168,"title":2169},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2171,"title":2172},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2174,"title":2175},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2177,"title":2178},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2180,"title":2181},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2183,"title":2184},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2186,"title":2187},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2189,"title":2190},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2192,"title":2193},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2195,"title":2196},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2198,"title":2199},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2201,"title":2202},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2204,"title":2205},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2207,"title":2208},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2210,"title":2211},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2213,"title":2214},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2216,"title":2217},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2219,"title":2220},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2222,"title":2223},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2225,"title":2226},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2228,"title":2229},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2231,"title":2232},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2234,"title":2235},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2237,"title":2238},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2240,"title":2241},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2243,"title":2244},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2246,"title":2247},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2249,"title":2250},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2252,"title":2253},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2255,"title":2256},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2258,"title":2259},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2261,"title":2262},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2264,"title":2265},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2267,"title":2268},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2270,"title":2271},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2273,"title":2274},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2276,"title":2277},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690030]