Spaces:
Running
Running
# `gradio_logsview` | |
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange"> | |
Visualize logs in your Gradio app | |
## Installation | |
```bash | |
pip install gradio_logsview | |
``` | |
## Usage | |
```python | |
import logging | |
import random | |
import time | |
import gradio as gr | |
from gradio_logsview import LogsView | |
def random_values(failing: bool = False): | |
for i in range(10): | |
logging.log( | |
random.choice( | |
[ # Random levels | |
logging.INFO, | |
logging.DEBUG, | |
logging.WARNING, | |
logging.ERROR, | |
logging.CRITICAL, | |
] | |
), | |
f"Value {i+1}", # Random values | |
) | |
time.sleep(random.uniform(0, 1)) | |
if failing and i == 5: | |
raise ValueError("Failing!!") | |
def fn_process_success(): | |
yield from LogsView.run_process(["python", "-u", "demo/script.py"]) | |
def fn_process_failing(): | |
yield from LogsView.run_process(["python", "-u", "demo/script.py", "--failing"]) | |
def fn_thread_success(): | |
yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=False) | |
def fn_thread_failing(): | |
yield from LogsView.run_thread(random_values, log_level=logging.INFO, failing=True) | |
markdown_top = """ | |
# LogsView Demo | |
This demo shows how to use the `LogsView` component to display logs from a process or a thread in real-time. | |
Click on any button to launch a process or a thread and see the logs displayed in real-time. | |
In the thread example, logs are generated randomly with different log levels. | |
In the process example, logs are generated by a Python script but any command can be executed. | |
""" | |
markdown_bottom = """ | |
## How to run in a thread? | |
With `LogsView.run_thread`, you can run a function in a separate thread and capture logs in real-time. | |
You can configure which logs to capture (log level and logger name). | |
```py | |
from gradio_logsview import LogsView | |
def fn_thread(): | |
# Run `my_function` in a separate thread | |
# All logs above `INFO` level will be captured and displayed in real-time. | |
yield from LogsView.run_thread(my_function, log_level=logging.INFO, arg1="value1") | |
with gr.Blocks() as demo: | |
logs = LogsView() | |
btn = gr.Button("Run thread") | |
btn.click(fn_thread, outputs=logs) | |
``` | |
## How to run in a process? | |
With `LogsView.run_process`, you can run a command in a separate process and capture logs from the process in real-time. | |
```py | |
from gradio_logsview import LogsView | |
def fn_process(): | |
# Run a process and capture all logs from the process | |
yield from LogsView.run_process( | |
cmd=[mergekit-yaml", "config.yaml", "merge", "--copy-", "--cuda", "--low-cpu-memory"] | |
) | |
with gr.Blocks() as demo: | |
logs = LogsView() | |
btn = gr.Button("Run process") | |
btn.click(fn_process, outputs=logs) | |
``` | |
""" | |
with gr.Blocks() as demo: | |
gr.Markdown(markdown_top) | |
with gr.Row(): | |
btn_thread_success = gr.Button("Run thread (success)") | |
btn_thread_failing = gr.Button("Run thread (failing)") | |
with gr.Row(): | |
btn_process_success = gr.Button("Run process (success)") | |
btn_process_failing = gr.Button("Run process (failing)") | |
logs = LogsView() | |
gr.Markdown(markdown_bottom) | |
btn_thread_failing.click(fn_thread_failing, outputs=logs) | |
btn_thread_success.click(fn_thread_success, outputs=logs) | |
btn_process_failing.click(fn_process_failing, outputs=logs) | |
btn_process_success.click(fn_process_success, outputs=logs) | |
if __name__ == "__main__": | |
demo.launch() | |
``` | |
## `LogsView` | |
### Initialization | |
<table> | |
<thead> | |
<tr> | |
<th align="left">name</th> | |
<th align="left" style="width: 25%;">type</th> | |
<th align="left">default</th> | |
<th align="left">description</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td align="left"><code>value</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
str | Callable | tuple[str] | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">Default value to show in the code editor. If callable, the function will be called whenever the app loads to set the initial value of the component.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>every</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
float | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>lines</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
int | |
``` | |
</td> | |
<td align="left"><code>5</code></td> | |
<td align="left">None</td> | |
</tr> | |
<tr> | |
<td align="left"><code>label</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
str | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>show_label</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
bool | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">if True, will display label.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>container</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
bool | |
``` | |
</td> | |
<td align="left"><code>True</code></td> | |
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>scale</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
int | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>min_width</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
int | |
``` | |
</td> | |
<td align="left"><code>160</code></td> | |
<td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>visible</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
bool | |
``` | |
</td> | |
<td align="left"><code>True</code></td> | |
<td align="left">If False, component will be hidden.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>elem_id</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
str | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>elem_classes</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
list[str] | str | None | |
``` | |
</td> | |
<td align="left"><code>None</code></td> | |
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td> | |
</tr> | |
<tr> | |
<td align="left"><code>render</code></td> | |
<td align="left" style="width: 25%;"> | |
```python | |
bool | |
``` | |
</td> | |
<td align="left"><code>True</code></td> | |
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td> | |
</tr> | |
</tbody></table> | |
### Events | |
| name | description | | |
|:-----|:------------| | |
| `change` | Triggered when the value of the LogsView changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. | | |
| `input` | This listener is triggered when the user changes the value of the LogsView. | | |
| `focus` | This listener is triggered when the LogsView is focused. | | |
| `blur` | This listener is triggered when the LogsView is unfocused/blurred. | | |
### User function | |
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both). | |
- When used as an Input, the component only impacts the input signature of the user function. | |
- When used as an output, the component only impacts the return signature of the user function. | |
The code snippet below is accurate in cases where the component is used as both an input and an output. | |
- **As output:** Is passed, passes the code entered as a `str`. | |
- **As input:** Should return, expects a list of `Log` logs. | |
```python | |
def predict( | |
value: LogsView | |
) -> list[Log]: | |
return value | |
``` | |
## `Log` | |
```python | |
@dataclass | |
class Log: | |
level: Literal[ | |
"INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL" | |
] | |
message: str | |
timestamp: str | |
``` | |
## `LogsView` | |
```python | |
class LogsView(Component): | |
EVENTS = [ | |
Events.change, | |
Events.input, | |
Events.focus, | |
Events.blur, | |
] | |
def __init__( | |
self, | |
value: str | Callable | tuple[str] | None = None, | |
*, | |
every: float | None = None, | |
lines: int = 5, | |
label: str | None = None, | |
show_label: bool | None = None, | |
container: bool = True, | |
scale: int | None = None, | |
min_width: int = 160, | |
visible: bool = True, | |
elem_id: str | None = None, | |
elem_classes: list[str] | str | None = None, | |
render: bool = True, | |
): | |
self.language = "shell" | |
self.lines = lines | |
self.interactive = False | |
super().__init__( | |
label=label, | |
every=every, | |
show_label=show_label, | |
container=container, | |
scale=scale, | |
min_width=min_width, | |
visible=visible, | |
elem_id=elem_id, | |
elem_classes=elem_classes, | |
render=render, | |
value=value, | |
) | |
def preprocess(self, payload: str | None) -> "LogsView": | |
raise NotImplementedError( | |
"LogsView cannot be used as an input component." | |
) | |
def postprocess(self, value: List[Log]) -> List[Log]: | |
return value | |
def api_info(self) -> dict[str, Any]: | |
return { | |
"items": { | |
"level": "string", | |
"message": "string", | |
"timestamp": "number", | |
}, | |
"title": "Logs", | |
"type": "array", | |
} | |
def example_payload(self) -> Any: | |
return [ | |
Log( | |
"INFO", | |
"Hello World", | |
datetime.now().isoformat(), | |
) | |
] | |
def example_value(self) -> Any: | |
return [ | |
Log( | |
"INFO", | |
"Hello World", | |
datetime.now().isoformat(), | |
) | |
] | |
@classmethod | |
def run_process( | |
cls, | |
command: List[str], | |
date_format: str = "%Y-%m-%d %H:%M:%S", | |
) -> Generator[List[Log], None, None]: | |
process = subprocess.Popen( | |
command, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.STDOUT, | |
text=True, | |
) | |
if process.stdout is None: | |
raise ValueError("stdout is None") | |
logs = [] | |
def _log(level: str, message: str): | |
log = Log( | |
level=level, | |
message=message, | |
timestamp=datetime.now().strftime( | |
date_format | |
), | |
) | |
logs.append(log) | |
return logs | |
_log("INFO", f"Running {' '.join(command)}") | |
for line in process.stdout: | |
yield _log("INFO", line.strip()) | |
# TODO: what if task is cancelled but process is still running? | |
process.stdout.close() | |
return_code = process.wait() | |
if return_code: | |
yield _log( | |
"ERROR", | |
f"Process exited with code {return_code}", | |
) | |
else: | |
yield _log( | |
"INFO", "Process exited successfully" | |
) | |
@classmethod | |
def run_thread( | |
cls, | |
fn: Callable, | |
log_level: int = logging.INFO, | |
logger_name: str | None = None, | |
date_format: str = "%Y-%m-%d %H:%M:%S", | |
**kwargs, | |
) -> Generator[List[Log], None, None]: | |
logs = [ | |
Log( | |
level="INFO", | |
message=f"Running {fn.__name__}({', '.join(f'{k}={v}' for k, v in kwargs.items())})", | |
timestamp=datetime.now().strftime( | |
date_format | |
), | |
) | |
] | |
yield logs | |
thread = Thread( | |
target=non_failing_fn(fn), kwargs=kwargs | |
) | |
def _log(record: logging.LogRecord) -> bool: | |
if record.thread != thread.ident: | |
return False # Skip if not from the thread | |
if logger_name and not record.name.startswith( | |
logger_name | |
): | |
return False # Skip if not from the logger | |
if record.levelno < log_level: | |
return False # Skip if too verbose | |
log = Log( | |
level=record.levelname, | |
message=record.getMessage(), | |
timestamp=datetime.fromtimestamp( | |
record.created | |
).strftime(date_format), | |
) | |
logs.append(log) | |
return True | |
with capture_logging(log_level) as log_queue: | |
thread.start() | |
# Loop to capture and yield logs from the thread | |
while thread.is_alive(): | |
while True: | |
try: | |
if _log(log_queue.get_nowait()): | |
yield logs | |
except queue.Empty: | |
break | |
thread.join( | |
timeout=0.1 | |
) # adjust the timeout as needed | |
# After the thread completes, yield any remaining logs | |
while True: | |
try: | |
if _log(log_queue.get_nowait()): | |
yield logs | |
except queue.Empty: | |
break | |
logs.append( | |
Log( | |
level="INFO", | |
message="Thread completed successfully", | |
timestamp=datetime.now().strftime( | |
date_format | |
), | |
) | |
) | |
``` | |