-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[App] Application logs in CLI #13634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cb233c6
fc6d578
4048fd5
e952ed1
624441c
86926a1
1c8102f
30c71c4
4d73bba
b5ad673
bb6fa12
01a2b24
fca96a1
265a0f6
1312101
d7b76fc
5701216
4b01eaa
0482951
4d36b70
ad6ebb4
da1b699
cdee98c
054ebdb
7e01a75
8095aa1
688c370
6a86216
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import json | ||
| import queue | ||
| import sys | ||
| from dataclasses import dataclass | ||
| from datetime import datetime, timedelta | ||
| 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 | ||
|
|
||
|
|
||
| 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) | ||
manskx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| labels = _LogEventLabels(**event_dict["labels"]) | ||
| 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)) | ||
adam-lightning marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
manskx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 = [ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably works well for some apps, and is a good start. I just want to point out that an app doesn't need to have a constant amount of "components". These could come and go during runtime of the app. So if you "follow" the logs, that's probably useful to know.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is definitely a skateboard version and needs to be improved upon. |
||
| 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() | ||
|
|
||
| user_log_start = "<<< BEGIN USER_RUN_FLOW SECTION >>>" | ||
| start_timestamp = None | ||
|
|
||
| # Print logs from queue when log event is available | ||
| try: | ||
| while True: | ||
tchaton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _, component_name, log_event = read_queue.get(timeout=None if follow else 1.0) | ||
| log_event: _LogEvent | ||
|
|
||
| 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. | ||
| 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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| 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: | ||
manskx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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( | ||
manskx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
Uh oh!
There was an error while loading. Please reload this page.