From cb233c6a5632a70a8172c88af4c009056e84d45f Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Tue, 12 Jul 2022 14:57:29 +0200 Subject: [PATCH 01/19] Added cloud app logs to CLI commands --- src/lightning_app/cli/lightning_cli.py | 74 ++++++++++- src/lightning_app/testing/testing.py | 2 +- src/lightning_app/utilities/app_logs.py | 116 ++++++++++++++++++ .../utilities/logs_socket_api.py | 91 ++++++++++++++ tests/tests_app/cli/test_cmd_show_logs.py | 61 +++++++++ tests/tests_app_examples/test_boring_app.py | 12 +- .../test_collect_failures.py | 3 +- .../test_custom_work_dependencies.py | 4 +- tests/tests_app_examples/test_drive.py | 3 +- tests/tests_app_examples/test_idle_timeout.py | 3 +- tests/tests_app_examples/test_payload.py | 4 +- tests/tests_app_examples/test_quick_start.py | 4 +- .../test_template_react_ui.py | 3 +- .../test_template_streamlit_ui.py | 3 +- tests/tests_app_examples/test_v0_app.py | 3 +- 15 files changed, 369 insertions(+), 17 deletions(-) create mode 100644 src/lightning_app/utilities/app_logs.py create mode 100644 src/lightning_app/utilities/logs_socket_api.py create mode 100644 tests/tests_app/cli/test_cmd_show_logs.py diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index fb39f743ec3a2..dd7e31ea54406 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -1,7 +1,7 @@ import logging import os from pathlib import Path -from typing import Tuple, Union +from typing import List, Tuple, Union import click from requests.exceptions import ConnectionError @@ -11,9 +11,11 @@ from lightning_app.core.constants import get_lightning_cloud_url, LOCAL_LAUNCH_ADMIN_VIEW from lightning_app.runners.runtime import dispatch from lightning_app.runners.runtime_type import RuntimeType +from lightning_app.utilities.app_logs import _app_logs_reader from lightning_app.utilities.cli_helpers import _format_input_env_variables -from lightning_app.utilities.install_components import register_all_external_components +from lightning_app.utilities.cloud import _get_project from lightning_app.utilities.login import Auth +from lightning_app.utilities.network import LightningClient logger = logging.getLogger(__name__) @@ -29,10 +31,76 @@ def get_app_url(runtime_type: RuntimeType, *args) -> str: @click.group() @click.version_option(ver) def main(): - register_all_external_components() pass +@main.group() +def show(): + """Show info about resource.""" + pass + + +@show.command() +@click.argument("app_name", required=False) +@click.argument("components", nargs=-1, required=False) +@click.option("-f", "--follow", required=False, is_flag=True) +def logs(app_name: str, components: List[str], follow: bool) -> None: + + # Get project user is working in + client = LightningClient() + project = _get_project(client) + + # List app applications in the project + apps = { + app.name: app + for app in client.lightningapp_instance_service_list_lightningapp_instances(project.project_id).lightningapps + } + + if not apps: + raise click.ClickException("Your app list is empty. Please run an application first.") + + # If app_name was not provided we ask to select one from the list + if not app_name: + raise click.ClickException( + f"You have not specified any LightningApp. Please select one of available: [{', '.join(apps.keys())}]" + ) + + # Verify that the app actually exists + if app_name not in apps: + raise click.ClickException(f"LightningApp '{app_name}' does not exist.") + + # Fetch all lightning works from given application + # 'Flow' component is somewhat implicit, only one for whole app, + # and not listed in lightningwork API - so we add it directly to the list + works = client.lightningwork_service_list_lightningwork( + project_id=project.project_id, app_id=apps[app_name].id + ).lightningworks + app_component_names = ["flow"] + [f.name for f in apps[app_name].spec.flow_servers] + [w.name for w in works] + + # If components were not provided directly we ask user to select from available + if not components: + components = app_component_names + + # Verify that all components actually exist + for component in components: + if component not in app_component_names: + raise click.ClickException(f"Component '{component}' does not exist in app {app_name}.") + + # Initialize log reader + log_reader = _app_logs_reader( + client=client, + project_id=project.project_id, + app_id=apps[app_name].id, + component_names=components, + follow=follow, + ) + + # Iterate over LogEvents. If we 'follow' then we'll wait on this loop for new entry + for component_name, log_event in log_reader: + message = f"[{component_name}] {log_event.timestamp.isoformat()} {log_event.message}" + click.echo(message, err=log_event.labels.stream == "stderr", color=True) + + @main.command() def login(): """Log in to your Lightning.ai account.""" diff --git a/src/lightning_app/testing/testing.py b/src/lightning_app/testing/testing.py index 7ae9bf6274e6c..06a79916b9052 100644 --- a/src/lightning_app/testing/testing.py +++ b/src/lightning_app/testing/testing.py @@ -270,7 +270,7 @@ def fetch_logs() -> str: ) try: - yield admin_page, view_page, fetch_logs + yield admin_page, view_page, fetch_logs, name except KeyboardInterrupt: pass finally: diff --git a/src/lightning_app/utilities/app_logs.py b/src/lightning_app/utilities/app_logs.py new file mode 100644 index 0000000000000..d797ad998f995 --- /dev/null +++ b/src/lightning_app/utilities/app_logs.py @@ -0,0 +1,116 @@ +import json +import queue +import sys +from dataclasses import dataclass +from datetime import datetime +from json import JSONDecodeError +from threading import Thread +from typing import Iterator, List, Optional, Tuple + +import dateutil.parser +from websocket import WebSocketApp + +from lightning_app.utilities.logs_socket_api import _LightningLogsSocketAPI +from lightning_app.utilities.network import LightningClient + + +@dataclass +class _LogEventLabels: + app: str + container: str + filename: str + job: str + namespace: str + node_name: str + pod: str + stream: Optional[str] = None + + +@dataclass +class _LogEvent: + message: str + timestamp: datetime + labels: _LogEventLabels + + +# This is to push new events to common read_queue +def _push_logevents_to_read_queue_callback(component_name: str, read_queue: queue.PriorityQueue): + def callback(ws_app: WebSocketApp, msg: str): + # We strongly trust that the contract on API will hold atm :D + event_dict = json.loads(msg) + labels = _LogEventLabels(**event_dict["labels"]) + # if "message" not in event_dict: + # print(">>>>>>>",component_name, event_dict) + if "message" in event_dict: + event = _LogEvent( + message=event_dict["message"], + timestamp=dateutil.parser.isoparse(event_dict["timestamp"]), + labels=labels, + ) + read_queue.put((event.timestamp, component_name, event)) + + return callback + + +def _error_callback(ws_app: WebSocketApp, error: Exception): + errors = { + KeyError: "Malformed log message, missing key", + JSONDecodeError: "Malformed log message", + TypeError: "Malformed log format", + ValueError: "Malformed date format", + } + print(f"Error while reading logs ({errors.get(type(error), 'Unknown')})", file=sys.stderr) + ws_app.close() + + +def _app_logs_reader( + client: LightningClient, project_id: str, app_id: str, component_names: List[str], follow: bool +) -> Iterator[Tuple[str, _LogEvent]]: + + read_queue = queue.PriorityQueue() + logs_api_client = _LightningLogsSocketAPI(client.api_client) + + # We will use a socket per component + log_sockets = [ + logs_api_client.create_lightning_logs_socket( + project_id=project_id, + app_id=app_id, + component=component_name, + on_message_callback=_push_logevents_to_read_queue_callback(component_name, read_queue), + on_error_callback=_error_callback, + ) + for component_name in component_names + ] + + # And each socket on separate thread pushing log event to print queue + # run_forever() will run until we close() the connection from outside + log_threads = [Thread(target=work.run_forever) for work in log_sockets] + + # Establish connection and begin pushing logs to the print queue + for th in log_threads: + th.start() + + # Print logs from queue when log event is available + try: + while True: + _, component_name, log_event = read_queue.get(timeout=None if follow else 1.0) + log_event: _LogEvent + + yield component_name, log_event + + except queue.Empty: + # Empty is raised by queue.get if timeout is reached. Follow = False case. + pass + + except KeyboardInterrupt: + # User pressed CTRL+C to exit, we sould respect that + pass + + finally: + # Close connections - it will cause run_forever() to finish -> thread as finishes aswell + for socket in log_sockets: + socket.close() + + # Because all socket were closed, we can just wait for threads to finish. + for th in log_threads: + th.join() diff --git a/src/lightning_app/utilities/logs_socket_api.py b/src/lightning_app/utilities/logs_socket_api.py new file mode 100644 index 0000000000000..b9beaa01874d4 --- /dev/null +++ b/src/lightning_app/utilities/logs_socket_api.py @@ -0,0 +1,91 @@ +from typing import Callable, Optional +from urllib.parse import urlparse + +from lightning_cloud.openapi import ApiClient, AuthServiceApi, V1LoginRequest +from websocket import WebSocketApp + +from lightning_app.utilities.login import Auth + + +class _LightningLogsSocketAPI: + def __init__(self, api_client: ApiClient): + self.api_client = api_client + self._auth = Auth() + self._auth.authenticate() + self._auth_service = AuthServiceApi(api_client) + + def _get_api_token(self) -> str: + token_resp = self._auth_service.auth_service_login( + body=V1LoginRequest( + username=self._auth.username, + api_key=self._auth.api_key, + ) + ) + return token_resp.token + + @staticmethod + def _socket_url(host: str, project_id: str, app_id: str, token: str, component: str) -> str: + return ( + f"wss://{host}/v1/projects/{project_id}/appinstances/{app_id}/logs?" + f"token={token}&component={component}&follow=true" + ) + + def create_lightning_logs_socket( + self, + project_id: str, + app_id: str, + component: str, + on_message_callback: Callable[[WebSocketApp, str], None], + on_error_callback: Optional[Callable[[Exception, str], None]] = None, + ) -> WebSocketApp: + """Creates and returns WebSocketApp to listen to lightning app logs. + + .. code-block:: python + # Synchronous reading, run_forever() is blocking + + def print_log_msg(ws_app, msg): + print(msg) + + flow_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'flow', print_log_msg) + flow_socket.run_forever() + + .. code-block:: python + # Asynchronous reading (with Threads) + + def print_log_msg(ws_app, msg): + print(msg) + + flow_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'flow', print_log_msg) + work_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'work_1', print_log_msg) + + flow_logs_thread = Thread(target=flow_logs_socket.run_forever) + work_logs_thread = Thread(target=work_logs_socket.run_forever) + + flow_logs_thread.start() + work_logs_thread.start() + ....... + + flow_logs_socket.close() + work_logs_thread.close() + + Arguments: + project_id: Project ID. + app_id: Application ID. + component: Component name eg flow. + on_message_callback: Callback object which is called when received data. + on_error_callback: Callback object which is called when we get error. + + Returns: + WebSocketApp of the wanted socket + """ + _token = self._get_api_token() + clean_ws_host = urlparse(self.api_client.configuration.host).netloc + socket_url = self._socket_url( + host=clean_ws_host, + project_id=project_id, + app_id=app_id, + token=_token, + component=component, + ) + + return WebSocketApp(socket_url, on_message=on_message_callback, on_error=on_error_callback) diff --git a/tests/tests_app/cli/test_cmd_show_logs.py b/tests/tests_app/cli/test_cmd_show_logs.py new file mode 100644 index 0000000000000..2c83aa5440d81 --- /dev/null +++ b/tests/tests_app/cli/test_cmd_show_logs.py @@ -0,0 +1,61 @@ +from unittest import mock + +from click.testing import CliRunner + +from lightning_app.cli.lightning_cli import logs + + +@mock.patch("lightning_app.cli.lightning_cli.LightningClient") +@mock.patch("lightning_app.cli.lightning_cli._get_project") +def test_show_logs_errors(project, client): + """Test that the CLI prints the errors for the show logs command.""" + + runner = CliRunner() + + # Response prep + app = mock.MagicMock() + app.name = "MyFakeApp" + work = mock.MagicMock() + work.name = "MyFakeWork" + flow = mock.MagicMock() + flow.name = "MyFakeFlow" + + # No apps ever run + apps = {} + client.return_value.lightningapp_instance_service_list_lightningapp_instances.return_value.lightningapps = apps + + result = runner.invoke(logs, ["NonExistentApp"]) + + assert result.exit_code == 1 + assert "Your app list is empty. Please run an application first." in result.output + + # App not specified + apps = {app} + client.return_value.lightningapp_instance_service_list_lightningapp_instances.return_value.lightningapps = apps + + result = runner.invoke(logs) + + assert result.exit_code == 1 + assert "You have not specified any LightningApp. Please select one of available: [MyFakeApp]" in result.output + + # App does not exit + apps = {app} + client.return_value.lightningapp_instance_service_list_lightningapp_instances.return_value.lightningapps = apps + + result = runner.invoke(logs, ["ThisAppDoesNotExist"]) + + assert result.exit_code == 1 + assert "LightningApp 'ThisAppDoesNotExist' does not exist." in result.output + + # Component does not exist + apps = {app} + works = {work} + flows = {flow} + client.return_value.lightningapp_instance_service_list_lightningapp_instances.return_value.lightningapps = apps + client.return_value.lightningwork_service_list_lightningwork.return_value.lightningworks = works + app.spec.flow_servers = flows + + result = runner.invoke(logs, ["MyFakeApp", "NonExistentComponent"]) + + assert result.exit_code == 1 + assert "Component 'NonExistentComponent' does not exist in app MyFakeApp." in result.output diff --git a/tests/tests_app_examples/test_boring_app.py b/tests/tests_app_examples/test_boring_app.py index 1f681260de5c2..3650bab495be8 100644 --- a/tests/tests_app_examples/test_boring_app.py +++ b/tests/tests_app_examples/test_boring_app.py @@ -1,9 +1,11 @@ import os import pytest -from tests_app import _PROJECT_ROOT +from click.testing import CliRunner +from lightning_app.cli.lightning_cli import logs from lightning_app.testing.testing import run_app_in_cloud, wait_for +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -12,6 +14,7 @@ def test_boring_app_example_cloud() -> None: _, view_page, _, + name, ): def check_hello_there(*_, **__): @@ -21,3 +24,10 @@ def check_hello_there(*_, **__): return True wait_for(view_page, check_hello_there) + + runner = CliRunner() + result = runner.invoke(logs, [name]) + + assert result.exit_code == 0 + assert result.exception is None + assert len(result.output.splitlines()) > 1, result.output diff --git a/tests/tests_app_examples/test_collect_failures.py b/tests/tests_app_examples/test_collect_failures.py index f263ebb1a9f58..7f20c4f32d67e 100644 --- a/tests/tests_app_examples/test_collect_failures.py +++ b/tests/tests_app_examples/test_collect_failures.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -26,6 +26,7 @@ def test_collect_failures_example_cloud() -> None: _, _, fetch_logs, + _, ): last_found_log_index = -1 while len(expected_logs) != 0: diff --git a/tests/tests_app_examples/test_custom_work_dependencies.py b/tests/tests_app_examples/test_custom_work_dependencies.py index 8390233e2eee3..1eae770f4c46c 100644 --- a/tests/tests_app_examples/test_custom_work_dependencies.py +++ b/tests/tests_app_examples/test_custom_work_dependencies.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -13,7 +13,7 @@ def test_custom_work_dependencies_example_cloud() -> None: with run_app_in_cloud( os.path.join(_PROJECT_ROOT, "tests/tests_app_examples/custom_work_dependencies/"), app_name="app.py", - ) as (_, _, fetch_logs): + ) as (_, _, fetch_logs, _): has_logs = False while not has_logs: for log in fetch_logs(): diff --git a/tests/tests_app_examples/test_drive.py b/tests/tests_app_examples/test_drive.py index 9cebca9cf1072..ff027377c30a9 100644 --- a/tests/tests_app_examples/test_drive.py +++ b/tests/tests_app_examples/test_drive.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -13,6 +13,7 @@ def test_drive_example_cloud() -> None: _, view_page, fetch_logs, + _, ): has_logs = False diff --git a/tests/tests_app_examples/test_idle_timeout.py b/tests/tests_app_examples/test_idle_timeout.py index fb58a83aefc93..6838a348f30bc 100644 --- a/tests/tests_app_examples/test_idle_timeout.py +++ b/tests/tests_app_examples/test_idle_timeout.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -13,6 +13,7 @@ def test_idle_timeout_example_cloud() -> None: _, _, fetch_logs, + _, ): has_logs = False while not has_logs: diff --git a/tests/tests_app_examples/test_payload.py b/tests/tests_app_examples/test_payload.py index 28d2391c18a2a..b001ebbb4f557 100644 --- a/tests/tests_app_examples/test_payload.py +++ b/tests/tests_app_examples/test_payload.py @@ -2,14 +2,14 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud +from tests_app import _PROJECT_ROOT @pytest.mark.cloud def test_payload_example_cloud() -> None: - with run_app_in_cloud(os.path.join(_PROJECT_ROOT, "examples/app_payload")) as (_, _, fetch_logs): + with run_app_in_cloud(os.path.join(_PROJECT_ROOT, "examples/app_payload")) as (_, _, fetch_logs, _): has_logs = False while not has_logs: diff --git a/tests/tests_app_examples/test_quick_start.py b/tests/tests_app_examples/test_quick_start.py index 272fdbb7f5b63..947cd86f512c0 100644 --- a/tests/tests_app_examples/test_quick_start.py +++ b/tests/tests_app_examples/test_quick_start.py @@ -4,12 +4,12 @@ import pytest from click.testing import CliRunner -from tests_app import _PROJECT_ROOT from lightning_app import LightningApp from lightning_app.cli.lightning_cli import run_app from lightning_app.testing.helpers import RunIf from lightning_app.testing.testing import run_app_in_cloud, wait_for +from tests_app import _PROJECT_ROOT class QuickStartApp(LightningApp): @@ -51,7 +51,7 @@ def test_quick_start_example(caplog, monkeypatch): @pytest.mark.cloud def test_quick_start_example_cloud() -> None: - with run_app_in_cloud(os.path.join(_PROJECT_ROOT, "lightning-quick-start/")) as (_, view_page, _): + with run_app_in_cloud(os.path.join(_PROJECT_ROOT, "lightning-quick-start/")) as (_, view_page, _, _): def click_gradio_demo(*_, **__): button = view_page.locator('button:has-text("Interactive demo")') diff --git a/tests/tests_app_examples/test_template_react_ui.py b/tests/tests_app_examples/test_template_react_ui.py index 2e348035fe6e5..58379fab7b996 100644 --- a/tests/tests_app_examples/test_template_react_ui.py +++ b/tests/tests_app_examples/test_template_react_ui.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud, wait_for +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -14,6 +14,7 @@ def test_template_react_ui_example_cloud() -> None: _, view_page, fetch_logs, + _, ): def click_button(*_, **__): diff --git a/tests/tests_app_examples/test_template_streamlit_ui.py b/tests/tests_app_examples/test_template_streamlit_ui.py index ec18206bf06dd..7f8ccce995daa 100644 --- a/tests/tests_app_examples/test_template_streamlit_ui.py +++ b/tests/tests_app_examples/test_template_streamlit_ui.py @@ -2,9 +2,9 @@ from time import sleep import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud, wait_for +from tests_app import _PROJECT_ROOT @pytest.mark.cloud @@ -14,6 +14,7 @@ def test_template_streamlit_ui_example_cloud() -> None: _, view_page, fetch_logs, + _, ): def click_button(*_, **__): diff --git a/tests/tests_app_examples/test_v0_app.py b/tests/tests_app_examples/test_v0_app.py index 2c03d2de60e29..09d1cb8780a13 100644 --- a/tests/tests_app_examples/test_v0_app.py +++ b/tests/tests_app_examples/test_v0_app.py @@ -3,10 +3,10 @@ from typing import Tuple import pytest -from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import application_testing, LightningTestApp, run_app_in_cloud, wait_for from lightning_app.utilities.enum import AppStage +from tests_app import _PROJECT_ROOT class LightningAppTestInt(LightningTestApp): @@ -36,6 +36,7 @@ def test_v0_app_example_cloud() -> None: _, view_page, fetch_logs, + _, ): def check_content(button_name, text_content): From fc6d5780184ad32ea7f6c50a575b5a9cd982c1e8 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Tue, 12 Jul 2022 15:15:56 +0200 Subject: [PATCH 02/19] Removed overly descriptive comments. Minor PR improvements. --- src/lightning_app/cli/lightning_cli.py | 8 -------- src/lightning_app/utilities/app_logs.py | 5 ++--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index dd7e31ea54406..29e83d500ea34 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -46,11 +46,9 @@ def show(): @click.option("-f", "--follow", required=False, is_flag=True) def logs(app_name: str, components: List[str], follow: bool) -> None: - # Get project user is working in client = LightningClient() project = _get_project(client) - # List app applications in the project apps = { app.name: app for app in client.lightningapp_instance_service_list_lightningapp_instances(project.project_id).lightningapps @@ -59,13 +57,11 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: if not apps: raise click.ClickException("Your app list is empty. Please run an application first.") - # If app_name was not provided we ask to select one from the list if not app_name: raise click.ClickException( f"You have not specified any LightningApp. Please select one of available: [{', '.join(apps.keys())}]" ) - # Verify that the app actually exists if app_name not in apps: raise click.ClickException(f"LightningApp '{app_name}' does not exist.") @@ -77,16 +73,13 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: ).lightningworks app_component_names = ["flow"] + [f.name for f in apps[app_name].spec.flow_servers] + [w.name for w in works] - # If components were not provided directly we ask user to select from available if not components: components = app_component_names - # Verify that all components actually exist for component in components: if component not in app_component_names: raise click.ClickException(f"Component '{component}' does not exist in app {app_name}.") - # Initialize log reader log_reader = _app_logs_reader( client=client, project_id=project.project_id, @@ -95,7 +88,6 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: follow=follow, ) - # Iterate over LogEvents. If we 'follow' then we'll wait on this loop for new entry for component_name, log_event in log_reader: message = f"[{component_name}] {log_event.timestamp.isoformat()} {log_event.message}" click.echo(message, err=log_event.labels.stream == "stderr", color=True) diff --git a/src/lightning_app/utilities/app_logs.py b/src/lightning_app/utilities/app_logs.py index d797ad998f995..effef144f1b5d 100644 --- a/src/lightning_app/utilities/app_logs.py +++ b/src/lightning_app/utilities/app_logs.py @@ -33,14 +33,13 @@ class _LogEvent: labels: _LogEventLabels -# This is to push new events to common read_queue def _push_logevents_to_read_queue_callback(component_name: str, read_queue: queue.PriorityQueue): + """Pushes _LogEvents from websocket to read_queue. Returns callback function used with `on_message_callback` of websocket.WebSocketApp.""" + def callback(ws_app: WebSocketApp, msg: str): # We strongly trust that the contract on API will hold atm :D event_dict = json.loads(msg) labels = _LogEventLabels(**event_dict["labels"]) - # if "message" not in event_dict: - # print(">>>>>>>",component_name, event_dict) if "message" in event_dict: event = _LogEvent( message=event_dict["message"], From 4048fd5dd628dae93b524553a3f33cc23cc9108b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 11:44:21 +0000 Subject: [PATCH 03/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lightning_app/utilities/app_logs.py | 5 ++++- tests/tests_app_examples/test_boring_app.py | 2 +- tests/tests_app_examples/test_collect_failures.py | 2 +- tests/tests_app_examples/test_custom_work_dependencies.py | 2 +- tests/tests_app_examples/test_drive.py | 2 +- tests/tests_app_examples/test_idle_timeout.py | 2 +- tests/tests_app_examples/test_payload.py | 2 +- tests/tests_app_examples/test_quick_start.py | 2 +- tests/tests_app_examples/test_template_react_ui.py | 2 +- tests/tests_app_examples/test_template_streamlit_ui.py | 2 +- tests/tests_app_examples/test_v0_app.py | 2 +- 11 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/lightning_app/utilities/app_logs.py b/src/lightning_app/utilities/app_logs.py index effef144f1b5d..a092deef7dbff 100644 --- a/src/lightning_app/utilities/app_logs.py +++ b/src/lightning_app/utilities/app_logs.py @@ -34,7 +34,10 @@ class _LogEvent: def _push_logevents_to_read_queue_callback(component_name: str, read_queue: queue.PriorityQueue): - """Pushes _LogEvents from websocket to read_queue. Returns callback function used with `on_message_callback` of websocket.WebSocketApp.""" + """Pushes _LogEvents from websocket to read_queue. + + Returns callback function used with `on_message_callback` of websocket.WebSocketApp. + """ def callback(ws_app: WebSocketApp, msg: str): # We strongly trust that the contract on API will hold atm :D diff --git a/tests/tests_app_examples/test_boring_app.py b/tests/tests_app_examples/test_boring_app.py index 3650bab495be8..ac74e76de0be9 100644 --- a/tests/tests_app_examples/test_boring_app.py +++ b/tests/tests_app_examples/test_boring_app.py @@ -2,10 +2,10 @@ import pytest from click.testing import CliRunner +from tests_app import _PROJECT_ROOT from lightning_app.cli.lightning_cli import logs from lightning_app.testing.testing import run_app_in_cloud, wait_for -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_collect_failures.py b/tests/tests_app_examples/test_collect_failures.py index 7f20c4f32d67e..c149211e10774 100644 --- a/tests/tests_app_examples/test_collect_failures.py +++ b/tests/tests_app_examples/test_collect_failures.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_custom_work_dependencies.py b/tests/tests_app_examples/test_custom_work_dependencies.py index 1eae770f4c46c..d7c9db5ef610a 100644 --- a/tests/tests_app_examples/test_custom_work_dependencies.py +++ b/tests/tests_app_examples/test_custom_work_dependencies.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_drive.py b/tests/tests_app_examples/test_drive.py index ff027377c30a9..14efc3458716e 100644 --- a/tests/tests_app_examples/test_drive.py +++ b/tests/tests_app_examples/test_drive.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_idle_timeout.py b/tests/tests_app_examples/test_idle_timeout.py index 6838a348f30bc..a39ae3f693f7a 100644 --- a/tests/tests_app_examples/test_idle_timeout.py +++ b/tests/tests_app_examples/test_idle_timeout.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_payload.py b/tests/tests_app_examples/test_payload.py index b001ebbb4f557..58fc28a4a8d3c 100644 --- a/tests/tests_app_examples/test_payload.py +++ b/tests/tests_app_examples/test_payload.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_quick_start.py b/tests/tests_app_examples/test_quick_start.py index 947cd86f512c0..2b884bfb77cb2 100644 --- a/tests/tests_app_examples/test_quick_start.py +++ b/tests/tests_app_examples/test_quick_start.py @@ -4,12 +4,12 @@ import pytest from click.testing import CliRunner +from tests_app import _PROJECT_ROOT from lightning_app import LightningApp from lightning_app.cli.lightning_cli import run_app from lightning_app.testing.helpers import RunIf from lightning_app.testing.testing import run_app_in_cloud, wait_for -from tests_app import _PROJECT_ROOT class QuickStartApp(LightningApp): diff --git a/tests/tests_app_examples/test_template_react_ui.py b/tests/tests_app_examples/test_template_react_ui.py index 58379fab7b996..4b4588d2397e5 100644 --- a/tests/tests_app_examples/test_template_react_ui.py +++ b/tests/tests_app_examples/test_template_react_ui.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud, wait_for -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_template_streamlit_ui.py b/tests/tests_app_examples/test_template_streamlit_ui.py index 7f8ccce995daa..66ec92d3dde6f 100644 --- a/tests/tests_app_examples/test_template_streamlit_ui.py +++ b/tests/tests_app_examples/test_template_streamlit_ui.py @@ -2,9 +2,9 @@ from time import sleep import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import run_app_in_cloud, wait_for -from tests_app import _PROJECT_ROOT @pytest.mark.cloud diff --git a/tests/tests_app_examples/test_v0_app.py b/tests/tests_app_examples/test_v0_app.py index 09d1cb8780a13..4249ae9f9916b 100644 --- a/tests/tests_app_examples/test_v0_app.py +++ b/tests/tests_app_examples/test_v0_app.py @@ -3,10 +3,10 @@ from typing import Tuple import pytest +from tests_app import _PROJECT_ROOT from lightning_app.testing.testing import application_testing, LightningTestApp, run_app_in_cloud, wait_for from lightning_app.utilities.enum import AppStage -from tests_app import _PROJECT_ROOT class LightningAppTestInt(LightningTestApp): From 1c8102ff9295d96ec26fb38444c37eef0eb01a98 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Tue, 26 Jul 2022 15:41:15 +0200 Subject: [PATCH 04/19] up --- src/lightning_app/cli/lightning_cli.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index ac4668a607613..38dea954d1833 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -52,7 +52,7 @@ def _main(): register_all_external_components() -@main.group() +@_main.group() def show(): """Show info about resource.""" pass @@ -111,12 +111,6 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: click.echo(message, err=log_event.labels.stream == "stderr", color=True) -@main.command() -def _main(): - register_all_external_components() - pass - - @_main.command() def login(): """Log in to your Lightning.ai account.""" From 30c71c42298ddcf0620a37b5b15678768142345b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Jul 2022 13:45:11 +0000 Subject: [PATCH 05/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lightning_app/cli/lightning_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 38dea954d1833..7c6b96a40333a 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -16,14 +16,14 @@ from lightning_app.runners.runtime import dispatch from lightning_app.runners.runtime_type import RuntimeType from lightning_app.utilities.app_logs import _app_logs_reader -from lightning_app.utilities.cloud import _get_project -from lightning_app.utilities.network import LightningClient from lightning_app.utilities.cli_helpers import ( _format_input_env_variables, _retrieve_application_url_and_available_commands, ) +from lightning_app.utilities.cloud import _get_project from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth +from lightning_app.utilities.network import LightningClient from lightning_app.utilities.state import headers_for logger = logging.getLogger(__name__) From 4d73bba63993460a7045ca61d7242e66fddbfd78 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Wed, 27 Jul 2022 14:11:34 +0200 Subject: [PATCH 06/19] update --- src/lightning_app/CHANGELOG.md | 1 + src/lightning_app/cli/lightning_cli.py | 27 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md index 7d0dcb589b9e3..e7c49e4576628 100644 --- a/src/lightning_app/CHANGELOG.md +++ b/src/lightning_app/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added - Add support for `Lightning App Commands` through the `configure_commands` hook on the Lightning Flow and the `ClientCommand` ([#13602](https://github.com/Lightning-AI/lightning/pull/13602)) +- Add support for printing application logs using CLI `lightning show logs [components]` ([#13634](https://github.com/Lightning-AI/lightning/pull/13634)) ### Changed diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 38dea954d1833..33daba83dd998 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -49,20 +49,39 @@ def main(): @click.group() @click.version_option(ver) def _main(): - register_all_external_components() + # register_all_external_components() + pass @_main.group() def show(): - """Show info about resource.""" + """Show given resource.""" pass @show.command() @click.argument("app_name", required=False) @click.argument("components", nargs=-1, required=False) -@click.option("-f", "--follow", required=False, is_flag=True) +@click.option("-f", "--follow", required=False, is_flag=True, help="Wait for new logs, to exit use CTRL+C.") def logs(app_name: str, components: List[str], follow: bool) -> None: + """Show application logs. By default prints logs for all currently available components. + + Example uses: + + Print all application logs: + + $ lightning show logs my-application + + + Print logs only from the root flow component: + + $ lightning show logs my-application flow + + + Print logs only from selected components: + + $ lightning show logs my-application root.comp_a root.comp_b + """ client = LightningClient() project = _get_project(client) @@ -81,7 +100,7 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: ) if app_name not in apps: - raise click.ClickException(f"LightningApp '{app_name}' does not exist.") + raise click.ClickException(f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]") # Fetch all lightning works from given application # 'Flow' component is somewhat implicit, only one for whole app, From bb6fa12fd7683a274e7015b8186103989b264733 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:14:00 +0000 Subject: [PATCH 07/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lightning_app/cli/lightning_cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index cd4be03c54724..bfce5c815b20d 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -64,14 +64,14 @@ def show(): @click.argument("components", nargs=-1, required=False) @click.option("-f", "--follow", required=False, is_flag=True, help="Wait for new logs, to exit use CTRL+C.") def logs(app_name: str, components: List[str], follow: bool) -> None: - """Show application logs. By default prints logs for all currently available components. + """Show application logs. By default prints logs for all currently available components. Example uses: Print all application logs: $ lightning show logs my-application - + Print logs only from the root flow component: @@ -100,7 +100,9 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: ) if app_name not in apps: - raise click.ClickException(f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]") + raise click.ClickException( + f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]" + ) # Fetch all lightning works from given application # 'Flow' component is somewhat implicit, only one for whole app, From 01a2b245616445a1988133cfe968dcce66660141 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Wed, 27 Jul 2022 14:14:59 +0200 Subject: [PATCH 08/19] update --- src/lightning_app/cli/lightning_cli.py | 9 +++++---- src/lightning_app/utilities/logs_socket_api.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index cd4be03c54724..60e066ac85927 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -21,7 +21,6 @@ _retrieve_application_url_and_available_commands, ) from lightning_app.utilities.cloud import _get_project -from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth from lightning_app.utilities.network import LightningClient from lightning_app.utilities.state import headers_for @@ -64,14 +63,14 @@ def show(): @click.argument("components", nargs=-1, required=False) @click.option("-f", "--follow", required=False, is_flag=True, help="Wait for new logs, to exit use CTRL+C.") def logs(app_name: str, components: List[str], follow: bool) -> None: - """Show application logs. By default prints logs for all currently available components. + """Show application logs. By default prints logs for all currently available components. Example uses: Print all application logs: $ lightning show logs my-application - + Print logs only from the root flow component: @@ -100,7 +99,9 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: ) if app_name not in apps: - raise click.ClickException(f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]") + raise click.ClickException( + f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]" + ) # Fetch all lightning works from given application # 'Flow' component is somewhat implicit, only one for whole app, diff --git a/src/lightning_app/utilities/logs_socket_api.py b/src/lightning_app/utilities/logs_socket_api.py index b9beaa01874d4..0ab9a5c24f3e5 100644 --- a/src/lightning_app/utilities/logs_socket_api.py +++ b/src/lightning_app/utilities/logs_socket_api.py @@ -43,27 +43,31 @@ def create_lightning_logs_socket( .. code-block:: python # Synchronous reading, run_forever() is blocking + def print_log_msg(ws_app, msg): print(msg) - flow_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'flow', print_log_msg) + + flow_logs_socket = client.create_lightning_logs_socket("project_id", "app_id", "flow", print_log_msg) flow_socket.run_forever() .. code-block:: python # Asynchronous reading (with Threads) + def print_log_msg(ws_app, msg): print(msg) - flow_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'flow', print_log_msg) - work_logs_socket = client.create_lightning_logs_socket('project_id', 'app_id', 'work_1', print_log_msg) + + flow_logs_socket = client.create_lightning_logs_socket("project_id", "app_id", "flow", print_log_msg) + work_logs_socket = client.create_lightning_logs_socket("project_id", "app_id", "work_1", print_log_msg) flow_logs_thread = Thread(target=flow_logs_socket.run_forever) work_logs_thread = Thread(target=work_logs_socket.run_forever) flow_logs_thread.start() work_logs_thread.start() - ....... + # ....... flow_logs_socket.close() work_logs_thread.close() From 265a0f6a9698fb91254d458136e846fbd3a27b90 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Wed, 27 Jul 2022 14:49:45 +0200 Subject: [PATCH 09/19] update tests --- tests/tests_app_examples/test_boring_app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/tests_app_examples/test_boring_app.py b/tests/tests_app_examples/test_boring_app.py index ac74e76de0be9..f8143b1db1a88 100644 --- a/tests/tests_app_examples/test_boring_app.py +++ b/tests/tests_app_examples/test_boring_app.py @@ -27,7 +27,12 @@ def check_hello_there(*_, **__): runner = CliRunner() result = runner.invoke(logs, [name]) + lines = result.output.splitlines() assert result.exit_code == 0 assert result.exception is None - assert len(result.output.splitlines()) > 1, result.output + assert len(lines) > 1, result.output + # We know that at some point we need to intstall lightning, so we check for that + assert any( + "Successfully built lightning" in line for line in lines + ), f"Did not find logs with lightning installation: {result.output}" From 1312101dd0d2e6ae36b6d66807f55bf3f0323345 Mon Sep 17 00:00:00 2001 From: Adam Bobowski Date: Wed, 27 Jul 2022 15:48:48 +0200 Subject: [PATCH 10/19] fix test --- tests/tests_app_examples/test_commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tests_app_examples/test_commands.py b/tests/tests_app_examples/test_commands.py index 5116b1b9d54bb..266f0305c7604 100644 --- a/tests/tests_app_examples/test_commands.py +++ b/tests/tests_app_examples/test_commands.py @@ -16,6 +16,7 @@ def test_commands_example_cloud() -> None: admin_page, _, fetch_logs, + _, ): app_id = admin_page.url.split("/")[-1] cmd = f"lightning trigger_with_client_command --name=something --app_id {app_id}" From 4b01eaaac307d8315d6ec8e7a97240b742411db7 Mon Sep 17 00:00:00 2001 From: Adam Bobowski <100693297+adam-lightning@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:23:28 +0200 Subject: [PATCH 11/19] Update src/lightning_app/cli/lightning_cli.py Co-authored-by: thomas chaton --- src/lightning_app/cli/lightning_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 1e7929bf0d9e2..82184e3beab47 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -66,7 +66,7 @@ def show(): @click.argument("components", nargs=-1, required=False) @click.option("-f", "--follow", required=False, is_flag=True, help="Wait for new logs, to exit use CTRL+C.") def logs(app_name: str, components: List[str], follow: bool) -> None: - """Show application logs. By default prints logs for all currently available components. + """Show cloud application logs. By default prints logs for all currently available components. Example uses: From 04829519c35f6bd0d7df8245974cbfca2ad46ecf Mon Sep 17 00:00:00 2001 From: Adam Bobowski <100693297+adam-lightning@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:23:36 +0200 Subject: [PATCH 12/19] Update src/lightning_app/cli/lightning_cli.py Co-authored-by: thomas chaton --- src/lightning_app/cli/lightning_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 82184e3beab47..59b760ebb1439 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -98,7 +98,7 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: if not app_name: raise click.ClickException( - f"You have not specified any LightningApp. Please select one of available: [{', '.join(apps.keys())}]" + f"You have not specified any Lightning App. Please select one of available: [{', '.join(apps.keys())}]" ) if app_name not in apps: From 4d36b708ee972f69fdcf973800400bf0d0dd3782 Mon Sep 17 00:00:00 2001 From: Adam Bobowski <100693297+adam-lightning@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:23:41 +0200 Subject: [PATCH 13/19] Update src/lightning_app/cli/lightning_cli.py Co-authored-by: thomas chaton --- src/lightning_app/cli/lightning_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 59b760ebb1439..77a24b40d6679 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -94,7 +94,7 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: } if not apps: - raise click.ClickException("Your app list is empty. Please run an application first.") + raise click.ClickException("You don't have any application in the cloud. Please, run an application first with `--cloud`.") if not app_name: raise click.ClickException( From ad6ebb4949b1110e5fcd5a8426ba3450c3fcffc0 Mon Sep 17 00:00:00 2001 From: Adam Bobowski <100693297+adam-lightning@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:23:47 +0200 Subject: [PATCH 14/19] Update src/lightning_app/cli/lightning_cli.py Co-authored-by: thomas chaton --- src/lightning_app/cli/lightning_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 77a24b40d6679..28d20ab0d5888 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -103,7 +103,7 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: if app_name not in apps: raise click.ClickException( - f"LightningApp '{app_name}' does not exist. Please select one of available: [{', '.join(apps.keys())}]" + f"The Lightning App '{app_name}' does not exist. Please select one of following: [{', '.join(apps.keys())}]" ) # Fetch all lightning works from given application From da1b6998437abbe9ec89028af31e2102365adc26 Mon Sep 17 00:00:00 2001 From: Adam Bobowski <100693297+adam-lightning@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:25:06 +0200 Subject: [PATCH 15/19] up --- src/lightning_app/cli/lightning_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 28d20ab0d5888..36c54a5be3471 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -24,6 +24,7 @@ _retrieve_application_url_and_available_commands, ) from lightning_app.utilities.cloud import _get_project +from lightning_app.utilities.install_components import register_all_external_components from lightning_app.utilities.login import Auth from lightning_app.utilities.network import LightningClient from lightning_app.utilities.state import headers_for @@ -51,8 +52,7 @@ def main(): @click.group() @click.version_option(ver) def _main(): - # register_all_external_components() - pass + register_all_external_components() @_main.group() From cdee98cca461135173c51553571bc09e52e105e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Aug 2022 14:26:40 +0000 Subject: [PATCH 16/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lightning_app/cli/lightning_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 36c54a5be3471..50d2704232d20 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -94,7 +94,9 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: } if not apps: - raise click.ClickException("You don't have any application in the cloud. Please, run an application first with `--cloud`.") + raise click.ClickException( + "You don't have any application in the cloud. Please, run an application first with `--cloud`." + ) if not app_name: raise click.ClickException( From 054ebdb10738ee976319d852ab1e3083f1ce6ff1 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Wed, 10 Aug 2022 09:43:09 +0200 Subject: [PATCH 17/19] update --- src/lightning_app/cli/lightning_cli.py | 6 +++--- tests/tests_app/cli/test_cmd_show_logs.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 50d2704232d20..07904957d219b 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -75,14 +75,14 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: $ lightning show logs my-application - Print logs only from the root flow component: + Print logs only from the flow (no work): $ lightning show logs my-application flow - Print logs only from selected components: + Print logs only from selected works: - $ lightning show logs my-application root.comp_a root.comp_b + $ lightning show logs my-application root.work_a root.work_b """ client = LightningClient() diff --git a/tests/tests_app/cli/test_cmd_show_logs.py b/tests/tests_app/cli/test_cmd_show_logs.py index 2c83aa5440d81..0dc06025151fa 100644 --- a/tests/tests_app/cli/test_cmd_show_logs.py +++ b/tests/tests_app/cli/test_cmd_show_logs.py @@ -27,7 +27,7 @@ def test_show_logs_errors(project, client): result = runner.invoke(logs, ["NonExistentApp"]) assert result.exit_code == 1 - assert "Your app list is empty. Please run an application first." in result.output + assert "Error: You don't have any application in the cloud" in result.output # App not specified apps = {app} @@ -36,7 +36,7 @@ def test_show_logs_errors(project, client): result = runner.invoke(logs) assert result.exit_code == 1 - assert "You have not specified any LightningApp. Please select one of available: [MyFakeApp]" in result.output + assert "Please select one of available: [MyFakeApp]" in str(result.output) # App does not exit apps = {app} @@ -45,7 +45,7 @@ def test_show_logs_errors(project, client): result = runner.invoke(logs, ["ThisAppDoesNotExist"]) assert result.exit_code == 1 - assert "LightningApp 'ThisAppDoesNotExist' does not exist." in result.output + assert "The Lightning App 'ThisAppDoesNotExist' does not exist." in str(result.output) # Component does not exist apps = {app} From 8095aa1cc51deab76b78f4156f40ea4a3cd1ec13 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Wed, 10 Aug 2022 10:43:24 +0200 Subject: [PATCH 18/19] update --- src/lightning_app/utilities/app_logs.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lightning_app/utilities/app_logs.py b/src/lightning_app/utilities/app_logs.py index a092deef7dbff..4a7af9b5c5143 100644 --- a/src/lightning_app/utilities/app_logs.py +++ b/src/lightning_app/utilities/app_logs.py @@ -2,7 +2,7 @@ import queue import sys from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from json import JSONDecodeError from threading import Thread from typing import Iterator, List, Optional, Tuple @@ -92,13 +92,20 @@ def _app_logs_reader( for th in log_threads: th.start() + user_log_start = "<<< BEGIN USER_RUN_FLOW SECTION >>>" + start_timestamp = None + # Print logs from queue when log event is available try: while True: _, component_name, log_event = read_queue.get(timeout=None if follow else 1.0) log_event: _LogEvent - yield component_name, log_event + if user_log_start in log_event.message: + start_timestamp = log_event.timestamp + timedelta(seconds=0.5) + + if start_timestamp and log_event.timestamp > start_timestamp: + yield component_name, log_event except queue.Empty: # Empty is raised by queue.get if timeout is reached. Follow = False case. From 688c37048c86ce944d0e9bf04258e25b747663d4 Mon Sep 17 00:00:00 2001 From: thomas chaton Date: Wed, 10 Aug 2022 11:05:27 +0200 Subject: [PATCH 19/19] update --- src/lightning_app/cli/lightning_cli.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 07904957d219b..45c80d4dcc357 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -8,7 +8,9 @@ import click import requests +import rich from requests.exceptions import ConnectionError +from rich.color import ANSI_COLOR_NAMES from lightning_app import __version__ as ver from lightning_app.cli import cmd_init, cmd_install, cmd_pl_init, cmd_react_ui_init @@ -131,9 +133,13 @@ def logs(app_name: str, components: List[str], follow: bool) -> None: follow=follow, ) + rich_colors = list(ANSI_COLOR_NAMES) + colors = {c: rich_colors[i + 1] for i, c in enumerate(components)} + for component_name, log_event in log_reader: - message = f"[{component_name}] {log_event.timestamp.isoformat()} {log_event.message}" - click.echo(message, err=log_event.labels.stream == "stderr", color=True) + date = log_event.timestamp.strftime("%m/%d/%Y %H:%M:%S") + color = colors[component_name] + rich.print(f"[{color}]{component_name}[/{color}] {date} {log_event.message}") @_main.command()