From 23255d49d4ad1eac6352b916f2c2a4e9cf066dfd Mon Sep 17 00:00:00 2001 From: Martin Xu <15661672+martinxu9@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:30:04 -0700 Subject: [PATCH] Add back build log command to CLI (#2053) --- reflex/reflex.py | 41 +++++++++++++++---- reflex/utils/hosting.py | 87 ++++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 48 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 22001a34fa..18b604b3d2 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -544,7 +544,7 @@ def deploy( enabled_regions = pre_deploy_response.enabled_regions except Exception as ex: - console.error(f"Unable to prepare deployment due to: {ex}") + console.error(f"Unable to prepare deployment") raise typer.Exit(1) from ex # The app prefix should not change during the time of preparation @@ -572,7 +572,6 @@ def deploy( key = key_candidate # Then CP needs to know the user's location, which requires user permission - console.debug(f"{enabled_regions=}") while True: region_input = console.ask( "Region to deploy to. Enter to use default.", @@ -669,10 +668,15 @@ def deploy( console.print("Waiting for server to report progress ...") # Display the key events such as build, deploy, etc - asyncio.get_event_loop().run_until_complete( + server_report_deploy_success = asyncio.get_event_loop().run_until_complete( hosting.display_deploy_milestones(key, from_iso_timestamp=deploy_requested_at) ) - + if not server_report_deploy_success: + console.error("Hosting server reports failure.") + console.error( + f"Check the server logs using `reflex deployments build-logs {key}`" + ) + raise typer.Exit(1) console.print("Waiting for the new deployment to come up") backend_up = frontend_up = False @@ -741,7 +745,7 @@ def list_deployments( try: deployments = hosting.list_deployments() except Exception as ex: - console.error(f"Unable to list deployments due to: {ex}") + console.error(f"Unable to list deployments") raise typer.Exit(1) from ex if as_json: @@ -768,7 +772,7 @@ def delete_deployment( try: hosting.delete_deployment(key) except Exception as ex: - console.error(f"Unable to delete deployment due to: {ex}") + console.error(f"Unable to delete deployment") raise typer.Exit(1) from ex console.print(f"Successfully deleted [ {key} ].") @@ -805,7 +809,7 @@ def get_deployment_status( table = list(frontend_status.values()) console.print(tabulate([table], headers=headers)) except Exception as ex: - console.error(f"Unable to get deployment status due to: {ex}") + console.error(f"Unable to get deployment status") raise typer.Exit(1) from ex @@ -822,7 +826,28 @@ def get_deployment_logs( try: asyncio.get_event_loop().run_until_complete(hosting.get_logs(key)) except Exception as ex: - console.error(f"Unable to get deployment logs due to: {ex}") + console.error(f"Unable to get deployment logs") + raise typer.Exit(1) from ex + + +@deployments_cli.command(name="build-logs") +def get_deployment_build_logs( + key: str = typer.Argument(..., help="The name of the deployment."), + loglevel: constants.LogLevel = typer.Option( + config.loglevel, help="The log level to use." + ), +): + """Get the logs for a deployment.""" + console.set_log_level(loglevel) + + console.print("Note: there is a few seconds delay for logs to be available.") + try: + # TODO: we need to find a way not to fetch logs + # that match the deployed app name but not previously of a different owner + # This should not happen often + asyncio.run(hosting.get_logs(key, log_type=hosting.LogType.BUILD_LOG)) + except Exception as ex: + console.error(f"Unable to get deployment logs") raise typer.Exit(1) from ex diff --git a/reflex/utils/hosting.py b/reflex/utils/hosting.py index 6102b9d27e..494ef398a1 100644 --- a/reflex/utils/hosting.py +++ b/reflex/utils/hosting.py @@ -44,7 +44,7 @@ # Expected server response time to new deployment request. In seconds. DEPLOYMENT_PICKUP_DELAY = 30 # End of deployment workflow message. Used to determine if it is the last message from server. -END_OF_DEPLOYMENT_MESSAGES = ["deploy success", "deploy failed"] +END_OF_DEPLOYMENT_MESSAGES = ["deploy success"] # How many iterations to try and print the deployment event messages from server during deployment. DEPLOYMENT_EVENT_MESSAGES_RETRIES = 90 # Timeout limit for http requests @@ -93,7 +93,7 @@ def validate_token(token: str): response.raise_for_status() except httpx.RequestError as re: console.debug(f"Request to auth server failed due to {re}") - raise Exception("request error") from re + raise Exception(str(re)) from re except httpx.HTTPError as ex: console.debug(f"Unable to validate the token due to: {ex}") raise Exception("server error") from ex @@ -307,22 +307,22 @@ def prepare_deploy( enabled_regions=response_json.get("enabled_regions"), ) except httpx.RequestError as re: - console.debug(f"Unable to prepare launch due to {re}.") - raise Exception("request error") from re + console.error(f"Unable to prepare launch due to {re}.") + raise Exception(str(re)) from re except httpx.HTTPError as he: - console.debug(f"Unable to prepare deploy due to {he}.") + console.error(f"Unable to prepare deploy due to {he}.") raise Exception(f"{he}") from he except json.JSONDecodeError as jde: - console.debug(f"Server did not respond with valid json: {jde}") + console.error(f"Server did not respond with valid json: {jde}") raise Exception("internal errors") from jde except (KeyError, ValidationError) as kve: - console.debug(f"The server response format is unexpected {kve}") + console.error(f"The server response format is unexpected {kve}") raise Exception("internal errors") from kve except ValueError as ve: # This is a recognized client error, currently indicates forbidden raise Exception(f"{ve}") from ve except Exception as ex: - console.debug(f"Unexpected error: {ex}.") + console.error(f"Unexpected error: {ex}.") raise Exception("internal errors") from ex @@ -460,26 +460,26 @@ def deploy( backend_url=response_json["backend_url"], ) except OSError as oe: - console.debug(f"Client side error related to file operation: {oe}") + console.error(f"Client side error related to file operation: {oe}") raise except httpx.RequestError as re: - console.debug(f"Unable to deploy due to request error: {re}") + console.error(f"Unable to deploy due to request error: {re}") raise Exception("request error") from re except httpx.HTTPError as he: - console.debug(f"Unable to deploy due to {he}.") - raise Exception("internal errors") from he + console.error(f"Unable to deploy due to {he}.") + raise Exception(str) from he except json.JSONDecodeError as jde: - console.debug(f"Server did not respond with valid json: {jde}") + console.error(f"Server did not respond with valid json: {jde}") raise Exception("internal errors") from jde except (KeyError, ValidationError) as kve: - console.debug(f"Post params or server response format unexpected: {kve}") + console.error(f"Post params or server response format unexpected: {kve}") raise Exception("internal errors") from kve except AssertionError as ve: - console.debug(f"Unable to deploy due to request error: {ve}") + console.error(f"Unable to deploy due to request error: {ve}") # re-raise the error back to the user as client side error raise except Exception as ex: - console.debug(f"Unable to deploy due to internal errors: {ex}.") + console.error(f"Unable to deploy due to internal errors: {ex}.") raise Exception("internal errors") from ex @@ -552,13 +552,13 @@ def list_deployments( for deployment in response.json() ] except httpx.RequestError as re: - console.debug(f"Unable to list deployments due to request error: {re}") + console.error(f"Unable to list deployments due to request error: {re}") raise Exception("request timeout") from re except httpx.HTTPError as he: - console.debug(f"Unable to list deployments due to {he}.") + console.error(f"Unable to list deployments due to {he}.") raise Exception("internal errors") from he except (ValidationError, KeyError, json.JSONDecodeError) as vkje: - console.debug(f"Server response format unexpected: {vkje}") + console.error(f"Server response format unexpected: {vkje}") raise Exception("internal errors") from vkje except Exception as ex: console.error(f"Unexpected error: {ex}.") @@ -584,15 +584,15 @@ def fetch_token(request_id: str) -> tuple[str, str]: access_token = (resp_json := resp.json()).get("access_token", "") invitation_code = resp_json.get("code", "") except httpx.RequestError as re: - console.debug(f"Unable to fetch token due to request error: {re}") + console.error(f"Unable to fetch token due to request error: {re}") except httpx.HTTPError as he: - console.debug(f"Unable to fetch token due to {he}") + console.error(f"Unable to fetch token due to {he}") except json.JSONDecodeError as jde: - console.debug(f"Server did not respond with valid json: {jde}") + console.error(f"Server did not respond with valid json: {jde}") except KeyError as ke: - console.debug(f"Server response format unexpected: {ke}") + console.error(f"Server response format unexpected: {ke}") except Exception: - console.debug("Unexpected errors: {ex}") + console.error("Unexpected errors: {ex}") return access_token, invitation_code @@ -608,7 +608,7 @@ def poll_backend(backend_url: str) -> bool: """ try: console.debug(f"Polling backend at {backend_url}") - resp = httpx.get(f"{backend_url}/ping", timeout=HTTP_REQUEST_TIMEOUT) + resp = httpx.get(f"{backend_url}/ping", timeout=1) resp.raise_for_status() return True except httpx.HTTPError: @@ -626,7 +626,7 @@ def poll_frontend(frontend_url: str) -> bool: """ try: console.debug(f"Polling frontend at {frontend_url}") - resp = httpx.get(f"{frontend_url}", timeout=HTTP_REQUEST_TIMEOUT) + resp = httpx.get(f"{frontend_url}", timeout=1) resp.raise_for_status() return True except httpx.HTTPError: @@ -664,13 +664,13 @@ def delete_deployment(key: str): response.raise_for_status() except httpx.TimeoutException as te: - console.debug("Unable to delete deployment due to request timeout.") + console.error("Unable to delete deployment due to request timeout.") raise Exception("request timeout") from te except httpx.HTTPError as he: - console.debug(f"Unable to delete deployment due to {he}.") + console.error(f"Unable to delete deployment due to {he}.") raise Exception("internal errors") from he except Exception as ex: - console.debug(f"Unexpected errors {ex}.") + console.error(f"Unexpected errors {ex}.") raise Exception("internal errors") from ex @@ -755,7 +755,7 @@ def get_deployment_status(key: str) -> DeploymentStatusResponse: ), ) except Exception as ex: - console.debug(f"Unable to get deployment status due to {ex}.") + console.error(f"Unable to get deployment status due to {ex}.") raise Exception("internal errors") from ex @@ -772,7 +772,7 @@ def convert_to_local_time(iso_timestamp: str) -> str: local_dt = datetime.fromisoformat(iso_timestamp).astimezone() return local_dt.strftime("%Y-%m-%d %H:%M:%S.%f %Z") except Exception as ex: - console.debug(f"Unable to convert iso timestamp {iso_timestamp} due to {ex}.") + console.error(f"Unable to convert iso timestamp {iso_timestamp} due to {ex}.") return iso_timestamp @@ -1041,7 +1041,7 @@ def log_out_on_browser(): ) -async def display_deploy_milestones(key: str, from_iso_timestamp: datetime): +async def display_deploy_milestones(key: str, from_iso_timestamp: datetime) -> bool: """Display the deploy milestone messages reported back from the hosting server. Args: @@ -1051,6 +1051,9 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime): Raises: ValueError: If a non-empty key is not provided. Exception: If the user is not authenticated. + + Returns: + False if server reports back failure, True otherwise. """ if not key: raise ValueError("Non-empty key is required for querying deploy status.") @@ -1076,18 +1079,22 @@ async def display_deploy_milestones(key: str, from_iso_timestamp: datetime): ] ) ) - if any( - msg in row_json["message"].lower() - for msg in END_OF_DEPLOYMENT_MESSAGES - ): + server_message = row_json["message"].lower() + if "fail" in server_message: + console.debug( + "Received failure message, stop event message streaming" + ) + return False + if any(msg in server_message for msg in END_OF_DEPLOYMENT_MESSAGES): console.debug( "Received end of deployment message, stop event message streaming" ) - return + return True else: console.debug("Server responded, no new events yet, this is normal") except Exception as ex: console.debug(f"Unable to get more deployment events due to {ex}.") + return False def wait_for_server_to_pick_up_request(): @@ -1141,16 +1148,16 @@ def get_regions() -> list[dict]: response.raise_for_status() response_json = response.json() if response_json is None or not isinstance(response_json, list): - console.debug("Expect server to return a list ") + console.error("Expect server to return a list ") return [] if ( response_json and response_json[0] is not None and not isinstance(response_json[0], dict) ): - console.debug("Expect return values are dict's") + console.error("Expect return values are dict's") return [] return response_json except Exception as ex: - console.debug(f"Unable to get regions due to {ex}.") + console.error(f"Unable to get regions due to {ex}.") return []