zenml-nightly 0.84.0.dev20250729__py3-none-any.whl → 0.84.1.dev20250731__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. zenml/VERSION +1 -1
  2. zenml/client.py +6 -1
  3. zenml/constants.py +28 -2
  4. zenml/integrations/gcp/__init__.py +4 -1
  5. zenml/logging/__init__.py +0 -11
  6. zenml/logging/step_logging.py +255 -149
  7. zenml/models/__init__.py +4 -0
  8. zenml/models/v2/core/service_account.py +87 -1
  9. zenml/models/v2/core/user.py +20 -6
  10. zenml/models/v2/misc/external_user.py +4 -1
  11. zenml/orchestrators/step_launcher.py +2 -4
  12. zenml/zen_server/auth.py +138 -27
  13. zenml/zen_server/dashboard/assets/{404-B5cfnwZ1.js → 404-CMbwR10f.js} +1 -1
  14. zenml/zen_server/dashboard/assets/{@radix-C_LirfyT.js → @radix-B1sy0Lhr.js} +1 -1
  15. zenml/zen_server/dashboard/assets/{@react-router-BSsrkPOd.js → @react-router-CHjLNlgw.js} +1 -1
  16. zenml/zen_server/dashboard/assets/{@reactflow-D9hglKLF.js → @reactflow-gbyyU3kq.js} +2 -2
  17. zenml/zen_server/dashboard/assets/{@tanstack-C0SeHZng.js → @tanstack-Dwlisomv.js} +1 -1
  18. zenml/zen_server/dashboard/assets/AlertDialogDropdownItem-BpLj419i.js +1 -0
  19. zenml/zen_server/dashboard/assets/ButtonGroup-DVhzbQxV.js +1 -0
  20. zenml/zen_server/dashboard/assets/{CodeSnippet-D8iBqOVv.js → CodeSnippet-B7Wd4R8u.js} +1 -1
  21. zenml/zen_server/dashboard/assets/{CollapsibleCard-D0-pQi1n.js → CollapsibleCard-DSZzbjx5.js} +1 -1
  22. zenml/zen_server/dashboard/assets/{ComponentBadge-mw2Ja_ON.js → ComponentBadge-MVT08kBV.js} +1 -1
  23. zenml/zen_server/dashboard/assets/{ComponentIcon-BXgpt-jw.js → ComponentIcon-C9AY8usJ.js} +1 -1
  24. zenml/zen_server/dashboard/assets/DeleteAlertDialog-CzhTVQaC.js +1 -0
  25. zenml/zen_server/dashboard/assets/{DialogItem-DeME0oSt.js → DialogItem-DD_7v3UY.js} +1 -1
  26. zenml/zen_server/dashboard/assets/{DisplayDate-v3KW7oez.js → DisplayDate-57lUNS79.js} +1 -1
  27. zenml/zen_server/dashboard/assets/{EmptyState-DG0m-CGv.js → EmptyState-BMA34iVR.js} +1 -1
  28. zenml/zen_server/dashboard/assets/{Error-DcVLcrLs.js → Error-Cakgjexo.js} +1 -1
  29. zenml/zen_server/dashboard/assets/{ExecutionStatus-C4tlFnlh.js → ExecutionStatus-CQbWovhV.js} +1 -1
  30. zenml/zen_server/dashboard/assets/{Helpbox-C-RGHz3S.js → Helpbox-7FZpmsHB.js} +1 -1
  31. zenml/zen_server/dashboard/assets/{Infobox-DFCWPbMb.js → Infobox-DnduZjNU.js} +1 -1
  32. zenml/zen_server/dashboard/assets/{LeftSideMenu-Czev0KCA.js → LeftSideMenu-D5UEs4vB.js} +1 -1
  33. zenml/zen_server/dashboard/assets/{Lock-CRP5J_su.js → Lock-u8WOoTTd.js} +1 -1
  34. zenml/zen_server/dashboard/assets/NestedCollapsible-0yviIfaA.js +1 -0
  35. zenml/zen_server/dashboard/assets/{NumberBox-CoQjQYDJ.js → NumberBox-C2Pju4PR.js} +1 -1
  36. zenml/zen_server/dashboard/assets/{Pagination-CcDD5yHh.js → Pagination-C1sE5Nzn.js} +1 -1
  37. zenml/zen_server/dashboard/assets/{Partials-DlMzfKgs.js → Partials-DosysQMU.js} +1 -1
  38. zenml/zen_server/dashboard/assets/{PasswordChecker-BZwoeQIm.js → PasswordChecker-DOPLfvg7.js} +1 -1
  39. zenml/zen_server/dashboard/assets/{ProCta-CU2ycJDo.js → ProCta-CUkpV3PJ.js} +1 -1
  40. zenml/zen_server/dashboard/assets/{ProviderIcon-BMAn9Jld.js → ProviderIcon-aQjTuTRX.js} +1 -1
  41. zenml/zen_server/dashboard/assets/{ProviderRadio-D_q9tE3G.js → ProviderRadio-C6pLVNxC.js} +1 -1
  42. zenml/zen_server/dashboard/assets/RunsBody-CZAiSxYK.js +1 -0
  43. zenml/zen_server/dashboard/assets/{SearchField-D_0-uAPj.js → SearchField-Byv1PtST.js} +1 -1
  44. zenml/zen_server/dashboard/assets/{SecretTooltip-BcWMKb9f.js → SecretTooltip-CErfhVgF.js} +1 -1
  45. zenml/zen_server/dashboard/assets/{SetPassword-CaKVSqAL.js → SetPassword-C4OH-cn1.js} +1 -1
  46. zenml/zen_server/dashboard/assets/{SheetHeader-7vwlsY_i.js → SheetHeader-Bb3v9rha.js} +1 -1
  47. zenml/zen_server/dashboard/assets/StackComponentList-D85oab98.js +1 -0
  48. zenml/zen_server/dashboard/assets/StackList-DSAjbVb5.js +1 -0
  49. zenml/zen_server/dashboard/assets/Tabs-2uwqXsdL.js +1 -0
  50. zenml/zen_server/dashboard/assets/Tick-BQ7_xDBl.js +1 -0
  51. zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-Da5RndbV.js → UpdatePasswordSchemas-DNkYgzN6.js} +1 -1
  52. zenml/zen_server/dashboard/assets/{Wizard-8aJzxUjb.js → Wizard-2FIi8JNY.js} +1 -1
  53. zenml/zen_server/dashboard/assets/WizardFooter-DQEnlDmZ.js +1 -0
  54. zenml/zen_server/dashboard/assets/{all-pipeline-runs-query-gorNNEaT.js → all-pipeline-runs-query-CdvWtiAY.js} +1 -1
  55. zenml/zen_server/dashboard/assets/{arrow-left-hcj2H8HY.js → arrow-left-BFB2mgJo.js} +1 -1
  56. zenml/zen_server/dashboard/assets/{bar-chart-square-check-9siI9icm.js → bar-chart-square-check-_m-oYysN.js} +1 -1
  57. zenml/zen_server/dashboard/assets/{bulk-delete-B5RTlnD_.js → bulk-delete-wdObjfps.js} +2 -2
  58. zenml/zen_server/dashboard/assets/{check-D1bHMJkL.js → check-C9Nye5lR.js} +1 -1
  59. zenml/zen_server/dashboard/assets/{check-circle-mnEgPhPF.js → check-circle-CG6XAdk-.js} +1 -1
  60. zenml/zen_server/dashboard/assets/{chevron-down-Z3nUe-0U.js → chevron-down-Bl7WwQAL.js} +1 -1
  61. zenml/zen_server/dashboard/assets/{chevron-right-double-CbRQKN4Q.js → chevron-right-double-CbMG1dOM.js} +1 -1
  62. zenml/zen_server/dashboard/assets/{clock-BMjHXT3f.js → clock-BnPvxp3D.js} +1 -1
  63. zenml/zen_server/dashboard/assets/{code-browser-DftoiCIg.js → code-browser-CzWvJ4S6.js} +1 -1
  64. zenml/zen_server/dashboard/assets/configuration-form-BbcNBQTO.js +1 -0
  65. zenml/zen_server/dashboard/assets/constants-DfvsDtcH.js +1 -0
  66. zenml/zen_server/dashboard/assets/{create-stack-BruqH_6X.js → create-stack-D4Sf7QpF.js} +1 -1
  67. zenml/zen_server/dashboard/assets/{credit-card-CH1BHrXY.js → credit-card-DfBA6dBb.js} +1 -1
  68. zenml/zen_server/dashboard/assets/{dataflow-2-qHjWt7zp.js → dataflow-2-BLkaTH0Q.js} +1 -1
  69. zenml/zen_server/dashboard/assets/{delete-run-ibBtciMR.js → delete-run-XSre8ru-.js} +1 -1
  70. zenml/zen_server/dashboard/assets/{expand-full-CD4fFvM-.js → expand-full-C8p_0jQv.js} +1 -1
  71. zenml/zen_server/dashboard/assets/{eye-CLNgIh_K.js → eye-BXcQevds.js} +1 -1
  72. zenml/zen_server/dashboard/assets/{file-text-CltVhgwZ.js → file-text-TdPXNd7s.js} +1 -1
  73. zenml/zen_server/dashboard/assets/form-C1jJgRy1.js +1 -0
  74. zenml/zen_server/dashboard/assets/{form-schemas-B9XgTS1V.js → form-schemas-BjWDRvGd.js} +1 -1
  75. zenml/zen_server/dashboard/assets/{help-B0CvBhCm.js → help-CRPfbPTO.js} +1 -1
  76. zenml/zen_server/dashboard/assets/{icon-hDriJUXY.js → icon--Tp6obFZ.js} +1 -1
  77. zenml/zen_server/dashboard/assets/{index-mA8kL088.js → index-B4ZeIaPd.js} +1 -1
  78. zenml/zen_server/dashboard/assets/{index-dCcVgFNl.js → index-BYHxFvoG.js} +1 -1
  79. zenml/zen_server/dashboard/assets/{index-BQWlHo1Y.js → index-BYzR7YrM.js} +1 -1
  80. zenml/zen_server/dashboard/assets/{index-BacoJBEQ.js → index-CO6UN3UX.js} +11 -11
  81. zenml/zen_server/dashboard/assets/{index-B7CRNU8l.js → index-d_QrPL-8.js} +1 -1
  82. zenml/zen_server/dashboard/assets/{index-BRhKF2z-.js → index-g7wOyc86.js} +1 -1
  83. zenml/zen_server/dashboard/assets/index-n_sn2ITN.css +1 -0
  84. zenml/zen_server/dashboard/assets/{index.es-DcVFDpJU.js → index.es-CZMSrR6z.js} +1 -1
  85. zenml/zen_server/dashboard/assets/{index.esm-COnaHLSh.js → index.esm-SBF9nn_P.js} +1 -1
  86. zenml/zen_server/dashboard/assets/{info-CyMih3vQ.js → info-CI2szPUG.js} +1 -1
  87. zenml/zen_server/dashboard/assets/{key-icon-HOx2gazv.js → key-icon-BiV171aI.js} +1 -1
  88. zenml/zen_server/dashboard/assets/layout-Cyc-V16c.js +1 -0
  89. zenml/zen_server/dashboard/assets/{layout-C5dgIReC.js → layout-GIiNvS10.js} +1 -1
  90. zenml/zen_server/dashboard/assets/{login-mutation-CidpsqyH.js → login-mutation-CDgTlQBD.js} +1 -1
  91. zenml/zen_server/dashboard/assets/{logs-DoLoTEfj.js → logs-lNLbZqHL.js} +1 -1
  92. zenml/zen_server/dashboard/assets/{mail-C160gvB0.js → mail-DBJRSzyu.js} +1 -1
  93. zenml/zen_server/dashboard/assets/{message-chat-square-DLz6XmPS.js → message-chat-square-C3CRCQvD.js} +1 -1
  94. zenml/zen_server/dashboard/assets/{package-BhYXGPxF.js → package-pR4bbXJp.js} +1 -1
  95. zenml/zen_server/dashboard/assets/page-1DXG7hp8.js +1 -0
  96. zenml/zen_server/dashboard/assets/{page-BCRXJXC9.js → page-43SWdz4k.js} +1 -1
  97. zenml/zen_server/dashboard/assets/{page-YdWnx9MX.js → page-AOVMuHDc.js} +1 -1
  98. zenml/zen_server/dashboard/assets/page-B4QBxV0B.js +22 -0
  99. zenml/zen_server/dashboard/assets/{page-CvllZMBF.js → page-B8ict_cR.js} +1 -1
  100. zenml/zen_server/dashboard/assets/page-BDre_qCO.js +1 -0
  101. zenml/zen_server/dashboard/assets/page-BbsvGfyi.js +18 -0
  102. zenml/zen_server/dashboard/assets/page-Bi4I23Hs.js +1 -0
  103. zenml/zen_server/dashboard/assets/page-BnAMyQZb.js +1 -0
  104. zenml/zen_server/dashboard/assets/{page-D6cvOG8w.js → page-BnZEAhNn.js} +1 -1
  105. zenml/zen_server/dashboard/assets/page-Bncp08FW.js +1 -0
  106. zenml/zen_server/dashboard/assets/{page-BTDi81N3.js → page-C4Jnp1qP.js} +1 -1
  107. zenml/zen_server/dashboard/assets/page-C7Z2sDHE.js +1 -0
  108. zenml/zen_server/dashboard/assets/page-C8t9qLdV.js +1 -0
  109. zenml/zen_server/dashboard/assets/{page-BByayrO-.js → page-C9IsnFid.js} +2 -2
  110. zenml/zen_server/dashboard/assets/page-CEHZzQ1Q.js +1 -0
  111. zenml/zen_server/dashboard/assets/page-CGtll3Jk.js +1 -0
  112. zenml/zen_server/dashboard/assets/{page-q41JNDWO.js → page-CKPkbOoE.js} +1 -1
  113. zenml/zen_server/dashboard/assets/{page-DF4FVxxW.js → page-CMvcoaSZ.js} +2 -2
  114. zenml/zen_server/dashboard/assets/page-CVwTAlzd.js +1 -0
  115. zenml/zen_server/dashboard/assets/{page-DSZfclXt.js → page-ClqRvz_V.js} +1 -1
  116. zenml/zen_server/dashboard/assets/{page-7CJ4Wq3O.js → page-CzDjN1wE.js} +1 -1
  117. zenml/zen_server/dashboard/assets/{page-BK59rZvf.js → page-D654xHWd.js} +1 -1
  118. zenml/zen_server/dashboard/assets/{page-DcXrWWWh.js → page-DLrYW-Dn.js} +1 -1
  119. zenml/zen_server/dashboard/assets/page-DOnAInjC.js +1 -0
  120. zenml/zen_server/dashboard/assets/{page-Cc8owYXQ.js → page-DQncGJeH.js} +1 -1
  121. zenml/zen_server/dashboard/assets/page-DQzNmXk3.js +1 -0
  122. zenml/zen_server/dashboard/assets/{page-FQxi1Otg.js → page-DSn1Jcvs.js} +1 -1
  123. zenml/zen_server/dashboard/assets/page-DZ5hcS1o.js +1 -0
  124. zenml/zen_server/dashboard/assets/page-DerIbaqa.js +1 -0
  125. zenml/zen_server/dashboard/assets/{page-8U20Tu_8.js → page-DgOEl3Bc.js} +1 -1
  126. zenml/zen_server/dashboard/assets/{page-DgldL5UB.js → page-DgSu4pTE.js} +2 -2
  127. zenml/zen_server/dashboard/assets/page-DgblJuXC.js +1 -0
  128. zenml/zen_server/dashboard/assets/{page-CY0LPcAJ.js → page-Dpw-xP4q.js} +1 -1
  129. zenml/zen_server/dashboard/assets/page-DsFXol8x.js +1 -0
  130. zenml/zen_server/dashboard/assets/page-Du6bFZTS.js +1 -0
  131. zenml/zen_server/dashboard/assets/{page-CeGBDh1Q.js → page-DznxxpKA.js} +1 -1
  132. zenml/zen_server/dashboard/assets/page-E9Mx9_rn.js +1 -0
  133. zenml/zen_server/dashboard/assets/page-HrebZOAM.js +1 -0
  134. zenml/zen_server/dashboard/assets/{page-BX67x4iL.js → page-O5xnz_nW.js} +1 -1
  135. zenml/zen_server/dashboard/assets/{page-DDWW21kl.js → page-hX7NkNdA.js} +1 -1
  136. zenml/zen_server/dashboard/assets/{persist-BKKcL1Kp.js → persist-B3Hb8HDW.js} +1 -1
  137. zenml/zen_server/dashboard/assets/{persist-DxiyfAax.js → persist-Dkbp9MFP.js} +1 -1
  138. zenml/zen_server/dashboard/assets/{pipeline-BJ8liDnl.js → pipeline-CDJSAXrb.js} +1 -1
  139. zenml/zen_server/dashboard/assets/{plus-cI8zD2xh.js → plus-DOrkJCmy.js} +1 -1
  140. zenml/zen_server/dashboard/assets/primary-role-CjkpP9fL.js +1 -0
  141. zenml/zen_server/dashboard/assets/{react-error-boundary.esm-DoXxY4pT.js → react-error-boundary.esm-DgOYA0wh.js} +1 -1
  142. zenml/zen_server/dashboard/assets/{refresh-3EF2R7ja.js → refresh-C8BXS3Fb.js} +1 -1
  143. zenml/zen_server/dashboard/assets/{resource-tyes-list-B5rkZcbc.js → resource-tyes-list-CugyWUAE.js} +1 -1
  144. zenml/zen_server/dashboard/assets/{resource-type-tooltip-E97WGqfk.js → resource-type-tooltip-V-zdiLKz.js} +1 -1
  145. zenml/zen_server/dashboard/assets/{service-B9aVzfAF.js → service-FjqYTmBw.js} +2 -2
  146. zenml/zen_server/dashboard/assets/{service-connectors-DL2-k_E2.js → service-connectors-nF4-OXB9.js} +1 -1
  147. zenml/zen_server/dashboard/assets/{sharedSchema-DyUO09BR.js → sharedSchema-Br1JPr8d.js} +1 -1
  148. zenml/zen_server/dashboard/assets/{slash-circle-D2Lb2FyR.js → slash-circle-B0xMh8q6.js} +1 -1
  149. zenml/zen_server/dashboard/assets/{stack-detail-query-Bc4QKlWg.js → stack-detail-query-DnyMCdVE.js} +1 -1
  150. zenml/zen_server/dashboard/assets/{terminal-BObrvDlO.js → terminal-C2tAF2XG.js} +1 -1
  151. zenml/zen_server/dashboard/assets/{terminal-square-BObrvDlO.js → terminal-square-C2tAF2XG.js} +1 -1
  152. zenml/zen_server/dashboard/assets/{transform-DFpKTKgF.js → transform-S7omcvTm.js} +1 -1
  153. zenml/zen_server/dashboard/assets/{trash-HKxXWbSG.js → trash-D9LA4VFD.js} +1 -1
  154. zenml/zen_server/dashboard/assets/{update-current-user-mutation-DSyUyHVj.js → update-current-user-mutation-B3Dpv3u6.js} +1 -1
  155. zenml/zen_server/dashboard/assets/{update-server-settings-mutation-CdM-Sdds.js → update-server-settings-mutation-s7tlHN8s.js} +1 -1
  156. zenml/zen_server/dashboard/assets/{zod-DgEcN9jD.js → zod-D48zuELD.js} +1 -1
  157. zenml/zen_server/dashboard/index.html +7 -7
  158. zenml/zen_server/rbac/zenml_cloud_rbac.py +8 -4
  159. zenml/zen_server/routers/service_accounts_endpoints.py +71 -12
  160. zenml/zen_stores/base_zen_store.py +0 -19
  161. zenml/zen_stores/migrations/versions/0.84.1_release.py +23 -0
  162. zenml/zen_stores/migrations/versions/8d4b9ba22c1f_add_user_avatar.py +39 -0
  163. zenml/zen_stores/schemas/user_schemas.py +16 -5
  164. zenml/zen_stores/sql_zen_store.py +5 -1
  165. {zenml_nightly-0.84.0.dev20250729.dist-info → zenml_nightly-0.84.1.dev20250731.dist-info}/METADATA +1 -1
  166. {zenml_nightly-0.84.0.dev20250729.dist-info → zenml_nightly-0.84.1.dev20250731.dist-info}/RECORD +169 -167
  167. zenml/zen_server/dashboard/assets/AlertDialogDropdownItem-druRNuO2.js +0 -1
  168. zenml/zen_server/dashboard/assets/ButtonGroup-SF2DlzHV.js +0 -1
  169. zenml/zen_server/dashboard/assets/DeleteAlertDialog-BbyFVnVI.js +0 -1
  170. zenml/zen_server/dashboard/assets/NestedCollapsible-CN9scBUn.js +0 -1
  171. zenml/zen_server/dashboard/assets/RunsBody-BToytB8e.js +0 -1
  172. zenml/zen_server/dashboard/assets/StackComponentList-s7eSfm8o.js +0 -1
  173. zenml/zen_server/dashboard/assets/StackList-Dt0FrIkM.js +0 -1
  174. zenml/zen_server/dashboard/assets/Tabs-B27AHUfo.js +0 -1
  175. zenml/zen_server/dashboard/assets/Tick-DDeDgTuT.js +0 -1
  176. zenml/zen_server/dashboard/assets/WizardFooter-Bt7_UE14.js +0 -1
  177. zenml/zen_server/dashboard/assets/configuration-form-Yz8m0QIG.js +0 -1
  178. zenml/zen_server/dashboard/assets/constants-DeV48DuZ.js +0 -1
  179. zenml/zen_server/dashboard/assets/form-6aSt3tIl.js +0 -1
  180. zenml/zen_server/dashboard/assets/index-eggipFZS.css +0 -1
  181. zenml/zen_server/dashboard/assets/layout-CFLL6-CM.js +0 -1
  182. zenml/zen_server/dashboard/assets/page-6huxSHEu.js +0 -1
  183. zenml/zen_server/dashboard/assets/page-BMpXak4U.js +0 -1
  184. zenml/zen_server/dashboard/assets/page-Bjmcdg64.js +0 -1
  185. zenml/zen_server/dashboard/assets/page-BsAn8p4m.js +0 -1
  186. zenml/zen_server/dashboard/assets/page-BwjPRuaY.js +0 -1
  187. zenml/zen_server/dashboard/assets/page-CDtSVkNc.js +0 -1
  188. zenml/zen_server/dashboard/assets/page-CEDU0L2T.js +0 -1
  189. zenml/zen_server/dashboard/assets/page-COJK90rG.js +0 -1
  190. zenml/zen_server/dashboard/assets/page-C_XMn4GU.js +0 -1
  191. zenml/zen_server/dashboard/assets/page-Cb3KGsPK.js +0 -22
  192. zenml/zen_server/dashboard/assets/page-CiGOVsj3.js +0 -1
  193. zenml/zen_server/dashboard/assets/page-CmLSFMkW.js +0 -1
  194. zenml/zen_server/dashboard/assets/page-CnfCptXq.js +0 -1
  195. zenml/zen_server/dashboard/assets/page-CxzglV3-.js +0 -1
  196. zenml/zen_server/dashboard/assets/page-DVLez4R1.js +0 -1
  197. zenml/zen_server/dashboard/assets/page-Dg7-H_9i.js +0 -1
  198. zenml/zen_server/dashboard/assets/page-Dw7XuiSo.js +0 -18
  199. zenml/zen_server/dashboard/assets/page-XrmOHHg7.js +0 -1
  200. zenml/zen_server/dashboard/assets/page-oRm7D4TC.js +0 -1
  201. zenml/zen_server/dashboard/assets/page-x2GXC8sI.js +0 -1
  202. zenml/zen_server/dashboard/assets/page-z2FXP4GY.js +0 -1
  203. zenml/zen_server/dashboard/assets/primary-role-CPGHymjN.js +0 -1
  204. {zenml_nightly-0.84.0.dev20250729.dist-info → zenml_nightly-0.84.1.dev20250731.dist-info}/LICENSE +0 -0
  205. {zenml_nightly-0.84.0.dev20250729.dist-info → zenml_nightly-0.84.1.dev20250731.dist-info}/WHEEL +0 -0
  206. {zenml_nightly-0.84.0.dev20250729.dist-info → zenml_nightly-0.84.1.dev20250731.dist-info}/entry_points.txt +0 -0
