[{"data":1,"prerenderedAt":1989},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts":3},{"id":4,"title":5,"body":6,"description":1978,"extension":1979,"meta":1980,"navigation":271,"path":1985,"seo":1986,"stem":1987,"__hash__":1988},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002Findex.md","Handle Errors and Logging in Unattended PyQGIS Scripts",{"type":7,"value":8,"toc":1964},"minimark",[9,13,17,26,203,208,233,237,379,386,390,395,550,567,571,578,878,900,904,907,1074,1079,1181,1185,1188,1330,1355,1359,1362,1616,1621,1628,1701,1705,1712,1804,1808,1877,1881,1884,1888,1894,1900,1914,1920,1926,1930,1960],[10,11,5],"h1",{"id":12},"handle-errors-and-logging-in-unattended-pyqgis-scripts",[14,15,16],"p",{},"The worst outcome for a scheduled job is not a crash. A crash gets noticed. The worst outcome is a run that completes in four seconds instead of four minutes, writes an empty layer, exits zero, and keeps doing that every night for six weeks until someone opens the map and asks where the data went.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","Headless QGIS and Server Automation",". It covers capturing the messages QGIS writes to its own log, feeding Processing's progress into Python logging, asserting that a result is plausible before declaring success, and using exit codes a scheduler can act on.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,101,105,109,112,116,120,123,127,131,139,143,148,152,158,162,166,170,174,178,184,187,190,194,200],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 286","img","Four message streams — Python logging, the QGIS message log, Processing feedback and provider errors — converging into one handler that writes a single log file and sets the exit code","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Four sources of truth, one log",[40,41,42],"desc",{},"Python's own logging, the QGIS message log, Processing feedback objects and provider error messages each carry information about a run. Left alone they go to four different places, three of which are invisible on a server. A single handler collects all four into one structured log file, and the outcome sets the process exit code.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","286","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"logArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Three of these are invisible unless you connect them",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"16","52","212","46","#edf8e9","#15803d","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"122","72","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","logging.getLogger()",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"90","text-anchor:middle;font-size:10px;font-family:sans-serif","visible by default",[44,97],{"x":78,"y":98,"width":80,"height":81,"rx":58,"fill":99,"stroke":100,"style":84},"108","#fdf2e2","#b45309",[68,102,104],{"x":87,"y":103,"style":89,"fill":100,"textAnchor":74},"128","QgsMessageLog",[68,106,108],{"x":87,"y":107,"style":94,"fill":66,"textAnchor":74},"146","goes to a GUI panel that is not there",[44,110],{"x":78,"y":111,"width":80,"height":81,"rx":58,"fill":99,"stroke":100,"style":84},"164",[68,113,115],{"x":87,"y":114,"style":89,"fill":100,"textAnchor":74},"184","QgsProcessingFeedback",[68,117,119],{"x":87,"y":118,"style":94,"fill":66,"textAnchor":74},"202","discarded unless you pass one",[44,121],{"x":78,"y":122,"width":80,"height":81,"rx":58,"fill":99,"stroke":100,"style":84},"220",[68,124,126],{"x":87,"y":125,"style":89,"fill":100,"textAnchor":74},"240","provider.error()",[68,128,130],{"x":87,"y":129,"style":94,"fill":66,"textAnchor":74},"258","only if you read it",[44,132],{"x":133,"y":98,"width":134,"height":135,"rx":58,"fill":136,"stroke":137,"style":138},"292","176","102","#eef7f4","#0f766e","stroke-width:2.5",[68,140,142],{"x":70,"y":107,"style":141,"fill":137,"textAnchor":74},"text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","one handler",[68,144,147],{"x":70,"y":145,"style":146,"fill":66,"textAnchor":74},"168","text-anchor:middle;font-size:11px;font-family:sans-serif","timestamp · level",[68,149,151],{"x":70,"y":150,"style":146,"fill":66,"textAnchor":74},"187","counts · duration",[44,153],{"x":154,"y":155,"width":80,"height":156,"rx":58,"fill":157,"stroke":73,"style":84},"532","80","66","#fffdf7",[68,159,161],{"x":160,"y":98,"style":141,"fill":73,"textAnchor":74},"638","nightly.log",[68,163,165],{"x":160,"y":164,"style":146,"fill":66,"textAnchor":74},"130","one line per unit of work",[44,167],{"x":154,"y":145,"width":80,"height":156,"rx":58,"fill":168,"stroke":169,"style":138},"#eff3ff","#2563eb",[68,171,173],{"x":160,"y":172,"style":141,"fill":169,"textAnchor":74},"196","exit code",[68,175,177],{"x":160,"y":176,"style":146,"fill":66,"textAnchor":74},"218","0 good · 1 error · 2 implausible",[179,180],"line",{"x1":181,"y1":182,"x2":48,"y2":164,"stroke":66,"style":183},"228","75","stroke-width:2;marker-end:url(#logArrow)",[179,185],{"x1":181,"y1":186,"x2":48,"y2":107,"stroke":66,"style":183},"131",[179,188],{"x1":181,"y1":150,"x2":48,"y2":189,"stroke":66,"style":183},"172",[179,191],{"x1":181,"y1":192,"x2":48,"y2":193,"stroke":66,"style":183},"243","190",[179,195],{"x1":196,"y1":197,"x2":198,"y2":199,"stroke":66,"style":183},"468","140","526","118",[179,201],{"x1":196,"y1":134,"x2":198,"y2":202,"stroke":66,"style":183},"194",[204,205,207],"h2",{"id":206},"prerequisites","Prerequisites",[209,210,211,218,221],"ul",{},[212,213,214,215,217],"li",{},"A headless PyQGIS script — see ",[21,216,24],{"href":23}," for the initialisation pattern.",[212,219,220],{},"A writable log location, and a scheduler that watches exit codes.",[212,222,223,224,228,229,232],{},"Familiarity with Python's ",[225,226,227],"code",{},"logging"," module; nothing beyond ",[225,230,231],{},"basicConfig"," and a logger per module is needed.",[204,234,236],{"id":235},"set-up-logging-before-anything-else","Set up logging before anything else",[238,239,244],"pre",{"className":240,"code":241,"language":242,"meta":243,"style":243},"language-python shiki shiki-themes github-dark","import logging\nimport sys\n\nlogging.basicConfig(\n    level=logging.INFO,\n    format=\"%(asctime)s %(levelname)-7s %(name)s %(message)s\",\n    datefmt=\"%Y-%m-%dT%H:%M:%S\",\n    stream=sys.stdout,\n)\nlog = logging.getLogger(\"nightly_export\")\n","python","",[225,245,246,258,266,273,279,299,327,346,357,363],{"__ignoreMap":243},[247,248,250,254],"span",{"class":179,"line":249},1,[247,251,253],{"class":252},"snl16","import",[247,255,257],{"class":256},"s95oV"," logging\n",[247,259,261,263],{"class":179,"line":260},2,[247,262,253],{"class":252},[247,264,265],{"class":256}," sys\n",[247,267,269],{"class":179,"line":268},3,[247,270,272],{"emptyLinePlaceholder":271},true,"\n",[247,274,276],{"class":179,"line":275},4,[247,277,278],{"class":256},"logging.basicConfig(\n",[247,280,282,286,289,292,296],{"class":179,"line":281},5,[247,283,285],{"class":284},"s9osk","    level",[247,287,288],{"class":252},"=",[247,290,291],{"class":256},"logging.",[247,293,295],{"class":294},"sDLfK","INFO",[247,297,298],{"class":256},",\n",[247,300,302,305,307,311,314,317,320,323,325],{"class":179,"line":301},6,[247,303,304],{"class":284},"    format",[247,306,288],{"class":252},[247,308,310],{"class":309},"sU2Wk","\"",[247,312,313],{"class":294},"%(asctime)s",[247,315,316],{"class":294}," %(levelname)-7s",[247,318,319],{"class":294}," %(name)s",[247,321,322],{"class":294}," %(message)s",[247,324,310],{"class":309},[247,326,298],{"class":256},[247,328,330,333,335,338,341,344],{"class":179,"line":329},7,[247,331,332],{"class":284},"    datefmt",[247,334,288],{"class":252},[247,336,337],{"class":309},"\"%Y-%m-",[247,339,340],{"class":294},"%d",[247,342,343],{"class":309},"T%H:%M:%S\"",[247,345,298],{"class":256},[247,347,349,352,354],{"class":179,"line":348},8,[247,350,351],{"class":284},"    stream",[247,353,288],{"class":252},[247,355,356],{"class":256},"sys.stdout,\n",[247,358,360],{"class":179,"line":359},9,[247,361,362],{"class":256},")\n",[247,364,366,369,371,374,377],{"class":179,"line":365},10,[247,367,368],{"class":256},"log ",[247,370,288],{"class":252},[247,372,373],{"class":256}," logging.getLogger(",[247,375,376],{"class":309},"\"nightly_export\"",[247,378,362],{"class":256},[14,380,381,385],{},[382,383,384],"strong",{},"Breakdown:"," Writing to standard output rather than opening a file lets the wrapper script or the container runtime decide where the log goes — the same script then works under cron, systemd and Docker without change. A fixed-width level column makes the log greppable; ISO timestamps make it sortable and unambiguous across time zones. Configure this before importing anything from QGIS so that early provider warnings are already captured.",[204,387,389],{"id":388},"capture-what-qgis-itself-is-saying","Capture what QGIS itself is saying",[14,391,392,394],{},[225,393,104],{}," is where providers, algorithms and plugins report problems. On the desktop it fills a panel; on a server it goes nowhere unless you connect to it.",[238,396,398],{"className":240,"code":397,"language":242,"meta":243,"style":243},"from qgis.core import QgsApplication, Qgis\n\nLEVELS = {\n    Qgis.Info: logging.INFO,\n    Qgis.Warning: logging.WARNING,\n    Qgis.Critical: logging.ERROR,\n    Qgis.Success: logging.INFO,\n}\n\ndef forward_qgis_messages(message, tag, level):\n    logging.getLogger(f\"qgis.{tag or 'general'}\").log(\n        LEVELS.get(level, logging.INFO), message.strip()\n    )\n\nQgsApplication.messageLog().messageReceived.connect(forward_qgis_messages)\n",[225,399,400,413,417,428,437,447,457,466,471,475,487,519,533,539,544],{"__ignoreMap":243},[247,401,402,405,408,410],{"class":179,"line":249},[247,403,404],{"class":252},"from",[247,406,407],{"class":256}," qgis.core ",[247,409,253],{"class":252},[247,411,412],{"class":256}," QgsApplication, Qgis\n",[247,414,415],{"class":179,"line":260},[247,416,272],{"emptyLinePlaceholder":271},[247,418,419,422,425],{"class":179,"line":268},[247,420,421],{"class":294},"LEVELS",[247,423,424],{"class":252}," =",[247,426,427],{"class":256}," {\n",[247,429,430,433,435],{"class":179,"line":275},[247,431,432],{"class":256},"    Qgis.Info: logging.",[247,434,295],{"class":294},[247,436,298],{"class":256},[247,438,439,442,445],{"class":179,"line":281},[247,440,441],{"class":256},"    Qgis.Warning: logging.",[247,443,444],{"class":294},"WARNING",[247,446,298],{"class":256},[247,448,449,452,455],{"class":179,"line":301},[247,450,451],{"class":256},"    Qgis.Critical: logging.",[247,453,454],{"class":294},"ERROR",[247,456,298],{"class":256},[247,458,459,462,464],{"class":179,"line":329},[247,460,461],{"class":256},"    Qgis.Success: logging.",[247,463,295],{"class":294},[247,465,298],{"class":256},[247,467,468],{"class":179,"line":348},[247,469,470],{"class":256},"}\n",[247,472,473],{"class":179,"line":359},[247,474,272],{"emptyLinePlaceholder":271},[247,476,477,480,484],{"class":179,"line":365},[247,478,479],{"class":252},"def",[247,481,483],{"class":482},"svObZ"," forward_qgis_messages",[247,485,486],{"class":256},"(message, tag, level):\n",[247,488,490,493,496,499,502,505,508,511,514,516],{"class":179,"line":489},11,[247,491,492],{"class":256},"    logging.getLogger(",[247,494,495],{"class":252},"f",[247,497,498],{"class":309},"\"qgis.",[247,500,501],{"class":294},"{",[247,503,504],{"class":256},"tag ",[247,506,507],{"class":252},"or",[247,509,510],{"class":309}," 'general'",[247,512,513],{"class":294},"}",[247,515,310],{"class":309},[247,517,518],{"class":256},").log(\n",[247,520,522,525,528,530],{"class":179,"line":521},12,[247,523,524],{"class":294},"        LEVELS",[247,526,527],{"class":256},".get(level, logging.",[247,529,295],{"class":294},[247,531,532],{"class":256},"), message.strip()\n",[247,534,536],{"class":179,"line":535},13,[247,537,538],{"class":256},"    )\n",[247,540,542],{"class":179,"line":541},14,[247,543,272],{"emptyLinePlaceholder":271},[247,545,547],{"class":179,"line":546},15,[247,548,549],{"class":256},"QgsApplication.messageLog().messageReceived.connect(forward_qgis_messages)\n",[14,551,552,554,555,558,559,562,563,566],{},[382,553,384],{}," The signal fires for every message QGIS logs, with the originating tag — ",[225,556,557],{},"Processing",", ",[225,560,561],{},"PostGIS",", a plugin's name — which becomes the logger name so a noisy component can be filtered or silenced independently. Connecting this immediately after ",[225,564,565],{},"initQgis()"," catches provider errors that would otherwise be invisible, and it is often the single change that explains a mysterious failure: the answer was already being written, just not anywhere you could see.",[204,568,570],{"id":569},"route-processing-feedback-into-the-same-log","Route Processing feedback into the same log",[14,572,573,574,577],{},"Every ",[225,575,576],{},"processing.run()"," accepts a feedback object. Supply one and the algorithm's progress and warnings become log lines rather than nothing.",[238,579,581],{"className":240,"code":580,"language":242,"meta":243,"style":243},"from qgis.core import QgsProcessingFeedback\n\nclass LoggingFeedback(QgsProcessingFeedback):\n    def __init__(self, logger):\n        super().__init__(False)\n        self._log = logger\n        self._last = -1\n\n    def pushInfo(self, info):\n        self._log.info(info)\n\n    def reportError(self, error, fatalError=False):\n        self._log.error(error)\n\n    def setProgress(self, progress):\n        step = int(progress) \u002F\u002F 10 * 10\n        if step != self._last:\n            self._last = step\n            self._log.info(\"progress %d%%\", step)\n\nresult = processing.run(\n    \"native:buffer\",\n    {\"INPUT\": source, \"DISTANCE\": 25, \"OUTPUT\": destination},\n    feedback=LoggingFeedback(log),\n)\n",[225,582,583,594,598,614,625,643,656,671,675,685,692,696,712,719,723,733,759,777,790,809,814,825,833,862,873],{"__ignoreMap":243},[247,584,585,587,589,591],{"class":179,"line":249},[247,586,404],{"class":252},[247,588,407],{"class":256},[247,590,253],{"class":252},[247,592,593],{"class":256}," QgsProcessingFeedback\n",[247,595,596],{"class":179,"line":260},[247,597,272],{"emptyLinePlaceholder":271},[247,599,600,603,606,609,611],{"class":179,"line":268},[247,601,602],{"class":252},"class",[247,604,605],{"class":482}," LoggingFeedback",[247,607,608],{"class":256},"(",[247,610,115],{"class":482},[247,612,613],{"class":256},"):\n",[247,615,616,619,622],{"class":179,"line":275},[247,617,618],{"class":252},"    def",[247,620,621],{"class":294}," __init__",[247,623,624],{"class":256},"(self, logger):\n",[247,626,627,630,633,636,638,641],{"class":179,"line":281},[247,628,629],{"class":294},"        super",[247,631,632],{"class":256},"().",[247,634,635],{"class":294},"__init__",[247,637,608],{"class":256},[247,639,640],{"class":294},"False",[247,642,362],{"class":256},[247,644,645,648,651,653],{"class":179,"line":301},[247,646,647],{"class":294},"        self",[247,649,650],{"class":256},"._log ",[247,652,288],{"class":252},[247,654,655],{"class":256}," logger\n",[247,657,658,660,663,665,668],{"class":179,"line":329},[247,659,647],{"class":294},[247,661,662],{"class":256},"._last ",[247,664,288],{"class":252},[247,666,667],{"class":252}," -",[247,669,670],{"class":294},"1\n",[247,672,673],{"class":179,"line":348},[247,674,272],{"emptyLinePlaceholder":271},[247,676,677,679,682],{"class":179,"line":359},[247,678,618],{"class":252},[247,680,681],{"class":482}," pushInfo",[247,683,684],{"class":256},"(self, info):\n",[247,686,687,689],{"class":179,"line":365},[247,688,647],{"class":294},[247,690,691],{"class":256},"._log.info(info)\n",[247,693,694],{"class":179,"line":489},[247,695,272],{"emptyLinePlaceholder":271},[247,697,698,700,703,706,708,710],{"class":179,"line":521},[247,699,618],{"class":252},[247,701,702],{"class":482}," reportError",[247,704,705],{"class":256},"(self, error, fatalError",[247,707,288],{"class":252},[247,709,640],{"class":294},[247,711,613],{"class":256},[247,713,714,716],{"class":179,"line":535},[247,715,647],{"class":294},[247,717,718],{"class":256},"._log.error(error)\n",[247,720,721],{"class":179,"line":541},[247,722,272],{"emptyLinePlaceholder":271},[247,724,725,727,730],{"class":179,"line":546},[247,726,618],{"class":252},[247,728,729],{"class":482}," setProgress",[247,731,732],{"class":256},"(self, progress):\n",[247,734,736,739,741,744,747,750,753,756],{"class":179,"line":735},16,[247,737,738],{"class":256},"        step ",[247,740,288],{"class":252},[247,742,743],{"class":294}," int",[247,745,746],{"class":256},"(progress) ",[247,748,749],{"class":252},"\u002F\u002F",[247,751,752],{"class":294}," 10",[247,754,755],{"class":252}," *",[247,757,758],{"class":294}," 10\n",[247,760,762,765,768,771,774],{"class":179,"line":761},17,[247,763,764],{"class":252},"        if",[247,766,767],{"class":256}," step ",[247,769,770],{"class":252},"!=",[247,772,773],{"class":294}," self",[247,775,776],{"class":256},"._last:\n",[247,778,780,783,785,787],{"class":179,"line":779},18,[247,781,782],{"class":294},"            self",[247,784,662],{"class":256},[247,786,288],{"class":252},[247,788,789],{"class":256}," step\n",[247,791,793,795,798,801,804,806],{"class":179,"line":792},19,[247,794,782],{"class":294},[247,796,797],{"class":256},"._log.info(",[247,799,800],{"class":309},"\"progress ",[247,802,803],{"class":294},"%d%%",[247,805,310],{"class":309},[247,807,808],{"class":256},", step)\n",[247,810,812],{"class":179,"line":811},20,[247,813,272],{"emptyLinePlaceholder":271},[247,815,817,820,822],{"class":179,"line":816},21,[247,818,819],{"class":256},"result ",[247,821,288],{"class":252},[247,823,824],{"class":256}," processing.run(\n",[247,826,828,831],{"class":179,"line":827},22,[247,829,830],{"class":309},"    \"native:buffer\"",[247,832,298],{"class":256},[247,834,836,839,842,845,848,851,854,856,859],{"class":179,"line":835},23,[247,837,838],{"class":256},"    {",[247,840,841],{"class":309},"\"INPUT\"",[247,843,844],{"class":256},": source, ",[247,846,847],{"class":309},"\"DISTANCE\"",[247,849,850],{"class":256},": ",[247,852,853],{"class":294},"25",[247,855,558],{"class":256},[247,857,858],{"class":309},"\"OUTPUT\"",[247,860,861],{"class":256},": destination},\n",[247,863,865,868,870],{"class":179,"line":864},24,[247,866,867],{"class":284},"    feedback",[247,869,288],{"class":252},[247,871,872],{"class":256},"LoggingFeedback(log),\n",[247,874,876],{"class":179,"line":875},25,[247,877,362],{"class":256},[14,879,880,882,883,886,887,890,891,894,895,899],{},[382,881,384],{}," ",[225,884,885],{},"super().__init__(False)"," disables the default console progress bar, which is meaningless in a log file. Overriding ",[225,888,889],{},"setProgress"," to log only when the tens digit changes turns a hundred updates per algorithm into ten lines — enough to see that a long run is advancing, few enough to read. ",[225,892,893],{},"reportError"," fires for recoverable problems as well as fatal ones, so a run can succeed while still having logged errors worth investigating. Passing the same feedback object through a chain of algorithms gives one continuous narrative, which pairs well with the patterns in ",[21,896,898],{"href":897},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS",".",[204,901,903],{"id":902},"assert-that-the-result-is-plausible","Assert that the result is plausible",[14,905,906],{},"Success is not \"the code ran\". Success is \"the output looks like the thing it should look like\". Check it explicitly.",[238,908,910],{"className":240,"code":909,"language":242,"meta":243,"style":243},"EXPECTED_MINIMUM = 1000\n\nwritten = QgsVectorLayer(destination, \"output\", \"ogr\")\nif not written.isValid():\n    log.error(\"output layer did not open: %s\", destination)\n    sys.exit(1)\n\ncount = written.featureCount()\nlog.info(\"wrote %d features to %s\", count, destination)\n\nif count \u003C EXPECTED_MINIMUM:\n    log.error(\"only %d features — expected at least %d, refusing to publish\",\n              count, EXPECTED_MINIMUM)\n    sys.exit(2)\n",[225,911,912,922,926,946,957,973,983,987,997,1017,1021,1037,1056,1065],{"__ignoreMap":243},[247,913,914,917,919],{"class":179,"line":249},[247,915,916],{"class":294},"EXPECTED_MINIMUM",[247,918,424],{"class":252},[247,920,921],{"class":294}," 1000\n",[247,923,924],{"class":179,"line":260},[247,925,272],{"emptyLinePlaceholder":271},[247,927,928,931,933,936,939,941,944],{"class":179,"line":268},[247,929,930],{"class":256},"written ",[247,932,288],{"class":252},[247,934,935],{"class":256}," QgsVectorLayer(destination, ",[247,937,938],{"class":309},"\"output\"",[247,940,558],{"class":256},[247,942,943],{"class":309},"\"ogr\"",[247,945,362],{"class":256},[247,947,948,951,954],{"class":179,"line":275},[247,949,950],{"class":252},"if",[247,952,953],{"class":252}," not",[247,955,956],{"class":256}," written.isValid():\n",[247,958,959,962,965,968,970],{"class":179,"line":281},[247,960,961],{"class":256},"    log.error(",[247,963,964],{"class":309},"\"output layer did not open: ",[247,966,967],{"class":294},"%s",[247,969,310],{"class":309},[247,971,972],{"class":256},", destination)\n",[247,974,975,978,981],{"class":179,"line":301},[247,976,977],{"class":256},"    sys.exit(",[247,979,980],{"class":294},"1",[247,982,362],{"class":256},[247,984,985],{"class":179,"line":329},[247,986,272],{"emptyLinePlaceholder":271},[247,988,989,992,994],{"class":179,"line":348},[247,990,991],{"class":256},"count ",[247,993,288],{"class":252},[247,995,996],{"class":256}," written.featureCount()\n",[247,998,999,1002,1005,1007,1010,1012,1014],{"class":179,"line":359},[247,1000,1001],{"class":256},"log.info(",[247,1003,1004],{"class":309},"\"wrote ",[247,1006,340],{"class":294},[247,1008,1009],{"class":309}," features to ",[247,1011,967],{"class":294},[247,1013,310],{"class":309},[247,1015,1016],{"class":256},", count, destination)\n",[247,1018,1019],{"class":179,"line":365},[247,1020,272],{"emptyLinePlaceholder":271},[247,1022,1023,1025,1028,1031,1034],{"class":179,"line":489},[247,1024,950],{"class":252},[247,1026,1027],{"class":256}," count ",[247,1029,1030],{"class":252},"\u003C",[247,1032,1033],{"class":294}," EXPECTED_MINIMUM",[247,1035,1036],{"class":256},":\n",[247,1038,1039,1041,1044,1046,1049,1051,1054],{"class":179,"line":521},[247,1040,961],{"class":256},[247,1042,1043],{"class":309},"\"only ",[247,1045,340],{"class":294},[247,1047,1048],{"class":309}," features — expected at least ",[247,1050,340],{"class":294},[247,1052,1053],{"class":309},", refusing to publish\"",[247,1055,298],{"class":256},[247,1057,1058,1061,1063],{"class":179,"line":535},[247,1059,1060],{"class":256},"              count, ",[247,1062,916],{"class":294},[247,1064,362],{"class":256},[247,1066,1067,1069,1072],{"class":179,"line":541},[247,1068,977],{"class":256},[247,1070,1071],{"class":294},"2",[247,1073,362],{"class":256},[14,1075,1076,1078],{},[382,1077,384],{}," Re-opening the written file rather than trusting the writer's return value catches the case where the write succeeded and the file is unusable. The threshold check is the part that catches silent upstream breakage — a source feed that returns an empty response produces a technically successful run with zero features, and only a plausibility assertion turns that into an alert. Distinct exit codes let the scheduler treat \"it crashed\" and \"it ran but the numbers are wrong\" differently.",[14,1080,1081],{},[29,1082,1085,1088,1091,1094,1102,1105,1109,1112,1115,1118,1122,1125,1128,1132,1135,1138,1142,1145,1148,1152,1156,1158,1161,1164,1168,1172,1175,1179],{"viewBox":1083,"role":32,"ariaLabel":1084,"xmlns":34},"0 0 760 262","Decision flow from a completed run through validity, count and duration checks to one of three exit codes",[36,1086,1087],{},"From finished run to exit code",[40,1089,1090],{},"A finished run is checked three times. If the output does not open, the job exits with code one. If it opens but the feature count is below the expected minimum, it exits with code two and does not publish. If both pass, the counts and duration are logged and the job exits zero.",[44,1092],{"x":46,"y":46,"width":47,"height":1093,"fill":49},"262",[51,1095,1096],{},[54,1097,1099],{"id":1098,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"exitArrow",[63,1100],{"d":65,"fill":1101},"#59645f",[68,1103,1104],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Exit codes are the only thing a scheduler reads",[44,1106],{"x":1107,"y":1108,"width":197,"height":79,"rx":58,"fill":157,"stroke":73,"style":138},"20","104",[68,1110,1111],{"x":93,"y":103,"style":141,"fill":73,"textAnchor":74},"run finished",[68,1113,1114],{"x":93,"y":107,"style":94,"fill":66,"textAnchor":74},"no exception raised",[44,1116],{"x":172,"y":1108,"width":1117,"height":79,"rx":58,"fill":168,"stroke":169,"style":84},"150",[68,1119,1121],{"x":1120,"y":103,"style":89,"fill":169,"textAnchor":74},"271","output opens?",[68,1123,1124],{"x":1120,"y":107,"style":94,"fill":66,"textAnchor":74},"isValid()",[44,1126],{"x":1127,"y":1108,"width":1117,"height":79,"rx":58,"fill":168,"stroke":169,"style":84},"382",[68,1129,1131],{"x":1130,"y":103,"style":89,"fill":169,"textAnchor":74},"457","count plausible?",[68,1133,1134],{"x":1130,"y":107,"style":94,"fill":66,"textAnchor":74},"above the floor",[44,1136],{"x":1137,"y":1108,"width":189,"height":79,"rx":58,"fill":82,"stroke":83,"style":138},"568",[68,1139,1141],{"x":1140,"y":103,"style":141,"fill":83,"textAnchor":74},"654","exit 0",[68,1143,1144],{"x":1140,"y":107,"style":94,"fill":66,"textAnchor":74},"log counts and duration",[44,1146],{"x":172,"y":202,"width":1117,"height":81,"rx":58,"fill":99,"stroke":1147,"style":84},"#b91c1c",[68,1149,1151],{"x":1120,"y":1150,"style":89,"fill":1147,"textAnchor":74},"214","exit 1",[68,1153,1155],{"x":1120,"y":1154,"style":94,"fill":66,"textAnchor":74},"231","broken output",[44,1157],{"x":1127,"y":202,"width":1117,"height":81,"rx":58,"fill":99,"stroke":100,"style":84},[68,1159,1160],{"x":1130,"y":1150,"style":89,"fill":100,"textAnchor":74},"exit 2",[68,1162,1163],{"x":1130,"y":1154,"style":94,"fill":66,"textAnchor":74},"suspicious result",[179,1165],{"x1":1166,"y1":164,"x2":193,"y2":164,"stroke":1101,"style":1167},"160","stroke-width:2;marker-end:url(#exitArrow)",[179,1169],{"x1":1170,"y1":164,"x2":1171,"y2":164,"stroke":1101,"style":1167},"346","376",[179,1173],{"x1":154,"y1":164,"x2":1174,"y2":164,"stroke":1101,"style":1167},"562",[179,1176],{"x1":1120,"y1":1177,"x2":1120,"y2":1178,"stroke":1147,"style":1167},"156","188",[179,1180],{"x1":1130,"y1":1177,"x2":1130,"y2":1178,"stroke":100,"style":1167},[204,1182,1184],{"id":1183},"fail-once-at-the-top","Fail once, at the top",[14,1186,1187],{},"Wrap the whole job so that no exception can escape unlogged, and so the traceback lands in the same place as everything else.",[238,1189,1191],{"className":240,"code":1190,"language":242,"meta":243,"style":243},"def main():\n    started = time.monotonic()\n    log.info(\"starting nightly export\")\n    do_the_work()\n    log.info(\"finished in %.1fs\", time.monotonic() - started)\n    return 0\n\nif __name__ == \"__main__\":\n    try:\n        sys.exit(main())\n    except Exception:\n        log.exception(\"nightly export failed\")\n        sys.exit(1)\n    finally:\n        app.exitQgis()\n",[225,1192,1193,1203,1213,1223,1228,1250,1258,1262,1277,1284,1289,1299,1309,1318,1325],{"__ignoreMap":243},[247,1194,1195,1197,1200],{"class":179,"line":249},[247,1196,479],{"class":252},[247,1198,1199],{"class":482}," main",[247,1201,1202],{"class":256},"():\n",[247,1204,1205,1208,1210],{"class":179,"line":260},[247,1206,1207],{"class":256},"    started ",[247,1209,288],{"class":252},[247,1211,1212],{"class":256}," time.monotonic()\n",[247,1214,1215,1218,1221],{"class":179,"line":268},[247,1216,1217],{"class":256},"    log.info(",[247,1219,1220],{"class":309},"\"starting nightly export\"",[247,1222,362],{"class":256},[247,1224,1225],{"class":179,"line":275},[247,1226,1227],{"class":256},"    do_the_work()\n",[247,1229,1230,1232,1235,1238,1241,1244,1247],{"class":179,"line":281},[247,1231,1217],{"class":256},[247,1233,1234],{"class":309},"\"finished in ",[247,1236,1237],{"class":294},"%.1f",[247,1239,1240],{"class":309},"s\"",[247,1242,1243],{"class":256},", time.monotonic() ",[247,1245,1246],{"class":252},"-",[247,1248,1249],{"class":256}," started)\n",[247,1251,1252,1255],{"class":179,"line":301},[247,1253,1254],{"class":252},"    return",[247,1256,1257],{"class":294}," 0\n",[247,1259,1260],{"class":179,"line":329},[247,1261,272],{"emptyLinePlaceholder":271},[247,1263,1264,1266,1269,1272,1275],{"class":179,"line":348},[247,1265,950],{"class":252},[247,1267,1268],{"class":294}," __name__",[247,1270,1271],{"class":252}," ==",[247,1273,1274],{"class":309}," \"__main__\"",[247,1276,1036],{"class":256},[247,1278,1279,1282],{"class":179,"line":359},[247,1280,1281],{"class":252},"    try",[247,1283,1036],{"class":256},[247,1285,1286],{"class":179,"line":365},[247,1287,1288],{"class":256},"        sys.exit(main())\n",[247,1290,1291,1294,1297],{"class":179,"line":489},[247,1292,1293],{"class":252},"    except",[247,1295,1296],{"class":294}," Exception",[247,1298,1036],{"class":256},[247,1300,1301,1304,1307],{"class":179,"line":521},[247,1302,1303],{"class":256},"        log.exception(",[247,1305,1306],{"class":309},"\"nightly export failed\"",[247,1308,362],{"class":256},[247,1310,1311,1314,1316],{"class":179,"line":535},[247,1312,1313],{"class":256},"        sys.exit(",[247,1315,980],{"class":294},[247,1317,362],{"class":256},[247,1319,1320,1323],{"class":179,"line":541},[247,1321,1322],{"class":252},"    finally",[247,1324,1036],{"class":256},[247,1326,1327],{"class":179,"line":546},[247,1328,1329],{"class":256},"        app.exitQgis()\n",[14,1331,1332,882,1334,1337,1338,1341,1342,1345,1346,1349,1350,1354],{},[382,1333,384],{},[225,1335,1336],{},"log.exception()"," records the message together with the full traceback at ERROR level — the single most useful call in this file, and the reason not to catch exceptions deeper in the code where the context is thinner. ",[225,1339,1340],{},"time.monotonic()"," rather than ",[225,1343,1344],{},"time.time()"," is immune to clock adjustments, so a duration is never negative after an NTP correction. The ",[225,1347,1348],{},"finally"," block releases QGIS whether the run succeeded or not, which prevents a stuck process from holding database connections. Logging both a start and a finish line means an absent finish is itself a signal, and the ",[21,1351,1353],{"href":1352},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fschedule-pyqgis-scripts-with-cron\u002F","locking pattern in the cron guide"," keeps a stuck run from being joined by another.",[204,1356,1358],{"id":1357},"retry-the-transient-fail-the-permanent","Retry the transient, fail the permanent",[14,1360,1361],{},"Some failures are worth trying again; most are not. Retrying a permanent error wastes time and buries the real message under three identical tracebacks, while not retrying a transient one turns a two-second network blip into a missed night.",[238,1363,1365],{"className":240,"code":1364,"language":242,"meta":243,"style":243},"import time\n\nTRANSIENT = (\"could not connect\", \"timeout expired\", \"connection reset\", \"temporarily unavailable\")\n\ndef is_transient(message):\n    lowered = message.lower()\n    return any(marker in lowered for marker in TRANSIENT)\n\ndef with_retry(operation, attempts=3, delay=5):\n    for attempt in range(1, attempts + 1):\n        try:\n            return operation()\n        except Exception as exc:\n            if attempt == attempts or not is_transient(str(exc)):\n                raise\n            log.warning(\"attempt %d failed (%s) — retrying in %ds\", attempt, exc, delay * attempt)\n            time.sleep(delay * attempt)\n",[225,1366,1367,1374,1378,1408,1412,1422,1432,1461,1465,1489,1517,1524,1532,1545,1571,1576,1607],{"__ignoreMap":243},[247,1368,1369,1371],{"class":179,"line":249},[247,1370,253],{"class":252},[247,1372,1373],{"class":256}," time\n",[247,1375,1376],{"class":179,"line":260},[247,1377,272],{"emptyLinePlaceholder":271},[247,1379,1380,1383,1385,1388,1391,1393,1396,1398,1401,1403,1406],{"class":179,"line":268},[247,1381,1382],{"class":294},"TRANSIENT",[247,1384,424],{"class":252},[247,1386,1387],{"class":256}," (",[247,1389,1390],{"class":309},"\"could not connect\"",[247,1392,558],{"class":256},[247,1394,1395],{"class":309},"\"timeout expired\"",[247,1397,558],{"class":256},[247,1399,1400],{"class":309},"\"connection reset\"",[247,1402,558],{"class":256},[247,1404,1405],{"class":309},"\"temporarily unavailable\"",[247,1407,362],{"class":256},[247,1409,1410],{"class":179,"line":275},[247,1411,272],{"emptyLinePlaceholder":271},[247,1413,1414,1416,1419],{"class":179,"line":281},[247,1415,479],{"class":252},[247,1417,1418],{"class":482}," is_transient",[247,1420,1421],{"class":256},"(message):\n",[247,1423,1424,1427,1429],{"class":179,"line":301},[247,1425,1426],{"class":256},"    lowered ",[247,1428,288],{"class":252},[247,1430,1431],{"class":256}," message.lower()\n",[247,1433,1434,1436,1439,1442,1445,1448,1451,1454,1456,1459],{"class":179,"line":329},[247,1435,1254],{"class":252},[247,1437,1438],{"class":294}," any",[247,1440,1441],{"class":256},"(marker ",[247,1443,1444],{"class":252},"in",[247,1446,1447],{"class":256}," lowered ",[247,1449,1450],{"class":252},"for",[247,1452,1453],{"class":256}," marker ",[247,1455,1444],{"class":252},[247,1457,1458],{"class":294}," TRANSIENT",[247,1460,362],{"class":256},[247,1462,1463],{"class":179,"line":348},[247,1464,272],{"emptyLinePlaceholder":271},[247,1466,1467,1469,1472,1475,1477,1480,1483,1485,1487],{"class":179,"line":359},[247,1468,479],{"class":252},[247,1470,1471],{"class":482}," with_retry",[247,1473,1474],{"class":256},"(operation, attempts",[247,1476,288],{"class":252},[247,1478,1479],{"class":294},"3",[247,1481,1482],{"class":256},", delay",[247,1484,288],{"class":252},[247,1486,59],{"class":294},[247,1488,613],{"class":256},[247,1490,1491,1494,1497,1499,1502,1504,1506,1509,1512,1515],{"class":179,"line":365},[247,1492,1493],{"class":252},"    for",[247,1495,1496],{"class":256}," attempt ",[247,1498,1444],{"class":252},[247,1500,1501],{"class":294}," range",[247,1503,608],{"class":256},[247,1505,980],{"class":294},[247,1507,1508],{"class":256},", attempts ",[247,1510,1511],{"class":252},"+",[247,1513,1514],{"class":294}," 1",[247,1516,613],{"class":256},[247,1518,1519,1522],{"class":179,"line":489},[247,1520,1521],{"class":252},"        try",[247,1523,1036],{"class":256},[247,1525,1526,1529],{"class":179,"line":521},[247,1527,1528],{"class":252},"            return",[247,1530,1531],{"class":256}," operation()\n",[247,1533,1534,1537,1539,1542],{"class":179,"line":535},[247,1535,1536],{"class":252},"        except",[247,1538,1296],{"class":294},[247,1540,1541],{"class":252}," as",[247,1543,1544],{"class":256}," exc:\n",[247,1546,1547,1550,1552,1555,1558,1560,1562,1565,1568],{"class":179,"line":541},[247,1548,1549],{"class":252},"            if",[247,1551,1496],{"class":256},[247,1553,1554],{"class":252},"==",[247,1556,1557],{"class":256}," attempts ",[247,1559,507],{"class":252},[247,1561,953],{"class":252},[247,1563,1564],{"class":256}," is_transient(",[247,1566,1567],{"class":294},"str",[247,1569,1570],{"class":256},"(exc)):\n",[247,1572,1573],{"class":179,"line":546},[247,1574,1575],{"class":252},"                raise\n",[247,1577,1578,1581,1584,1586,1589,1591,1594,1596,1598,1601,1604],{"class":179,"line":735},[247,1579,1580],{"class":256},"            log.warning(",[247,1582,1583],{"class":309},"\"attempt ",[247,1585,340],{"class":294},[247,1587,1588],{"class":309}," failed (",[247,1590,967],{"class":294},[247,1592,1593],{"class":309},") — retrying in ",[247,1595,340],{"class":294},[247,1597,1240],{"class":309},[247,1599,1600],{"class":256},", attempt, exc, delay ",[247,1602,1603],{"class":252},"*",[247,1605,1606],{"class":256}," attempt)\n",[247,1608,1609,1612,1614],{"class":179,"line":761},[247,1610,1611],{"class":256},"            time.sleep(delay ",[247,1613,1603],{"class":252},[247,1615,1606],{"class":256},[14,1617,1618,1620],{},[382,1619,384],{}," The classification is deliberately conservative: anything not recognised as transient is re-raised immediately, because a missing file, a syntax error in an expression or a permissions problem will fail identically on every attempt. Logging each failed attempt at warning level leaves a trail showing that the eventual success took three tries — information that disappears if the retry is silent, and that is exactly what tells you a link is degrading before it fails outright. The growing delay gives a restarting service time to come back.",[14,1622,1623,1624,899],{},"Retry only operations that are safe to repeat. A read is; a write that may already have committed is not, which is why the database guidance pairs retries with a unique constraint and a transaction rather than with hope — see ",[21,1625,1627],{"href":1626},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fappend-features-to-postgis-table-pyqgis\u002F","Append Features to a PostGIS Table in PyQGIS",[14,1629,1630],{},[29,1631,1634,1637,1640,1643,1650,1653,1658,1662,1666,1670,1673,1677,1680,1684,1687,1690,1694,1698],{"viewBox":1632,"role":32,"ariaLabel":1633,"xmlns":34},"0 0 760 250","Decision flow classifying a failure as transient or permanent, retrying transient failures with growing delays and failing immediately on permanent ones",[36,1635,1636],{},"Which failures deserve a second attempt",[40,1638,1639],{},"A failure is classified by its message. Connection timeouts and resets are treated as transient and retried with growing delays, succeeding on a later attempt or eventually failing with a clear history. A missing file, a permission error or a bad expression is permanent, so the job fails immediately rather than repeating the same error three times.",[44,1641],{"x":46,"y":46,"width":47,"height":1642,"fill":49},"250",[51,1644,1645],{},[54,1646,1648],{"id":1647,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"retryArrow",[63,1649],{"d":65,"fill":1101},[68,1651,1652],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Three identical tracebacks are not resilience",[44,1654],{"x":1655,"y":1656,"width":114,"height":1657,"rx":58,"fill":157,"stroke":73,"style":138},"288","48","44",[68,1659,1661],{"x":70,"y":1660,"style":141,"fill":73,"textAnchor":74},"76","the operation failed",[44,1663],{"x":1107,"y":103,"width":1664,"height":1665,"rx":58,"fill":82,"stroke":83,"style":138},"336","92",[68,1667,1669],{"x":1178,"y":1668,"style":141,"fill":83,"textAnchor":74},"152","transient — retry",[68,1671,1672],{"x":1178,"y":134,"style":146,"fill":66,"textAnchor":74},"connection reset · timeout expired",[68,1674,1676],{"x":1178,"y":1675,"style":146,"fill":66,"textAnchor":74},"198","wait 5 s, then 10 s, logging each attempt",[44,1678],{"x":1679,"y":103,"width":1664,"height":1665,"rx":58,"fill":99,"stroke":1147,"style":138},"404",[68,1681,1683],{"x":1682,"y":1668,"style":141,"fill":1147,"textAnchor":74},"572","permanent — raise now",[68,1685,1686],{"x":1682,"y":134,"style":146,"fill":66,"textAnchor":74},"missing file · permission denied",[68,1688,1689],{"x":1682,"y":1675,"style":146,"fill":66,"textAnchor":74},"a second attempt fails identically",[179,1691],{"x1":1692,"y1":1665,"x2":122,"y2":87,"stroke":1101,"style":1693},"330","stroke-width:2;marker-end:url(#retryArrow)",[179,1695],{"x1":1696,"y1":1665,"x2":1697,"y2":87,"stroke":1101,"style":1693},"430","540",[68,1699,1700],{"x":70,"y":125,"style":146,"fill":1101,"textAnchor":74},"Retry reads freely; retry a write only when a unique constraint makes the repeat safe",[204,1702,1704],{"id":1703},"qgis-version-compatibility","QGIS version compatibility",[14,1706,1707,1708,1711],{},"The examples target ",[382,1709,1710],{},"QGIS 3.34 LTR"," (Python 3.12).",[1713,1714,1715,1731],"table",{},[1716,1717,1718],"thead",{},[1719,1720,1721,1725,1728],"tr",{},[1722,1723,1724],"th",{},"QGIS version",[1722,1726,1727],{},"Python",[1722,1729,1730],{},"Notes",[1732,1733,1734,1753,1773,1784],"tbody",{},[1719,1735,1736,1740,1743],{},[1737,1738,1739],"td",{},"3.22 LTR",[1737,1741,1742],{},"3.9",[1737,1744,1745,1748,1749,1752],{},[225,1746,1747],{},"messageReceived"," carries the same three arguments; ",[225,1750,1751],{},"Qgis.MessageLevel"," members are named identically.",[1719,1754,1755,1758,1760],{},[1737,1756,1757],{},"3.28 LTR",[1737,1759,1742],{},[1737,1761,1762,1764,1765,1768,1769,1772],{},[225,1763,115],{}," gains ",[225,1766,1767],{},"pushFormattedMessage","; overriding ",[225,1770,1771],{},"pushInfo"," still captures everything.",[1719,1774,1775,1778,1781],{},[1737,1776,1777],{},"3.34 LTR",[1737,1779,1780],{},"3.12",[1737,1782,1783],{},"Baseline for this page.",[1719,1785,1786,1789,1791],{},[1737,1787,1788],{},"3.40 \u002F 3.44",[1737,1790,1780],{},[1737,1792,1793,1795,1796,1799,1800,1803],{},[225,1794,893],{},"'s ",[225,1797,1798],{},"fatalError"," argument is unchanged; new ",[225,1801,1802],{},"pushWarning"," may be overridden for finer control.",[204,1805,1807],{"id":1806},"troubleshooting","Troubleshooting",[209,1809,1810,1820,1831,1845,1851,1867],{},[212,1811,1812,882,1815,1817,1818,899],{},[382,1813,1814],{},"The log contains nothing from QGIS.",[225,1816,1747],{}," was connected after the failing operation, or the connection was made to a different application instance. Connect immediately after ",[225,1819,565],{},[212,1821,1822,882,1825,1828,1829,899],{},[382,1823,1824],{},"The traceback is missing from the log.",[225,1826,1827],{},"log.error(str(exc))"," was used instead of ",[225,1830,1336],{},[212,1832,1833,1836,1837,1840,1841,1844],{},[382,1834,1835],{},"Log lines appear only when the job ends."," Python is buffering standard output. Set ",[225,1838,1839],{},"PYTHONUNBUFFERED=1",", or pass ",[225,1842,1843],{},"flush=True"," if you are printing.",[212,1846,1847,1850],{},[382,1848,1849],{},"Progress floods the log."," The feedback object logs every update. Throttle it as shown, or drop progress below INFO level.",[212,1852,1853,882,1856,1858,1859,1862,1863,1866],{},[382,1854,1855],{},"The job exits zero after an algorithm failed.",[225,1857,576],{}," raises ",[225,1860,1861],{},"QgsProcessingException"," on failure, but only if you do not catch and ignore it; a bare ",[225,1864,1865],{},"except: pass"," anywhere in the chain hides it.",[212,1868,1869,1872,1873,1876],{},[382,1870,1871],{},"Timestamps disagree between machines."," Log in UTC by setting ",[225,1874,1875],{},"logging.Formatter.converter = time.gmtime",", and let the reader localise.",[204,1878,1880],{"id":1879},"conclusion","Conclusion",[14,1882,1883],{},"An unattended script needs four things wired together: Python logging configured to standard output, the QGIS message log forwarded into it, a feedback object passed to every algorithm, and a plausibility check on the result that can fail the run with a distinct exit code. Add a top-level handler that logs the traceback and always releases QGIS, and a silent failure stops being possible.",[204,1885,1887],{"id":1886},"frequently-asked-questions","Frequently Asked Questions",[14,1889,1890,1893],{},[382,1891,1892],{},"Should I log to a file directly from Python?","\nPrefer standard output and let the runner redirect it. That keeps one script working under cron, systemd and Docker, and it makes log rotation somebody else's job.",[14,1895,1896,1899],{},[382,1897,1898],{},"How verbose should a nightly job be?","\nOne line per stage plus counts, and progress at ten-percent intervals for anything slow. The test is whether two consecutive nights can be diffed usefully.",[14,1901,1902,1905,1906,1909,1910,1913],{},[382,1903,1904],{},"Can I send QGIS messages to syslog or the journal?","\nYes — attach a ",[225,1907,1908],{},"SysLogHandler"," or ",[225,1911,1912],{},"JournalHandler"," to the root logger. The forwarding shown above puts QGIS's messages into the same logging tree, so they inherit any handler you add.",[14,1915,1916,1919],{},[382,1917,1918],{},"Why not just use print()?","\nPrint has no levels, no timestamps and no way to be filtered or redirected per component. The moment you want to silence one noisy provider, you need logging.",[14,1921,1922,1925],{},[382,1923,1924],{},"How do I test the failure path?","\nPoint the job at an empty source and confirm it exits 2, and make it raise deliberately once to confirm the traceback lands in the log. A failure path that has never run is not a failure path.",[204,1927,1929],{"id":1928},"related","Related",[209,1931,1932,1937,1942,1948,1954],{},[212,1933,1934,1936],{},[21,1935,24],{"href":23}," — the guide this recipe belongs to",[212,1938,1939],{},[21,1940,1941],{"href":1352},"Schedule PyQGIS Scripts with cron",[212,1943,1944],{},[21,1945,1947],{"href":1946},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Flog-messages-to-qgis-message-log-pyqgis\u002F","Log Messages to the QGIS Message Log in PyQGIS",[212,1949,1950],{},[21,1951,1953],{"href":1952},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002F","Debugging PyQGIS Scripts",[212,1955,1956],{},[21,1957,1959],{"href":1958},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","Batch Processing with PyQGIS",[1961,1962,1963],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":243,"searchDepth":260,"depth":260,"links":1965},[1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977],{"id":206,"depth":260,"text":207},{"id":235,"depth":260,"text":236},{"id":388,"depth":260,"text":389},{"id":569,"depth":260,"text":570},{"id":902,"depth":260,"text":903},{"id":1183,"depth":260,"text":1184},{"id":1357,"depth":260,"text":1358},{"id":1703,"depth":260,"text":1704},{"id":1806,"depth":260,"text":1807},{"id":1879,"depth":260,"text":1880},{"id":1886,"depth":260,"text":1887},{"id":1928,"depth":260,"text":1929},"Turn a silent PyQGIS job into one that reports — capture QgsMessageLog output, route Processing feedback into Python logging, validate results, and exit with a code that means something.","md",{"slug":1981,"type":1982,"breadcrumb":1983,"datePublished":1984,"dateModified":1984},"handle-errors-and-logging-in-unattended-scripts","article","Errors & Logging","2026-08-10","\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts",{"title":5,"description":1978},"pyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002Findex","3NN_SbOaCNlgzEddAO0cdONDrGSSQVpt6wxvbG3CrHs",1786401337551]