From cc9ef87562abc108ddb289bd7fcc38db9ff8ce6f Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Thu, 15 May 2025 14:20:09 +0800 Subject: [PATCH 1/2] Streaming logs in deploy --- nbs/00_core.ipynb | 45 ++++++++++++++++++++++++++++++++------------ plash_cli/_modidx.py | 1 + plash_cli/core.py | 31 +++++++++++++++++++++--------- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index dcc6795..63b3648 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -34,6 +34,7 @@ "from pathlib import Path\n", "from uuid import uuid4\n", "from time import time, sleep\n", + "import threading\n", "\n", "import io, os, re, tarfile, tomllib" ] @@ -326,6 +327,36 @@ " return tarz, len(files)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "19fd70cb", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "log_modes = str_enum('log_modes', 'build', 'app')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1c1db20", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _logs_during_deploy(\n", + " path:Path, # Path to project\n", + " mode:str='build', # 'build' or 'run'\n", + " tail:bool=True): # We always want to tail the logs\n", + " # Give the server time to start\n", + " sleep(1) \n", + " # Unwrap the logs function to avoid the call_parse decorator so it can be wrapped in a thread\n", + " log_thread = threading.Thread(target=logs.__wrapped__, kwargs=dict(path=path, mode='build', tail=True))\n", + " log_thread.start()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -355,7 +386,8 @@ " if resp.status_code == 200:\n", " print('✅ Upload complete! Your app is currently being built.')\n", " print(f'It will be live at {endpoint(sub=aid)}')\n", - " else: print(f'Failure: {resp.status_code}\\n{resp.text}')" + " else: print(f'Failure: {resp.status_code}\\n{resp.text}')\n", + " _logs_during_deploy(path=path, mode='build', tail=True)" ] }, { @@ -461,17 +493,6 @@ "## App - logs" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "24e62f1f", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "log_modes = str_enum('log_modes', 'build', 'app')" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/plash_cli/_modidx.py b/plash_cli/_modidx.py index 474f693..66aa234 100644 --- a/plash_cli/_modidx.py +++ b/plash_cli/_modidx.py @@ -7,6 +7,7 @@ 'lib_path': 'plash_cli'}, 'syms': { 'plash_cli.core': { 'plash_cli.core.PlashError': ('core.html#plasherror', 'plash_cli/core.py'), 'plash_cli.core._deps': ('core.html#_deps', 'plash_cli/core.py'), + 'plash_cli.core._logs_during_deploy': ('core.html#_logs_during_deploy', 'plash_cli/core.py'), 'plash_cli.core.create_tar_archive': ('core.html#create_tar_archive', 'plash_cli/core.py'), 'plash_cli.core.delete': ('core.html#delete', 'plash_cli/core.py'), 'plash_cli.core.deploy': ('core.html#deploy', 'plash_cli/core.py'), diff --git a/plash_cli/core.py b/plash_cli/core.py index 8172b0b..11eb4f9 100644 --- a/plash_cli/core.py +++ b/plash_cli/core.py @@ -3,7 +3,7 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb. # %% auto 0 -__all__ = ['PLASH_CONFIG_HOME', 'PLASH_DOMAIN', 'pat', 'stop', 'start', 'log_modes', 'get_client', 'mk_auth_req', 'get_app_id', +__all__ = ['PLASH_CONFIG_HOME', 'PLASH_DOMAIN', 'pat', 'log_modes', 'stop', 'start', 'get_client', 'mk_auth_req', 'get_app_id', 'endpoint', 'is_included', 'poll_cookies', 'login', 'PlashError', 'validate_app', 'create_tar_archive', 'deploy', 'view', 'delete', 'endpoint_func', 'logs', 'download'] @@ -14,6 +14,7 @@ from pathlib import Path from uuid import uuid4 from time import time, sleep +import threading import io, os, re, tarfile, tomllib @@ -121,6 +122,20 @@ def create_tar_archive(path:Path) -> tuple[io.BytesIO, int]: return tarz, len(files) # %% ../nbs/00_core.ipynb 24 +log_modes = str_enum('log_modes', 'build', 'app') + +# %% ../nbs/00_core.ipynb 25 +def _logs_during_deploy( + path:Path, # Path to project + mode:str='build', # 'build' or 'run' + tail:bool=True): # We always want to tail the logs + # Give the server time to start + sleep(1) + # Unwrap the logs function to avoid the call_parse decorator so it can be wrapped in a thread + log_thread = threading.Thread(target=logs.__wrapped__, kwargs=dict(path=path, mode='build', tail=True)) + log_thread.start() + +# %% ../nbs/00_core.ipynb 26 @call_parse def deploy( path:Path=Path('.'), # Path to project @@ -143,8 +158,9 @@ def deploy( print('✅ Upload complete! Your app is currently being built.') print(f'It will be live at {endpoint(sub=aid)}') else: print(f'Failure: {resp.status_code}\n{resp.text}') + _logs_during_deploy(path=path, mode='build', tail=True) -# %% ../nbs/00_core.ipynb 26 +# %% ../nbs/00_core.ipynb 28 @call_parse def view( path:Path=Path('.'), # Path to project @@ -153,7 +169,7 @@ def view( print(f"Opening browser to view app :\n{url}\n") webbrowser.open(url) -# %% ../nbs/00_core.ipynb 28 +# %% ../nbs/00_core.ipynb 30 @call_parse def delete( path:Path=Path('.'), # Path to project @@ -170,7 +186,7 @@ def delete( r = mk_auth_req(endpoint(rt=f"/delete?aid={aid}"), "delete") return r.text -# %% ../nbs/00_core.ipynb 30 +# %% ../nbs/00_core.ipynb 32 def endpoint_func(endpoint_name): 'Creates a function for a specific API endpoint' @call_parse @@ -191,10 +207,7 @@ def func( stop = endpoint_func('/stop') start = endpoint_func('/start') -# %% ../nbs/00_core.ipynb 32 -log_modes = str_enum('log_modes', 'build', 'app') - -# %% ../nbs/00_core.ipynb 33 +# %% ../nbs/00_core.ipynb 34 @call_parse def logs( path:Path=Path('.'), # Path to project @@ -219,7 +232,7 @@ def logs( r = mk_auth_req(endpoint(rt=f"/logs?aid={aid}&mode={mode}")) return r.text -# %% ../nbs/00_core.ipynb 35 +# %% ../nbs/00_core.ipynb 36 @call_parse def download( path:Path=Path('.'), # Path to project From 6db74e950f87fb9fb3f01b3e4142742eed224273 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Thu, 15 May 2025 14:58:01 +0800 Subject: [PATCH 2/2] Last attempt without plash server modifications --- nbs/00_core.ipynb | 52 +++++++++++++++++++++++++++++++++++++++-------- plash_cli/core.py | 47 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index 63b3648..902a602 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -34,7 +34,9 @@ "from pathlib import Path\n", "from uuid import uuid4\n", "from time import time, sleep\n", + "from datetime import datetime\n", "import threading\n", + "from contextlib import redirect_stdout\n", "\n", "import io, os, re, tarfile, tomllib" ] @@ -348,13 +350,40 @@ "#| export\n", "def _logs_during_deploy(\n", " path:Path, # Path to project\n", - " mode:str='build', # 'build' or 'run'\n", - " tail:bool=True): # We always want to tail the logs\n", - " # Give the server time to start\n", - " sleep(1) \n", - " # Unwrap the logs function to avoid the call_parse decorator so it can be wrapped in a thread\n", - " log_thread = threading.Thread(target=logs.__wrapped__, kwargs=dict(path=path, mode='build', tail=True))\n", - " log_thread.start()" + " mode:str='build'): # 'build' or 'run'\n", + " \"Run the logs function in tail mode, waiting for the build to finish\"\n", + " # Get the current Build End time\n", + "\n", + " starttime = 0 \n", + " captured_output = io.StringIO()\n", + " with redirect_stdout(captured_output):\n", + " logs.__wrapped__(path=path, mode=mode, tail=False)\n", + " current_logs = captured_output.getvalue()\n", + " for line in reversed(current_logs.splitlines()):\n", + " print(line)\n", + " if line.strip().startswith('Build End Time:'):\n", + " dt_obj = line.strip().replace('Build End Time:', '').strip()\n", + " starttime = datetime.strptime(dt_obj, \"%Y-%m-%d %H:%M:%S.%f\").timestamp()\n", + "\n", + " print(f'Current build end time:', starttime)\n", + " # Look for a build start time that is greater than the last build end time\n", + " while True:\n", + " with redirect_stdout(captured_output):\n", + " logs.__wrapped__(path=path, mode=mode, tail=False)\n", + " current_logs = captured_output.getvalue()\n", + " for line in reversed(current_logs.splitlines()):\n", + " endtime = 0\n", + " if line.strip().startswith('Build Start Time:'):\n", + " dt_obj = line.strip().replace('Build Start Time:', '').strip()\n", + " endtime = datetime.strptime(dt_obj, \"%Y-%m-%d %H:%M:%S.%f\").timestamp()\n", + " if endtime > starttime:\n", + " break\n", + " print(f'Current build time:', endtime)\n", + " sleep(0.3)\n", + " \n", + " print('Tailing current logs...')\n", + " # Run the logs function in tail mode uncaptured for the user to see\n", + " logs.__wrapped__(path=path, mode=mode, tail=True)" ] }, { @@ -382,12 +411,17 @@ " aid = app_id or parse_env(fn=plash_app)['PLASH_APP_ID']\n", " \n", " tarz, _ = create_tar_archive(path)\n", + "\n", + " print('Start logging...')\n", + " logging = threading.Thread(target=_logs_during_deploy, args=(path, 'build'))\n", + " logging.start()\n", + "\n", + " print('Uploading...')\n", " resp = mk_auth_req(endpoint(rt=\"/upload\"), \"post\", files={'file': tarz}, timeout=300.0, data={'aid': aid})\n", " if resp.status_code == 200:\n", " print('✅ Upload complete! Your app is currently being built.')\n", " print(f'It will be live at {endpoint(sub=aid)}')\n", - " else: print(f'Failure: {resp.status_code}\\n{resp.text}')\n", - " _logs_during_deploy(path=path, mode='build', tail=True)" + " else: print(f'Failure: {resp.status_code}\\n{resp.text}')" ] }, { diff --git a/plash_cli/core.py b/plash_cli/core.py index 11eb4f9..f2e6749 100644 --- a/plash_cli/core.py +++ b/plash_cli/core.py @@ -14,6 +14,7 @@ from pathlib import Path from uuid import uuid4 from time import time, sleep +from datetime import datetime import threading import io, os, re, tarfile, tomllib @@ -126,14 +127,39 @@ def create_tar_archive(path:Path) -> tuple[io.BytesIO, int]: # %% ../nbs/00_core.ipynb 25 def _logs_during_deploy( + aid:str, # App ID that will be used as the subdomain in plash path:Path, # Path to project - mode:str='build', # 'build' or 'run' - tail:bool=True): # We always want to tail the logs - # Give the server time to start - sleep(1) - # Unwrap the logs function to avoid the call_parse decorator so it can be wrapped in a thread - log_thread = threading.Thread(target=logs.__wrapped__, kwargs=dict(path=path, mode='build', tail=True)) - log_thread.start() + mode:str='build'): # 'build' or 'run' + "Run the logs function in tail mode, waiting for the build to finish" + # Get the current Build End time + + starttime = 0 + r = mk_auth_req(endpoint(rt=f"/logs?aid={aid}&mode={mode}")) + for line in reversed(r.text.splitlines()): + print(line) + if line.strip().startswith('Build End Time:'): + dt_obj = line.strip().replace('Build End Time:', '').strip() + starttime = datetime.strptime(dt_obj, "%Y-%m-%d %H:%M:%S.%f").timestamp() + + print(f'Current build end time:', starttime) + # Look for a build start time that is greater than the last build end time + while True: + print('Waiting for build to start...') + r = mk_auth_req(endpoint(rt=f"/logs?aid={aid}&mode={mode}")) + if r.status_code != 200: print(f"Error: {r.status_code}") + for line in reversed(r.text.splitlines()): + endtime = 0 + if line.strip().startswith('Build Start Time:'): + dt_obj = line.strip().replace('Build Start Time:', '').strip() + endtime = datetime.strptime(dt_obj, "%Y-%m-%d %H:%M:%S.%f").timestamp() + if endtime > starttime: + break + print(f'Current build time:', endtime) + sleep(0.3) + + print('Tailing current logs...') + # Run the logs function in tail mode uncaptured for the user to see + logs.__wrapped__(path=path, mode=mode, tail=True) # %% ../nbs/00_core.ipynb 26 @call_parse @@ -153,12 +179,17 @@ def deploy( aid = app_id or parse_env(fn=plash_app)['PLASH_APP_ID'] tarz, _ = create_tar_archive(path) + + print('Start logging...') + logging = threading.Thread(target=_logs_during_deploy, args=(aid, path, 'build')) + logging.start() + + print('Uploading...') resp = mk_auth_req(endpoint(rt="/upload"), "post", files={'file': tarz}, timeout=300.0, data={'aid': aid}) if resp.status_code == 200: print('✅ Upload complete! Your app is currently being built.') print(f'It will be live at {endpoint(sub=aid)}') else: print(f'Failure: {resp.status_code}\n{resp.text}') - _logs_during_deploy(path=path, mode='build', tail=True) # %% ../nbs/00_core.ipynb 28 @call_parse