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