zenml/VERSION CHANGED
@@ -1 +1 @@
1
- 0.84.0.dev20250729
1
+ 0.84.1.dev20250731
zenml/client.py CHANGED
@@ -7339,19 +7339,24 @@ class Client(metaclass=ClientMetaClass):
7339
7339
  def create_service_account(
7340
7340
  self,
7341
7341
  name: str,
7342
+ full_name: Optional[str] = None,
7342
7343
  description: str = "",
7343
7344
  ) -> ServiceAccountResponse:
7344
7345
  """Create a new service account.
7345
7346
 
7346
7347
  Args:
7347
7348
  name: The name of the service account.
7349
+ full_name: The display name of the service account.
7348
7350
  description: The description of the service account.
7349
7351
 
7350
7352
  Returns:
7351
7353
  The created service account.
7352
7354
  """
7353
7355
  service_account = ServiceAccountRequest(
7354
- name=name, description=description, active=True
7356
+ name=name,
7357
+ full_name=full_name or "",
7358
+ description=description,
7359
+ active=True,
7355
7360
  )
7356
7361
  created_service_account = self.zen_store.create_service_account(
7357
7362
  service_account=service_account
zenml/constants.py CHANGED
@@ -174,6 +174,12 @@ ENV_ZENML_ENABLE_IMPLICIT_AUTH_METHODS = "ZENML_ENABLE_IMPLICIT_AUTH_METHODS"
174
174
  ENV_ZENML_DISABLE_PIPELINE_LOGS_STORAGE = "ZENML_DISABLE_PIPELINE_LOGS_STORAGE"
175
175
  ENV_ZENML_DISABLE_STEP_LOGS_STORAGE = "ZENML_DISABLE_STEP_LOGS_STORAGE"
176
176
  ENV_ZENML_DISABLE_STEP_NAMES_IN_LOGS = "ZENML_DISABLE_STEP_NAMES_IN_LOGS"
177
+ ENV_ZENML_LOGS_STORAGE_MAX_QUEUE_SIZE = "ZENML_LOGS_STORAGE_MAX_QUEUE_SIZE"
178
+ ENV_ZENML_LOGS_STORAGE_QUEUE_TIMEOUT = "ZENML_LOGS_STORAGE_QUEUE_TIMEOUT"
179
+
180
+ ENV_ZENML_LOGS_WRITE_INTERVAL_SECONDS = "ZENML_LOGS_WRITE_INTERVAL_SECONDS"
181
+ ENV_ZENML_LOGS_MERGE_INTERVAL_SECONDS = "ZENML_LOGS_MERGE_INTERVAL_SECONDS"
182
+
177
183
  ENV_ZENML_CUSTOM_SOURCE_ROOT = "ZENML_CUSTOM_SOURCE_ROOT"
178
184
  ENV_ZENML_PIPELINE_RUN_API_TOKEN_EXPIRATION = (
179
185
  "ZENML_PIPELINE_API_TOKEN_EXPIRATION"
@@ -314,8 +320,9 @@ _csp_connect_src_urls = [
314
320
  "https://raw.githubusercontent.com",
315
321
  ]
316
322
  _csp_img_src_urls = [
317
- "https://public-flavor-logos.s3.eu-central-1.amazonaws.com",
318
- "https://avatar.vercel.sh",
323
+ "*"
324
+ # "https://public-flavor-logos.s3.eu-central-1.amazonaws.com",
325
+ # "https://avatar.vercel.sh",
319
326
  ]
320
327
  _csp_frame_src_urls = [
321
328
  "https://zenml.hellonext.co",
@@ -504,3 +511,22 @@ STACK_DEPLOYMENT_API_TOKEN_EXPIRATION = 60 * 6 # 6 hours
504
511
  ZENML_PIPELINE_RUN_API_TOKEN_EXPIRATION = handle_int_env_var(
505
512
  ENV_ZENML_PIPELINE_RUN_API_TOKEN_EXPIRATION, default=0
506
513
  )
514
+
515
+ # Logs storage constants
516
+ # Maximum number of log batches to queue.
517
+ LOGS_STORAGE_MAX_QUEUE_SIZE = handle_int_env_var(
518
+ ENV_ZENML_LOGS_STORAGE_MAX_QUEUE_SIZE, default=100000
519
+ )
520
+ # Queue timeout controls log dropping vs application blocking:
521
+ # - Positive value (e.g., 30): Wait N seconds, then drop logs if queue full
522
+ # - Negative value (e.g., -1): Block indefinitely until queue has space (never drop logs)
523
+ LOGS_STORAGE_QUEUE_TIMEOUT = handle_int_env_var(
524
+ ENV_ZENML_LOGS_STORAGE_QUEUE_TIMEOUT, default=-1
525
+ )
526
+ # Log batch-write and merge windows in seconds.
527
+ LOGS_WRITE_INTERVAL_SECONDS = handle_int_env_var(
528
+ ENV_ZENML_LOGS_WRITE_INTERVAL_SECONDS, default=2
529
+ )
530
+ LOGS_MERGE_INTERVAL_SECONDS = handle_int_env_var(
531
+ ENV_ZENML_LOGS_MERGE_INTERVAL_SECONDS, default=10 * 60
532
+ )
@@ -43,9 +43,12 @@ class GcpIntegration(Integration):
43
43
  """Definition of Google Cloud Platform integration for ZenML."""
44
44
 
45
45
  NAME = GCP
46
+ # Adding the gcsfs<=2024.12.0 for now to solve the issue with the
47
+ # increased number of list calls. This is a temporary fix and should be
48
+ # removed once the issue is resolved.
46
49
  REQUIREMENTS = [
47
50
  "kfp>=2.6.0",
48
- "gcsfs!=2025.5.0,!=2025.5.0.post1",
51
+ "gcsfs!=2025.5.0,!=2025.5.0.post1,<=2024.12.0",
49
52
  "google-cloud-secret-manager",
50
53
  "google-cloud-container>=2.21.0",
51
54
  "google-cloud-artifact-registry>=1.11.3",
zenml/logging/__init__.py CHANGED
@@ -11,14 +11,3 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12
12
  # or implied. See the License for the specific language governing
13
13
  # permissions and limitations under the License.
14
-
15
- """Logging utilities."""
16
-
17
- # How many seconds to wait before uploading logs to the artifact store
18
- STEP_LOGS_STORAGE_INTERVAL_SECONDS: int = 15
19
-
20
- # How many messages to buffer before uploading logs to the artifact store
21
- STEP_LOGS_STORAGE_MAX_MESSAGES: int = 100
22
-
23
- # How often to merge logs into a single file
24
- STEP_LOGS_STORAGE_MERGE_INTERVAL_SECONDS: int = 10 * 60
@@ -13,10 +13,13 @@
13
13
  # permissions and limitations under the License.
14
14
  """ZenML logging handler."""
15
15
 
16
+ import asyncio
16
17
  import logging
17
18
  import os
19
+ import queue
18
20
  import re
19
21
  import sys
22
+ import threading
20
23
  import time
21
24
  from contextlib import nullcontext
22
25
  from contextvars import ContextVar
@@ -35,15 +38,14 @@ from zenml.client import Client
35
38
  from zenml.constants import (
36
39
  ENV_ZENML_DISABLE_PIPELINE_LOGS_STORAGE,
37
40
  ENV_ZENML_DISABLE_STEP_NAMES_IN_LOGS,
41
+ LOGS_MERGE_INTERVAL_SECONDS,
42
+ LOGS_STORAGE_MAX_QUEUE_SIZE,
43
+ LOGS_STORAGE_QUEUE_TIMEOUT,
44
+ LOGS_WRITE_INTERVAL_SECONDS,
38
45
  handle_bool_env_var,
39
46
  )
40
47
  from zenml.exceptions import DoesNotExistException
41
48
  from zenml.logger import get_logger
42
- from zenml.logging import (
43
- STEP_LOGS_STORAGE_INTERVAL_SECONDS,
44
- STEP_LOGS_STORAGE_MAX_MESSAGES,
45
- STEP_LOGS_STORAGE_MERGE_INTERVAL_SECONDS,
46
- )
47
49
  from zenml.models import (
48
50
  LogsRequest,
49
51
  PipelineDeploymentResponse,
@@ -58,6 +60,8 @@ logger = get_logger(__name__)
58
60
 
59
61
  redirected: ContextVar[bool] = ContextVar("redirected", default=False)
60
62
 
63
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
64
+
61
65
  LOGS_EXTENSION = ".log"
62
66
  PIPELINE_RUN_LOGS_FOLDER = "pipeline_runs"
63
67
 
@@ -71,7 +75,6 @@ def remove_ansi_escape_codes(text: str) -> str:
71
75
  Returns:
72
76
  the version of the input string where the escape codes are removed.
73
77
  """
74
- ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
75
78
  return ansi_escape.sub("", text)
76
79
 
77
80
 
@@ -232,93 +235,206 @@ def fetch_logs(
232
235
 
233
236
 
234
237
  class PipelineLogsStorage:
235
- """Helper class which buffers and stores logs to a given URI."""
238
+ """Helper class which buffers and stores logs to a given URI using a background thread."""
236
239
 
237
240
  def __init__(
238
241
  self,
239
242
  logs_uri: str,
240
243
  artifact_store: "BaseArtifactStore",
241
- max_messages: int = STEP_LOGS_STORAGE_MAX_MESSAGES,
242
- time_interval: int = STEP_LOGS_STORAGE_INTERVAL_SECONDS,
243
- merge_files_interval: int = STEP_LOGS_STORAGE_MERGE_INTERVAL_SECONDS,
244
+ max_queue_size: int = LOGS_STORAGE_MAX_QUEUE_SIZE,
245
+ queue_timeout: int = LOGS_STORAGE_QUEUE_TIMEOUT,
246
+ write_interval: int = LOGS_WRITE_INTERVAL_SECONDS,
247
+ merge_files_interval: int = LOGS_MERGE_INTERVAL_SECONDS,
244
248
  ) -> None:
245
249
  """Initialization.
246
250
 
247
251
  Args:
248
252
  logs_uri: the URI of the log file or folder.
249
253
  artifact_store: Artifact Store from the current step context
250
- max_messages: the maximum number of messages to save in the buffer.
251
- time_interval: the amount of seconds before the buffer gets saved
252
- automatically.
254
+ max_queue_size: maximum number of individual messages to queue.
255
+ queue_timeout: timeout in seconds for putting items in queue when full.
256
+ - Positive value: Wait N seconds, then drop logs if queue still full
257
+ - Negative value: Block indefinitely until queue has space (never drop logs)
258
+ write_interval: the amount of seconds before the created files
259
+ get written to the artifact store.
253
260
  merge_files_interval: the amount of seconds before the created files
254
261
  get merged into a single file.
255
262
  """
256
263
  # Parameters
257
264
  self.logs_uri = logs_uri
258
- self.max_messages = max_messages
259
- self.time_interval = time_interval
265
+ self.max_queue_size = max_queue_size
266
+ self.queue_timeout = queue_timeout
267
+ self.write_interval = write_interval
260
268
  self.merge_files_interval = merge_files_interval
261
269
 
262
270
  # State
263
- self.buffer: List[str] = []
264
- self.disabled_buffer: List[str] = []
265
- self.last_save_time = time.time()
266
- self.disabled = False
267
271
  self.artifact_store = artifact_store
268
272
 
269
273
  # Immutable filesystems state
270
274
  self.last_merge_time = time.time()
271
275
 
276
+ # Queue and log storage thread for async processing
277
+ self.log_queue: queue.Queue[str] = queue.Queue(maxsize=max_queue_size)
278
+ self.log_storage_thread: Optional[threading.Thread] = None
279
+ self.shutdown_event = threading.Event()
280
+ self.merge_event = threading.Event()
281
+
282
+ # Start the log storage thread
283
+ self._start_log_storage_thread()
284
+
285
+ def _start_log_storage_thread(self) -> None:
286
+ """Start the log storage thread for processing log queue."""
287
+ if (
288
+ self.log_storage_thread is None
289
+ or not self.log_storage_thread.is_alive()
290
+ ):
291
+ self.log_storage_thread = threading.Thread(
292
+ target=self._log_storage_worker,
293
+ name="LogsStorage-Worker",
294
+ )
295
+ self.log_storage_thread.start()
296
+
297
+ def _process_log_queue(self, force_merge: bool = False) -> None:
298
+ """Write and merge logs to the artifact store using time-based batching.
299
+
300
+ Args:
301
+ force_merge: Whether to force merge the logs.
302
+ """
303
+ try:
304
+ messages = []
305
+
306
+ # Get first message (blocking with timeout)
307
+ try:
308
+ first_message = self.log_queue.get(timeout=1)
309
+ messages.append(first_message)
310
+ except queue.Empty:
311
+ return
312
+
313
+ # Get any remaining messages without waiting (drain quickly)
314
+ while True:
315
+ try:
316
+ additional_message = self.log_queue.get_nowait()
317
+ messages.append(additional_message)
318
+ except queue.Empty:
319
+ break
320
+
321
+ # Write the messages to the artifact store
322
+ if messages:
323
+ self.write_buffer(messages)
324
+
325
+ # Merge the log files if needed
326
+ if (
327
+ self._is_merge_needed
328
+ or self.merge_event.is_set()
329
+ or force_merge
330
+ ):
331
+ self.merge_event.clear()
332
+
333
+ self.merge_log_files(merge_all_files=force_merge)
334
+
335
+ except Exception as e:
336
+ logger.error("Error in log storage thread: %s", e)
337
+ finally:
338
+ # Always mark all queue tasks as done
339
+ for _ in messages:
340
+ self.log_queue.task_done()
341
+
342
+ time.sleep(self.write_interval)
343
+
344
+ def _log_storage_worker(self) -> None:
345
+ """Log storage thread worker that processes the log queue."""
346
+ # Process the log queue until shutdown is requested
347
+ while not self.shutdown_event.is_set():
348
+ self._process_log_queue()
349
+
350
+ # Shutdown requested - drain remaining queue items and merge log files
351
+ self._process_log_queue(force_merge=True)
352
+
353
+ def _shutdown_log_storage_thread(self, timeout: int = 5) -> None:
354
+ """Shutdown the log storage thread gracefully.
355
+
356
+ Args:
357
+ timeout: Maximum time to wait for thread shutdown.
358
+ """
359
+ if self.log_storage_thread and self.log_storage_thread.is_alive():
360
+ # Then signal the worker to begin graceful shutdown
361
+ self.shutdown_event.set()
362
+
363
+ # Wait for thread to finish (it will drain the queue automatically)
364
+ self.log_storage_thread.join(timeout=timeout)
365
+
272
366
  def write(self, text: str) -> None:
273
- """Main write method.
367
+ """Main write method that sends individual messages directly to queue.
274
368
 
275
369
  Args:
276
370
  text: the incoming string.
277
371
  """
372
+ # Skip empty lines
278
373
  if text == "\n":
279
374
  return
280
375
 
281
- if not self.disabled:
282
- # Add timestamp to the message when it's received
376
+ # If the current thread is the log storage thread, do nothing
377
+ # to prevent recursion when the storage thread itself generates logs
378
+ if (
379
+ self.log_storage_thread
380
+ and threading.current_thread() == self.log_storage_thread
381
+ ):
382
+ return
383
+
384
+ # If the current thread is the fsspec IO thread, do nothing
385
+ if self._is_fsspec_io_thread:
386
+ return
387
+
388
+ try:
389
+ # Format the message with timestamp
283
390
  timestamp = utc_now().strftime("%Y-%m-%d %H:%M:%S")
284
391
  formatted_message = (
285
392
  f"[{timestamp} UTC] {remove_ansi_escape_codes(text)}"
286
393
  )
287
- self.buffer.append(formatted_message.rstrip())
288
- self.save_to_file()
394
+ formatted_message = formatted_message.rstrip()
395
+
396
+ # Send individual message directly to queue
397
+ if not self.shutdown_event.is_set():
398
+ try:
399
+ if self.queue_timeout < 0:
400
+ # Negative timeout = block indefinitely until queue has space
401
+ # Guarantees no log loss but may hang application
402
+ self.log_queue.put(formatted_message)
403
+ else:
404
+ # Positive timeout = wait specified time then drop logs
405
+ # Prevents application hanging but may lose logs
406
+ self.log_queue.put(
407
+ formatted_message, timeout=self.queue_timeout
408
+ )
409
+ except queue.Full:
410
+ # This only happens with positive timeout
411
+ # Queue is full - just skip this message to avoid blocking
412
+ # Better to drop logs than hang the application
413
+ pass
414
+
415
+ except Exception:
416
+ # Silently ignore errors to prevent recursion
417
+ pass
289
418
 
290
419
  @property
291
- def _is_write_needed(self) -> bool:
292
- """Checks whether the buffer needs to be written to disk.
420
+ def _is_merge_needed(self) -> bool:
421
+ """Checks whether the log files need to be merged.
293
422
 
294
423
  Returns:
295
- whether the buffer needs to be written to disk.
424
+ whether the log files need to be merged.
296
425
  """
297
426
  return (
298
- len(self.buffer) >= self.max_messages
299
- or time.time() - self.last_save_time >= self.time_interval
427
+ self.artifact_store.config.IS_IMMUTABLE_FILESYSTEM
428
+ and time.time() - self.last_merge_time > self.merge_files_interval
300
429
  )
301
430
 
302
- def _get_timestamped_filename(self, suffix: str = "") -> str:
303
- """Returns a timestamped filename.
304
-
305
- Args:
306
- suffix: optional suffix for the file name
431
+ @property
432
+ def _is_fsspec_io_thread(self) -> bool:
433
+ """Checks if the current thread is the fsspec IO thread.
307
434
 
308
435
  Returns:
309
- The timestamped filename.
436
+ whether the current thread is the fsspec IO thread.
310
437
  """
311
- return f"{time.time()}{suffix}{LOGS_EXTENSION}"
312
-
313
- def save_to_file(self, force: bool = False) -> None:
314
- """Method to save the buffer to the given URI.
315
-
316
- Args:
317
- force: whether to force a save even if the write conditions not met.
318
- """
319
- import asyncio
320
- import threading
321
-
322
438
  # Most artifact stores are based on fsspec, which converts between
323
439
  # sync and async operations by using a separate AIO thread.
324
440
  # It may happen that the fsspec call itself will log something,
@@ -329,82 +445,79 @@ class PipelineLogsStorage:
329
445
  # To avoid this, we simply check if we're running in the fsspec AIO
330
446
  # thread and skip the save if that's the case.
331
447
  try:
332
- if (
448
+ return (
333
449
  asyncio.events.get_running_loop() is not None
334
450
  and threading.current_thread().name == "fsspecIO"
335
- ):
336
- return
451
+ )
337
452
  except RuntimeError:
338
453
  # No running loop
339
- pass
454
+ return False
340
455
 
341
- if not self.disabled and (self._is_write_needed or force):
342
- # IMPORTANT: keep this as the first code line in this method! The
343
- # code that follows might still emit logging messages, which will
344
- # end up triggering this method again, causing an infinite loop.
345
- self.disabled = True
456
+ def _get_timestamped_filename(self, suffix: str = "") -> str:
457
+ """Returns a timestamped filename.
346
458
 
347
- try:
348
- # The configured logging handler uses a lock to ensure that
349
- # logs generated by different threads are not interleaved.
350
- # Given that most artifact stores are based on fsspec, which
351
- # use a separate thread for async operations, it may happen that
352
- # the fsspec library itself will log something, which will end
353
- # up in a deadlock.
354
- # To avoid this, we temporarily disable the lock in the logging
355
- # handler while writing to the file.
356
- logging_handler = logging.getLogger().handlers[0]
357
- logging_lock = logging_handler.lock
358
- logging_handler.lock = None
359
-
360
- if self.buffer:
361
- if self.artifact_store.config.IS_IMMUTABLE_FILESYSTEM:
362
- _logs_uri = self._get_timestamped_filename()
363
- with self.artifact_store.open(
364
- os.path.join(
365
- self.logs_uri,
366
- _logs_uri,
367
- ),
368
- "w",
369
- ) as file:
370
- for message in self.buffer:
371
- file.write(f"{message}\n")
372
- else:
373
- with self.artifact_store.open(
374
- self.logs_uri, "a"
375
- ) as file:
376
- for message in self.buffer:
377
- file.write(f"{message}\n")
378
- self.artifact_store._remove_previous_file_versions(
379
- self.logs_uri
380
- )
459
+ Args:
460
+ suffix: optional suffix for the file name
381
461
 
382
- except (OSError, IOError) as e:
383
- # This exception can be raised if there are issues with the
384
- # underlying system calls, such as reaching the maximum number
385
- # of open files, permission issues, file corruption, or other
386
- # I/O errors.
387
- logger.error(f"Error while trying to write logs: {e}")
388
- finally:
389
- # Restore the original logging handler lock
390
- logging_handler.lock = logging_lock
462
+ Returns:
463
+ The timestamped filename.
464
+ """
465
+ return f"{time.time()}{suffix}{LOGS_EXTENSION}"
391
466
 
392
- self.buffer = []
393
- self.last_save_time = time.time()
467
+ def write_buffer(self, buffer_to_write: List[str]) -> None:
468
+ """Write the given buffer to file. This runs in the log storage thread.
394
469
 
395
- self.disabled = False
396
- # merge created files on a given interval (defaults to 10 minutes)
397
- # only runs on Immutable Filesystems
398
- if (
399
- self.artifact_store.config.IS_IMMUTABLE_FILESYSTEM
400
- and time.time() - self.last_merge_time > self.merge_files_interval
401
- ):
402
- try:
403
- self.merge_log_files()
404
- except (OSError, IOError) as e:
405
- logger.error(f"Error while trying to roll up logs: {e}")
406
- finally:
407
- self.last_merge_time = time.time()
470
+ Args:
471
+ buffer_to_write: The buffer contents to write to file.
472
+ """
473
+ if not buffer_to_write:
474
+ return
475
+
476
+ # The configured logging handler uses a lock to ensure that
477
+ # logs generated by different threads are not interleaved.
478
+ # Given that most artifact stores are based on fsspec, which
479
+ # use a separate thread for async operations, it may happen that
480
+ # the fsspec library itself will log something, which will end
481
+ # up in a deadlock.
482
+ # To avoid this, we temporarily disable the lock in the logging
483
+ # handler while writing to the file.
484
+ logging_handler = None
485
+ logging_lock = None
486
+ try:
487
+ # Only try to access logging handler if it exists
488
+ root_logger = logging.getLogger()
489
+ if root_logger.handlers:
490
+ logging_handler = root_logger.handlers[0]
491
+ logging_lock = getattr(logging_handler, "lock", None)
492
+ if logging_lock:
493
+ logging_handler.lock = None
494
+
495
+ # If the artifact store is immutable, write the buffer to a new file
496
+ if self.artifact_store.config.IS_IMMUTABLE_FILESYSTEM:
497
+ _logs_uri = self._get_timestamped_filename()
498
+ with self.artifact_store.open(
499
+ os.path.join(
500
+ self.logs_uri,
501
+ _logs_uri,
502
+ ),
503
+ "w",
504
+ ) as file:
505
+ for message in buffer_to_write:
506
+ file.write(f"{message}\n")
507
+
508
+ # If the artifact store is mutable, append the buffer to the existing file
509
+ else:
510
+ with self.artifact_store.open(self.logs_uri, "a") as file:
511
+ for message in buffer_to_write:
512
+ file.write(f"{message}\n")
513
+ self.artifact_store._remove_previous_file_versions(
514
+ self.logs_uri
515
+ )
516
+
517
+ finally:
518
+ # Re-enable the logging lock
519
+ if logging_handler and logging_lock:
520
+ logging_handler.lock = logging_lock
408
521
 
409
522
  def merge_log_files(self, merge_all_files: bool = False) -> None:
410
523
  """Merges all log files into one in the given URI.
@@ -414,12 +527,15 @@ class PipelineLogsStorage:
414
527
  Args:
415
528
  merge_all_files: whether to merge all files or only raw files
416
529
  """
530
+ # If the artifact store is immutable, merge the log files
417
531
  if self.artifact_store.config.IS_IMMUTABLE_FILESYSTEM:
418
532
  merged_file_suffix = "_merged"
419
533
  files_ = self.artifact_store.listdir(self.logs_uri)
420
534
  if not merge_all_files:
421
535
  # already merged files will not be merged again
422
- files_ = [f for f in files_ if merged_file_suffix not in f]
536
+ files_ = [
537
+ f for f in files_ if merged_file_suffix not in str(f)
538
+ ]
423
539
  file_name_ = self._get_timestamped_filename(
424
540
  suffix=merged_file_suffix
425
541
  )
@@ -453,6 +569,13 @@ class PipelineLogsStorage:
453
569
  os.path.join(self.logs_uri, str(file))
454
570
  )
455
571
 
572
+ # Update the last merge time
573
+ self.last_merge_time = time.time()
574
+
575
+ def send_merge_event(self) -> None:
576
+ """Send a merge event to the log storage thread."""
577
+ self.merge_event.set()
578
+
456
579
 
457
580
  class PipelineLogsStorageContext:
458
581
  """Context manager which patches stdout and stderr during pipeline run execution."""
@@ -474,6 +597,7 @@ class PipelineLogsStorageContext:
474
597
  logs_uri=logs_uri, artifact_store=artifact_store
475
598
  )
476
599
  self.prepend_step_name = prepend_step_name
600
+ self._original_methods_saved = False
477
601
 
478
602
  def __enter__(self) -> "PipelineLogsStorageContext":
479
603
  """Enter condition of the context manager.
@@ -484,17 +608,16 @@ class PipelineLogsStorageContext:
484
608
  Returns:
485
609
  self
486
610
  """
611
+ # Save original write methods (flush methods are not wrapped)
487
612
  self.stdout_write = getattr(sys.stdout, "write")
488
- self.stdout_flush = getattr(sys.stdout, "flush")
489
-
490
613
  self.stderr_write = getattr(sys.stderr, "write")
491
- self.stderr_flush = getattr(sys.stderr, "flush")
614
+ self._original_methods_saved = True
492
615
 
616
+ # Wrap stdout/stderr write methods only
617
+ # Note: We don't wrap flush() as it's unnecessary overhead and the
618
+ # background thread handles log flushing based on time/buffer size
493
619
  setattr(sys.stdout, "write", self._wrap_write(self.stdout_write))
494
- setattr(sys.stdout, "flush", self._wrap_flush(self.stdout_flush))
495
-
496
620
  setattr(sys.stderr, "write", self._wrap_write(self.stderr_write))
497
- setattr(sys.stderr, "flush", self._wrap_flush(self.stderr_flush))
498
621
 
499
622
  redirected.set(True)
500
623
  return self
@@ -514,20 +637,20 @@ class PipelineLogsStorageContext:
514
637
 
515
638
  Restores the `write` method of both stderr and stdout.
516
639
  """
517
- self.storage.save_to_file(force=True)
518
-
519
- setattr(sys.stdout, "write", self.stdout_write)
520
- setattr(sys.stdout, "flush", self.stdout_flush)
521
-
522
- setattr(sys.stderr, "write", self.stderr_write)
523
- setattr(sys.stderr, "flush", self.stderr_flush)
524
-
525
- redirected.set(False)
640
+ # Step 1: Restore stdout/stderr FIRST to prevent logging during shutdown
641
+ try:
642
+ if self._original_methods_saved:
643
+ setattr(sys.stdout, "write", self.stdout_write)
644
+ setattr(sys.stderr, "write", self.stderr_write)
645
+ redirected.set(False)
646
+ except Exception:
647
+ pass
526
648
 
649
+ # Step 2: Shutdown thread (it will automatically drain queue and merge files)
527
650
  try:
528
- self.storage.merge_log_files(merge_all_files=True)
529
- except (OSError, IOError) as e:
530
- logger.warning(f"Step logs roll-up failed: {e}")
651
+ self.storage._shutdown_log_storage_thread()
652
+ except Exception:
653
+ pass
531
654
 
532
655
  def _wrap_write(self, method: Callable[..., Any]) -> Callable[..., Any]:
533
656
  """Wrapper function that utilizes the storage object to store logs.
@@ -577,23 +700,6 @@ class PipelineLogsStorageContext:
577
700
 
578
701
  return wrapped_write
579
702
 
580
- def _wrap_flush(self, method: Callable[..., Any]) -> Callable[..., Any]:
581
- """Wrapper function that flushes the buffer of the storage object.
582
-
583
- Args:
584
- method: the original flush method
585
-
586
- Returns:
587
- the wrapped flush method.
588
- """
589
-
590
- def wrapped_flush(*args: Any, **kwargs: Any) -> Any:
591
- output = method(*args, **kwargs)
592
- self.storage.save_to_file()
593
- return output
594
-
595
- return wrapped_flush
596
-
597
703
 
598
704
  def setup_orchestrator_logging(
599
705
  run_id: str, deployment: "PipelineDeploymentResponse"